├── .codesandbox └── tasks.json ├── .dockerignore ├── .env.development ├── .env.production ├── .env.test ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ ├── question.md │ └── rfc.md ├── PULL_REQUEST_TEMPLATE │ ├── standard_pr.md │ └── trivial_pr.md ├── pull.yml └── workflows │ └── test_unit.yml ├── .gitignore ├── .markdownlint.jsonc ├── .nvmrc ├── .prettierrc ├── .storybook ├── babel.config.js ├── backgrounds.ts ├── main.ts ├── manager.ts ├── mocks │ └── packages │ │ ├── next-config.js │ │ └── next-router.js ├── preview-head.html ├── preview.tsx ├── themes │ └── vulcanStorybook.js └── tsconfig.json ├── .vn ├── docker │ ├── cypress.dockerfile │ └── vn.production.dockerfile ├── jest.config.vn.js ├── nextConfig │ ├── extendEnv.ts │ ├── redirectsAndRewrites.js │ ├── withI18n.js │ └── withPkgInfo.js ├── scripts │ ├── README.md │ ├── build-scripts.sh │ ├── fix-apollo.js │ ├── is-monorepo.js │ ├── js-generated │ │ ├── reset.mjs │ │ ├── reset.mjs.map │ │ ├── seed.mjs │ │ └── seed.mjs.map │ ├── link-vulcan.sh │ ├── ts-sources │ │ └── db │ │ │ ├── reset.ts │ │ │ └── seed.ts │ └── tsconfig.json ├── stories │ ├── 0-Welcome.stories.js │ └── vns │ │ ├── publicImage.stories.tsx │ │ └── typescript.stories.tsx ├── tests │ ├── __mocks__ │ │ ├── fileMock.js │ │ ├── next │ │ │ └── router.ts │ │ └── styleMock.js │ ├── configTests │ │ ├── exampleClient.test.js │ │ ├── exampleServer.server.test.js │ │ └── extendEnv.test.js │ ├── globalSetup.js │ ├── setupTests.server.js │ ├── setupTests.ts │ └── vn │ │ ├── api │ │ └── auth.server.test.ts │ │ ├── coverage.test.ts │ │ ├── mongoConnection.server.test.ts │ │ ├── mongoDocker.test.ts │ │ ├── nextConfig.test.ts │ │ └── packages.test.ts └── tsconfig.json ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── vns.code-snippets ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── babel.config.jest.js ├── babel.config.js ├── cypress.json ├── cypress ├── cypress.json ├── examples │ ├── actions.spec.js │ ├── aliasing.spec.js │ ├── assertions.spec.js │ ├── connectors.spec.js │ ├── cookies.spec.js │ ├── cypress_api.spec.js │ ├── files.spec.js │ ├── local_storage.spec.js │ ├── location.spec.js │ ├── misc.spec.js │ ├── navigation.spec.js │ ├── network_requests.spec.js │ ├── querying.spec.js │ ├── spies_stubs_clocks.spec.js │ ├── traversal.spec.js │ ├── utilities.spec.js │ ├── viewport.spec.js │ ├── waiting.spec.js │ └── window.spec.js ├── fixtures │ ├── example.json │ ├── profile.json │ └── users.json ├── integration │ ├── e2e │ │ ├── api.spec.ts │ │ └── auth.spec.ts │ └── integration │ │ ├── auth.spec.ts │ │ ├── basic.spec.js │ │ ├── basic.spec.ts │ │ ├── doc.spec.ts │ │ ├── i18n.spec.ts │ │ ├── mongo.spec.ts │ │ ├── mui-styled-components.spec.ts │ │ ├── mui.spec.ts │ │ ├── packageInfo.spec.ts │ │ ├── private.spec.ts │ │ └── ssr.spec.ts ├── plugins │ ├── cy-webpack-preprocessor.js │ ├── index.js │ ├── load-env.js │ └── mail.js ├── support │ ├── after.ts │ ├── before.ts │ ├── commands.ts │ ├── commands │ │ ├── example.ts │ │ ├── i18n.ts │ │ └── ssr.ts │ ├── index.d.ts │ └── index.js ├── tsconfig.json └── webpack.config.js ├── jest.config.js ├── next-env.d.ts ├── next-i18next.config.js ├── next.config.js ├── nyc.config.js ├── package.json ├── packages ├── @vulcanjs │ ├── next-apollo │ │ ├── apolloClient.ts │ │ ├── index.ts │ │ ├── links │ │ │ └── error.ts │ │ ├── package.json │ │ └── tsconfig.json │ ├── next-config │ │ ├── extendNextConfig.js │ │ └── index.js │ ├── next-mui │ │ ├── components │ │ │ ├── Link.tsx │ │ │ ├── LinkOld.tsx.old │ │ │ ├── NextMuiButton.tsx │ │ │ └── stories │ │ │ │ └── Link.stories.tsx │ │ ├── emotion │ │ │ └── createEmotionCache.ts │ │ ├── index.ts │ │ ├── package.json │ │ ├── server.ts │ │ └── server │ │ │ └── getAppEnhancer.tsx │ ├── next-style-collector │ │ ├── index.ts │ │ └── package.json │ ├── next-utils │ │ ├── index.ts │ │ ├── package.json │ │ ├── routing.tsx.old │ │ ├── ssr.ts │ │ └── tsconfig.json │ └── webpack │ │ ├── extendWebpackConfig.js │ │ └── index.js ├── README.md └── tsconfig.json ├── postcss.config.js ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── img │ ├── docs │ │ └── env_token_secret.png │ ├── letter-96x96.png │ ├── vn-logo-full-1280-640.png │ ├── vn-logo-full-600.png │ ├── vn-logo-full-padded-720.png │ ├── vn-logo-full-padded-840.png │ ├── vn-logo-full-padded-dark-840.png │ ├── vns-logo-32.png │ ├── vns-logo-64.png │ └── vns-logo-96.png ├── locales │ ├── en │ │ └── common.json │ └── fr │ │ └── common.json ├── mstile-150x150.png ├── safari-pinned-tab.svg ├── site.webmanifest ├── vulcan-next-banner.svg └── vulcan-next-banner_800.png ├── src ├── account │ ├── components │ │ ├── ChangePassword.tsx │ │ ├── ErrorSuccessMessages.tsx │ │ ├── form.tsx │ │ ├── hooks.ts │ │ ├── layout.tsx │ │ └── tests │ │ │ └── changePassword.stories.tsx │ ├── models │ │ ├── storableToken.server.ts │ │ ├── user.server.ts │ │ └── user.ts │ └── server │ │ ├── accountManagement.ts │ │ ├── auth-cookies.ts │ │ ├── emails │ │ ├── changePasswordSuccess.tsx │ │ ├── resetPasswordSuccess.tsx │ │ ├── resetPasswordToken.tsx │ │ └── verifyEmail.tsx │ │ ├── index.ts │ │ ├── passport │ │ └── password-local.ts │ │ ├── session.ts │ │ └── utils.ts ├── core │ ├── README.md │ ├── components │ │ ├── home │ │ │ ├── home.tsx │ │ │ ├── index.tsx │ │ │ ├── stories │ │ │ │ └── home.stories.tsx │ │ │ └── tests │ │ │ │ └── home.test.tsx │ │ ├── layout │ │ │ ├── AppLayout.tsx │ │ │ ├── Footer.tsx │ │ │ ├── GitHubButtonsHeader.tsx │ │ │ ├── MuiMdxLayout.tsx │ │ │ ├── PageLayout.tsx │ │ │ ├── index.ts │ │ │ └── muiMdComponents.tsx │ │ ├── providers │ │ │ ├── MuiThemeProvider.tsx │ │ │ ├── index.ts │ │ │ └── stories │ │ │ │ └── MuiThemeProvider.stories.tsx │ │ └── ui │ │ │ ├── EmotionButton.tsx │ │ │ ├── Modal.tsx │ │ │ ├── StyledJsxButton.tsx │ │ │ └── stories │ │ │ ├── 0_mdx-example.stories.mdx │ │ │ ├── button.stories.tsx │ │ │ └── modal.stories.tsx │ ├── lib │ │ ├── debuggers.ts │ │ ├── example-helper.ts │ │ ├── i18n.ts │ │ └── utils.ts │ ├── models.server.ts │ ├── models.ts │ ├── routes.ts │ ├── server │ │ ├── apiRoutes.ts │ │ ├── context.ts │ │ ├── cors.ts │ │ ├── mail │ │ │ └── transports.ts │ │ ├── middlewares │ │ │ ├── mongoAppConnection.ts │ │ │ └── mongoConnection.ts │ │ ├── mongoose │ │ │ ├── connection.ts │ │ │ └── models.ts │ │ ├── runSeed.ts │ │ ├── seed.ts │ │ └── utils.ts │ └── style │ │ ├── colors.ts │ │ ├── darkTheme.ts │ │ ├── defaultTheme.ts │ │ └── typography.ts ├── learn │ └── components │ │ ├── GetHelp.mdx │ │ ├── GraphqlQueryEditor.tsx │ │ ├── LearnLayout.tsx │ │ ├── MultipleChoiceQuestion.tsx │ │ └── Steps.tsx ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── account │ │ ├── forgotten-password.tsx │ │ ├── login.tsx │ │ ├── profile.tsx │ │ ├── reset-password │ │ │ └── [token].tsx │ │ ├── signup.tsx │ │ └── verify-email │ │ │ ├── [token].tsx │ │ │ └── index.tsx │ ├── api │ │ ├── account │ │ │ ├── changePassword.ts │ │ │ ├── login.ts │ │ │ ├── logout.ts │ │ │ ├── reset-password.tsx │ │ │ ├── send-reset-password-email.ts │ │ │ ├── send-verification-email.ts │ │ │ ├── signup.ts │ │ │ ├── user.ts │ │ │ └── verify-email.ts │ │ ├── debug │ │ │ └── graphql-voyager.ts │ │ └── graphql.ts │ ├── index.tsx │ └── vn │ │ ├── admin │ │ ├── crud │ │ │ ├── VulcanUser.tsx │ │ │ ├── [modelName].tsx │ │ │ └── index.tsx │ │ └── index.tsx │ │ ├── debug │ │ ├── _middleware.ts │ │ ├── about.tsx │ │ ├── apolloSsr.tsx │ │ ├── css.tsx │ │ ├── emotion-mui.tsx │ │ ├── i18n.tsx │ │ ├── meteor.tsx │ │ ├── mongo.tsx │ │ ├── mui.tsx │ │ ├── noApolloSsr.tsx │ │ ├── private-initial-props.tsx.old │ │ ├── private-raw.tsx │ │ ├── private.tsx.old │ │ ├── private.tsx.old2 │ │ └── public.tsx │ │ ├── docs │ │ └── [[...filePath]].tsx │ │ ├── examples │ │ └── [M] │ │ │ ├── _middleware.ts │ │ │ └── megaparam-demo.tsx │ │ └── learn │ │ ├── about-models.mdx │ │ ├── final.mdx │ │ ├── graphql-server.mdx │ │ ├── index.tsx │ │ ├── intro-offline.mdx │ │ ├── intro-online.mdx │ │ ├── mongo.mdx │ │ └── server-only-models.mdx ├── stories │ ├── Button.stories.tsx │ ├── Button.tsx │ ├── Header.stories.tsx │ ├── Header.tsx │ ├── Introduction.stories.mdx │ ├── Page.stories.tsx │ ├── Page.tsx │ ├── assets │ │ ├── code-brackets.svg │ │ ├── colors.svg │ │ ├── comments.svg │ │ ├── direction.svg │ │ ├── flow.svg │ │ ├── plugin.svg │ │ ├── repo.svg │ │ └── stackalt.svg │ ├── button.css │ ├── header.css │ └── page.css ├── tsconfig.json ├── types │ ├── css.d.ts │ ├── mdx.d.ts │ └── simpl-schema │ │ └── index.d.ts └── vulcan-demo │ ├── README.md │ ├── components │ ├── ItemCard.tsx │ ├── cssModule.module.css │ ├── cssModule.tsx │ ├── stories │ │ ├── cssModule.stories.tsx │ │ ├── i18n.stories.tsx │ │ └── styledJsx.stories.tsx │ ├── styledJsx.tsx │ └── styledJsxPostcss.tsx │ ├── content │ └── vn │ │ └── docs │ │ └── features │ │ └── index.md │ └── models │ ├── sampleModel.server.ts │ ├── sampleModel.ts │ └── tests │ └── sampleModel.server.test.ts.skipped ├── tsconfig.common.json ├── tsconfig.json ├── tsup.config.ts ├── vercel.json ├── vulcan-next-sitemap.js └── yarn.vulcan-next.lock /.codesandbox/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // These tasks will run in order when initializing your CodeSandbox project. 3 | // @see https://docs.codesandbox.io/our-products/codesandbox-projects/configuration-tasks#80f443ed033a4c86b79d88568ab5137e 4 | "setupTasks": [ 5 | { 6 | "name": "Install Dependencies", 7 | "command": "yarn install" 8 | }, 9 | { 10 | "name": "Init CodeSandbox", 11 | "command": "yarn init:codesandbox" 12 | } 13 | ], 14 | // These tasks can be run from CodeSandbox. Running one will open a log in the app. 15 | // Optionally enable commands from package.json here 16 | "tasks": { 17 | "mongo": { 18 | "name": "mongo", 19 | "command": "yarn mongo", 20 | "runAtStart": true 21 | }, 22 | "build": { 23 | "name": "build", 24 | "command": "yarn build", 25 | "runAtStart": false 26 | }, 27 | "dev": { 28 | "name": "dev", 29 | "command": "yarn dev", 30 | "runAtStart": true 31 | }, 32 | "test": { 33 | "name": "test", 34 | "command": "yarn test", 35 | "runAtStart": false 36 | }, 37 | "test:unit": { 38 | "name": "test:unit", 39 | "command": "yarn test:unit", 40 | "runAtStart": false 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # now 27 | .now 28 | 29 | # Tests 30 | cypress/screenshots 31 | cypress/videos 32 | coverage-e2e/ 33 | .nyc_output/ 34 | coverage-unit 35 | reports/ 36 | 37 | # mdx enhanced 38 | .mdx-data 39 | 40 | # mongo 41 | .mongo 42 | 43 | # next-sitemap 44 | /public/sitemap.xml 45 | /public/robots.txt 46 | 47 | 48 | # Yalc 49 | .yalc 50 | yalc.lock 51 | .yalc/ 52 | 53 | # Storybook 54 | build-storybook.log 55 | storybook-static -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT .env.development! (except during development) 2 | # COPY ME in ".env.development.local" beforehand 3 | 4 | # ## App setup 5 | 6 | # Setup in production with your final main URL 7 | # With port and protocol: https://origin:port 8 | APP_URL= 9 | 10 | 11 | # ## Environment 12 | 13 | NEXT_PUBLIC_NODE_ENV=development 14 | 15 | # Needed for Cypress tests as middleware env variables are defined at build time 16 | # @see https://github.com/vercel/next.js/discussions/36338 17 | NEXT_PUBLIC_IS_LOCAL=1 18 | NEXT_PUBLIC_IS_CI=0 19 | 20 | # ## Auth and security 21 | 22 | # If token is less than 32 char you'll get "Password too short error"! 23 | TOKEN_SECRET=this-is-a-token-secret-with-at-least-32-chars 24 | 25 | ADMIN_EMAIL=you.need.to.change.this.value@vulcanjs.com 26 | # if you have changed ADMIN_INITIAL_PASSWORD here in .env.development, please add the 27 | # value in your local .env.test.local (it's safe, it's not tracked by git) so Cypress can work correctly 28 | ADMIN_INITIAL_PASSWORD=vulcan_is_cool 29 | 30 | # ## API 31 | NEXT_PUBLIC_GRAPHQL_URI=http://localhost:3000/api/graphql 32 | # Set to 1 when relying on a cross-domain API (provided by Vulcan Express, Vulcan Meteor backend or another Vulcan Next app for instance) 33 | # NEXT_PUBLIC_CROSS_DOMAIN_GRAPHQL_URI=1 34 | APOLLO_SERVER_CORS_WHITELIST=http://localhost:3000 35 | 36 | MONGO_URI=mongodb+srv://johnDoe:T74OcxqL15TRt7Zn@lbke-demo-ara2d.mongodb.net/sample_restaurants?retryWrites=true&w=majority 37 | # MONGO_URI="mongodb://localhost:27017/vulcan-next-app" 38 | 39 | # ## Testing 40 | 41 | # Replace with your own token, see https://www.chromatic.com 42 | CHROMATIC_PROJECT_TOKEN= 43 | 44 | # ## Emails 45 | 46 | # Replace with your SMTP server configuration 47 | MAIL_FROM=My App 48 | SMTP_HOST= 49 | SMTP_PORT= 50 | SMTP_SECURE= -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # /!\ Do not inline comment after the value 2 | # /!\ Do not use quotes for constant strings, they can lead to confusions 3 | # @see https://github.com/mikestead/vscode-dotenv/issues/31 4 | 5 | # Define this secret ONLY in UNTRACKED .env.production.local or in your production environement! 6 | # LEAVE ME EMPTY in .env.production! 7 | # If secret token is less than 32 char you'll get "Password too short error"! 8 | TOKEN_SECRET= 9 | 10 | NEXT_PUBLIC_GRAPHQL_URI=http://localhost:3000/api/graphql 11 | # Set to 1 when relying on a cross-domain API (provided by Vulcan Express, Vulcan Meteor backend or another Vulcan Next app for instance) 12 | # NEXT_PUBLIC_CROSS_DOMAIN_GRAPHQL_URI=1 13 | 14 | MONGO_URI=mongodb+srv://johnDoe:T74OcxqL15TRt7Zn@lbke-demo-ara2d.mongodb.net/sample_restaurants?retryWrites=true&w=majority 15 | APOLLO_SERVER_CORS_WHITELIST=http://localhost:3000 16 | 17 | # Set to 1 to force GraphQL Playground to be available in production 18 | # /!\ This setting is mainly meant for https://vulcan-next.vercel.app/vulcan-npm, you should need this only in your staging/demo environment 19 | ALLOW_INTROSPECTION= 20 | 21 | NEXT_PUBLIC_NODE_ENV=production 22 | 23 | # Replace with your SMTP server configuration (when empty, emails will be logged in the console) 24 | MAIL_FROM=My App 25 | SMTP_HOST= 26 | SMTP_PORT= 27 | SMTP_SECURE= 28 | 29 | NEXT_PUBLIC_IS_LOCAL=0 30 | NEXT_PUBLIC_IS_CI=0 -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # ## App setup 2 | 3 | # Setup in production with your final main URL 4 | # With port and protocol: https://origin:port 5 | APP_URL=http://localhost:3000 6 | 7 | # ## Environment 8 | 9 | NEXT_PUBLIC_NODE_ENV=test 10 | 11 | # If token is less than 32 char you'll get "Password too short error"! 12 | TOKEN_SECRET="test-secret-token-must-be-at-least-32-chars" 13 | NEXT_PUBLIC_GRAPHQL_URI="http://localhost:3000/api/graphql" 14 | MONGO_URI="mongodb://localhost:27017/vulcan-next-app" 15 | APOLLO_SERVER_CORS_WHITELIST="http://localhost:3000" 16 | 17 | # Need to be in sync with .env.development for Cypress to work ok 18 | ADMIN_EMAIL="you.need.to.change.this.value@vulcanjs.com" 19 | # if you have changed ADMIN_INITIAL_PASSWORD here in .env.development, please add the 20 | # value in your local .env.test.local (it's safe, it's not tracked by git) so Cypress can work correctly 21 | ADMIN_INITIAL_PASSWORD="vulcan_is_cool" 22 | 23 | # For Cypress, we use the "smtp-test" package that can spawn an SMTP server 24 | SMTP_HOST=localhost 25 | SMTP_PORT=7777 26 | SMTP_SECURE= 27 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/.next/** 3 | **/_next/** 4 | **/dist/** 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots (if applicable)** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (if applicable):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: I have a question about Vulcan 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | You have a question about Vulcan? Instead of opening an issue, join us on Slack and get answered directly by experienced users and contributors: http://slack.telescopeapp.org/ 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/rfc.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: RFC 3 | about: Request For Comments for a new concept or feature 4 | title: 'RFC: ' 5 | labels: RFC 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the problem you want to solve or enhancement you want to bring** 11 | 12 | **Define relevant concepts** 13 | 14 | **Describe your solution** 15 | 16 | **Questions to the community** 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/standard_pr.md: -------------------------------------------------------------------------------- 1 | **Why you should merge this PR** 2 | 3 | A clear and concise description of the problem you are solving or the enhancement you are providing with this PR. 4 | 5 | **Issues it closes (if applicable)** 6 | 7 | Closes #XYZ 8 | 9 | **Screenshots** 10 | 11 | If your improvement is UI related, please add a screenshot. 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/trivial_pr.md: -------------------------------------------------------------------------------- 1 | - I am fixing a typo or a grammar mistake OR 2 | - I am fixing an incorrect variable name, a wrong import, an incomplete typing 3 | 4 | If you are not in any of those scenarios, please use the standard PR template instead. 5 | -------------------------------------------------------------------------------- /.github/pull.yml: -------------------------------------------------------------------------------- 1 | # Ensure forks main branch is updated for people using wei/pull 2 | # @see https://github.com/wei/pull#readme 3 | version: "1" 4 | rules: 5 | - base: main # we need to specify the branch, because "main" is NOT the default (we use "devel") 6 | upstream: VulcanJS:main 7 | mergeMethod: hardreset 8 | -------------------------------------------------------------------------------- /.github/workflows/test_unit.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | # To test this script: see https://github.com/nektos/act 3 | 4 | name: Build and Unit test 5 | 6 | # Controls when the action will run. 7 | on: 8 | # Triggers the workflow on push or pull request events but only for the devel branch 9 | push: 10 | branches: [devel] 11 | pull_request: 12 | branches: [devel] 13 | 14 | # Allows you to run this workflow manually from the Actions tab 15 | workflow_dispatch: 16 | 17 | # Reduce the number of CI runs for this workflow 18 | # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency 19 | concurrency: 20 | group: $${{github.workflow}}-${{github.head_ref || github.run_id}} 21 | cancel-in-progress: true 22 | 23 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 24 | jobs: 25 | install: 26 | runs-on: ubuntu-latest 27 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 28 | steps: 29 | - uses: actions/checkout@v2 30 | - uses: actions/setup-node@v2.1.5 31 | - run: npm i -g yarn || true # Not necessary on Github but useful to test locally with `act` 32 | - run: yarn install --frozen-lockfile --check-files 33 | 34 | # This workflow contains a single job called "build" 35 | #unit-test: 36 | # The type of runner that the job will run on 37 | # runs-on: ubuntu-latest 38 | # needs: install 39 | 40 | # Steps represent a sequence of tasks that will be executed as part of the job 41 | # steps: 42 | - run: yarn run typecheck 43 | - run: yarn run test:unit 44 | 45 | #build: 46 | # runs-on: ubuntu-latest 47 | # needs: install 48 | # 49 | # steps: 50 | - run: yarn run build 51 | # Runs a single command using the runners shell 52 | #- name: Run a one-line script 53 | # run: echo Hello, world! 54 | # Runs a set of commands using the runners shell 55 | # - name: Run a multi-line script 56 | # run: | 57 | # echo Add other actions to build, 58 | # echo test, and deploy your project. 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # now 27 | .now 28 | 29 | # Tests 30 | cypress/screenshots 31 | cypress/videos 32 | coverage-e2e/ 33 | .nyc_output/ 34 | coverage-unit 35 | reports/ 36 | 37 | # mdx enhanced 38 | .mdx-data 39 | 40 | # mongo 41 | .mongo 42 | 43 | # next-sitemap 44 | /public/sitemap.xml 45 | /public/robots.txt 46 | 47 | 48 | # Yalc 49 | .yalc 50 | yalc.lock 51 | .yalc/ 52 | 53 | # Storybook 54 | build-storybook.log 55 | storybook-static 56 | 57 | 58 | # Local config 59 | # ! keep them in .dockerignore 60 | .env*.local -------------------------------------------------------------------------------- /.markdownlint.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "MD041":false, 3 | "no-inline-html": false, 4 | "first-line-heading/first-line-h1": false, 5 | "line-length": false, 6 | "no-bare-urls": false, 7 | "no-emphasis-as-heading/no-emphasis-as-header":false, 8 | "no-duplicate-heading": false, 9 | "no-trailing-punctuation": false, 10 | "single-title/single-h": false 11 | } -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.17.0 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false 4 | } 5 | -------------------------------------------------------------------------------- /.storybook/babel.config.js: -------------------------------------------------------------------------------- 1 | // @see https://github.com/storybookjs/storybook/issues/12952 2 | // Needed to get back React classic runtime 3 | const config = require("../babel.config"); 4 | module.exports = { 5 | ...config, 6 | presets: ["@babel/preset-env", "@babel/preset-typescript"], 7 | }; 8 | -------------------------------------------------------------------------------- /.storybook/backgrounds.ts: -------------------------------------------------------------------------------- 1 | export const backgrounds = { 2 | default: "white", 3 | values: [ 4 | { name: "light", value: "#fefefe" }, 5 | { name: "dark", value: "#030303" }, 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import { addons } from "@storybook/addons"; 2 | // import { themes } from "@storybook/theming"; // premade themes, you can try dark 3 | import vulcanStorybook from "./themes/vulcanStorybook"; 4 | 5 | addons.setConfig({ 6 | theme: vulcanStorybook, 7 | }); 8 | -------------------------------------------------------------------------------- /.storybook/mocks/packages/next-config.js: -------------------------------------------------------------------------------- 1 | // TODO: use `loadConfig` from `packages/next/next-server/server/config.ts` to load local next config 2 | // Put the config for storybook here 3 | // @see https://github.com/zeit/next.js/issues/11143 4 | module.exports = function getConfig() { 5 | return { 6 | publicRuntimeConfig: {}, 7 | serverRuntimeConfig: {}, 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /.storybook/mocks/packages/next-router.js: -------------------------------------------------------------------------------- 1 | const Router = { 2 | push: console.log, 3 | prefetch: console.log, 4 | replace: console.log, 5 | reload: console.log, 6 | back: console.log, 7 | prefetch: async () => null, // This one fixed it for me 8 | beforePopState: console.log, 9 | events: { 10 | on: console.log, 11 | off: console.log, 12 | emit: console.log, 13 | }, 14 | pathname: "/", 15 | route: "/", 16 | asPath: "/", 17 | query: {}, 18 | basePath: "", 19 | isFallback: false, 20 | }; 21 | export const useRouter = () => Router; 22 | export const withRouter = (C) => (props) => ; 23 | export default Router; 24 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /.storybook/themes/vulcanStorybook.js: -------------------------------------------------------------------------------- 1 | import { create } from "@storybook/theming/create"; 2 | 3 | export default create({ 4 | colorPrimary: "#E45729", 5 | colorSecondary: "#3F77FA", 6 | appBorderColor: "#61dafbaa", 7 | brandTitle: "Storybook -VN Edition", 8 | brandImage: "/img/vns-logo-96.png", 9 | }); 10 | -------------------------------------------------------------------------------- /.storybook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.common.json", 3 | "compilerOptions": { 4 | "allowJs": false, 5 | "noEmit": true, 6 | "isolatedModules": false, 7 | "module": "commonjs", 8 | "target": "ES2017" 9 | }, 10 | "include": [ 11 | "./**/*.ts", 12 | "./**/*.tsx" 13 | ] 14 | } -------------------------------------------------------------------------------- /.vn/docker/cypress.dockerfile: -------------------------------------------------------------------------------- 1 | 2 | # @see https://github.com/cypress-io/cypress/issues/419 3 | # @see https://github.com/cypress-io/cypress-docker-images/tree/master/base 4 | FROM cypress/base:10.16.0 5 | 6 | WORKDIR /app 7 | 8 | # Copy sources, tests, and stories (test might reuse Storybook stuffs) 9 | # Use .dockerignore if some folders are not needed 10 | COPY . /app 11 | 12 | RUN yarn install --frozen-lockfile 13 | 14 | RUN yarn run build 15 | 16 | CMD yarn run start-server-and-test start:test http://localhost:3000 cypress:run 17 | -------------------------------------------------------------------------------- /.vn/docker/vn.production.dockerfile: -------------------------------------------------------------------------------- 1 | # Use the same version than in your app, see .nvmrc file 2 | FROM node:14.17.5-alpine 3 | 4 | WORKDIR /app 5 | 6 | COPY . /app 7 | 8 | RUN yarn install --ignore-optional --pure-lockfile 9 | # If you want to reduce the bundle size 10 | # RUN yarn cache clean 11 | 12 | 13 | RUN yarn build 14 | 15 | # Yarn don't yet have the feature of removing dev only node_modules after production install 16 | # @see https://github.com/yarnpkg/yarn/issues/6373 17 | # Comment if you have issue with yarn run start 18 | RUN npm prune --production 19 | 20 | CMD yarn run start 21 | -------------------------------------------------------------------------------- /.vn/jest.config.vn.js: -------------------------------------------------------------------------------- 1 | const mainConfig = require("../jest.config"); 2 | const newConfig = { 3 | ...mainConfig, 4 | //rootDir: "../", // TODO: moving this file in .vn folder doesn't work, rootDir is never correct 5 | projects: mainConfig.projects.map((p) => ({ 6 | ...p, 7 | rootDir: "./", // = the folder where "jest" is run 8 | testPathIgnorePatterns: [ 9 | "/node_modules/", 10 | "/cypress/", 11 | "/storybook/", 12 | "/.next/", 13 | "/stories/", 14 | ], 15 | })), 16 | }; 17 | 18 | module.exports = newConfig; 19 | -------------------------------------------------------------------------------- /.vn/nextConfig/redirectsAndRewrites.js: -------------------------------------------------------------------------------- 1 | // Get a cleaner path for the learn live tutorial 2 | async function vnRedirects() { 3 | return [ 4 | { 5 | source: "/learn", 6 | destination: "/learn/intro-offline", 7 | permanent: true, 8 | has: [ 9 | { 10 | type: "host", 11 | value: "localhost", 12 | }, 13 | ], 14 | }, 15 | { 16 | source: "/learn", 17 | destination: "/learn/intro-online", 18 | permanent: true, 19 | has: [ 20 | { 21 | type: "host", 22 | value: "vulcan-next", 23 | }, 24 | ], 25 | }, 26 | { 27 | source: "/vn/learn", 28 | destination: "/learn/intro-offline", 29 | permanent: true, 30 | has: [ 31 | { 32 | type: "host", 33 | value: "localhost", 34 | }, 35 | ], 36 | }, 37 | { 38 | source: "/vn/learn", 39 | destination: "/learn/intro-online", 40 | permanent: true, 41 | has: [ 42 | { 43 | type: "host", 44 | value: "vulcan-next", 45 | }, 46 | ], 47 | }, 48 | ]; 49 | } 50 | async function vnRewrites() { 51 | return [ 52 | { 53 | source: "/learn/:path*", 54 | destination: "/vn/learn/:path*", 55 | }, 56 | ]; 57 | } 58 | 59 | module.exports = { vnRewrites, vnRedirects }; 60 | -------------------------------------------------------------------------------- /.vn/nextConfig/withI18n.js: -------------------------------------------------------------------------------- 1 | // @see https://github.com/isaachinman/next-i18next 2 | // This is equivalent to next-config.js default export, 3 | // but we wrap it as an HOC to avoid being locked with a specific 4 | // i18n library 5 | const i18nConfig = require("../../next-i18next.config"); 6 | 7 | const withI18n = (nextConfig = {}) => { 8 | return { ...nextConfig, ...i18nConfig }; 9 | }; 10 | module.exports = withI18n; 11 | -------------------------------------------------------------------------------- /.vn/nextConfig/withPkgInfo.js: -------------------------------------------------------------------------------- 1 | //NOTE: keep this a node JS file, since next.config.js doesn't accept TS at the time of writing 2 | // fooBar => FOO_BAR 3 | const camelToTitle = (camelStr) => { 4 | return camelStr 5 | .replace(/[A-Z]/g, " $1") // fooBar => foo Bar 6 | .split(" ") 7 | .map((t) => t.toUpperCase()) 8 | .join("_"); 9 | }; 10 | 11 | // NOTE: NEVER import package.json elswhere in your app! 12 | // We can import it here because next.config is not in the client side bundle 13 | // We then pass only the relevant values in the config 14 | const packageJSON = require("../../package.json"); 15 | 16 | // Add package.json metadata to runtime configs and environment 17 | const withPkgInfo = (nextConfig = {}) => { 18 | // Public 19 | // It's still unclear where such config should go 20 | // @see https://github.com/vercel/next.js/discussions/14308 21 | const publicPkgInfo = { 22 | version: packageJSON.version, 23 | }; 24 | if (!nextConfig.publicRuntimeConfig) nextConfig.publicRuntimeConfig = {}; 25 | nextConfig.publicRuntimeConfig.pkgInfo = publicPkgInfo; 26 | // Also enhance environment with the same infos 27 | Object.entries(publicPkgInfo).map(([key, value]) => { 28 | const envKey = `NEXT_PUBLIC_PKGINFO_${camelToTitle(key)}`; 29 | nextConfig.env[envKey] = `${value}`; // we convert to string 30 | }); 31 | 32 | return nextConfig; 33 | }; 34 | module.exports = withPkgInfo; 35 | -------------------------------------------------------------------------------- /.vn/scripts/README.md: -------------------------------------------------------------------------------- 1 | # How to develop a script 2 | 3 | ## Separate build for scripts 4 | 5 | You may want to enjoy TypeScript within scripts, and also reuse 6 | handy helpers you created in the app, like the Mongoose connection code. 7 | 8 | To do so, you need a build-step outside of Next, that mimicks most of the same feature 9 | but can run independently. 10 | 11 | To achieve this, you need a mix between Webpack and TypeScript. Webpack is responsible 12 | for creating one bundled .js file, TypeScript for transpiling the code. 13 | 14 | 15 | ## Step by step 16 | 17 | - Code your script in "./ts-sources" 18 | - Build the scripts using `yarn run build:scripts` => you end up with a few .js file at the root. 19 | - Commit the built JS file to the git repository. Note: usually, we don't commit built files, 20 | but it's ok for scripts because they don't change often. 21 | 22 | ## Legacy setup with ncc 23 | 24 | We previously used https://github.com/vercel/ncc 25 | 26 | However, producing fully autonomous scripts is not optimal here, 27 | because they will bundle half the code of the application... 28 | 29 | We now (2022/04) use Tsup instead and keep node_modules as externals. 30 | 31 | 32 | -------------------------------------------------------------------------------- /.vn/scripts/build-scripts.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # Utility to run NCC on multiple files 3 | # Will transform ./ts-sources/foobar.ts into a built ./foobar.js 4 | # See https://github.com/vercel/ncc 5 | # NOTE: you are expected to run "yarn run build:scripts" from the project root for path to be correct 6 | vn_scripts_dir=./.vn/scripts 7 | ts_sources_dir="$vn_scripts_dir/ts-sources" 8 | # The printf strips the root, so result is "db/reset.ts foobar.ts" etc. 9 | for f in `find "$ts_sources_dir" -type f -name "*.ts" -printf '%P\n'` 10 | do 11 | echo "Build $f" 12 | dname=`dirname "$f"` 13 | fname=`basename "$f"` 14 | fname_no_ext=`echo "$fname" | cut -d. -f1` 15 | echo "Filename $fname, without ext $fname_no_ext" 16 | # TODO: We use transpile-only because SimpleSchema typings are not correctly loaded, we would 17 | # need to find a way to point to the right declaration file 18 | yarn run ncc build "$ts_sources_dir/$f" --no-cache --out "$vn_scripts_dir/dist" --transpile-only 19 | mkdir -p "$vn_scripts_dir/$dname" 20 | mv "$vn_scripts_dir/dist/index.js" "$vn_scripts_dir/$dname/$fname_no_ext".js 21 | done 22 | rm -R "$vn_scripts_dir/dist" -------------------------------------------------------------------------------- /.vn/scripts/fix-apollo.js: -------------------------------------------------------------------------------- 1 | const edits = [ 2 | [ 3 | "node_modules/ts-invariant/package.json", 4 | { 5 | type: "module", 6 | exports: { 7 | // 0.9 8 | //".": "./lib/invariant.esm.js", 9 | // 0.10 10 | ".": "./lib/invariant.js", 11 | "./process/index.js": "./process/index.js", 12 | }, 13 | }, 14 | ], 15 | [ 16 | "node_modules/ts-invariant/process/package.json", 17 | { 18 | type: "module", 19 | }, 20 | ], 21 | [ 22 | "node_modules/@apollo/client/package.json", 23 | { 24 | type: "module", 25 | exports: { 26 | ".": "./index.js", 27 | "./link/error": "./link/error/index.js", 28 | "./testing": "./testing/index.js", 29 | "./core": "./core/index.js", 30 | "./cache": "./cache/index.js", 31 | "./utilities": "./utilities/index.js", 32 | }, 33 | }, 34 | ], 35 | , 36 | [ 37 | "node_modules/@apollo/client/link/error/package.json", 38 | { 39 | exports: { 40 | ".": "./index.js", 41 | }, 42 | }, 43 | ], 44 | ]; 45 | 46 | const fs = require("fs"); 47 | const path = require("path"); 48 | 49 | edits.forEach(([packageJsonPath, fieldsToAdd]) => { 50 | const fullPath = path.resolve(__dirname, "../../", packageJsonPath); 51 | console.log("Add fields", fieldsToAdd, "to", fullPath); 52 | try { 53 | const currentPackage = JSON.parse(fs.readFileSync(fullPath)); 54 | const editedPackage = { ...currentPackage, ...fieldsToAdd }; 55 | fs.writeFileSync(fullPath, JSON.stringify(editedPackage, null, 2)); 56 | } catch (err) { 57 | console.warn("Could not read/write file", fullPath); 58 | console.warn("Maybe the package has been bootstraped via Lerna?"); 59 | console.error(err); 60 | } 61 | // Drop .next folder to force a rebuild 62 | console.log( 63 | "Edited, will drop '.next' folder to avoid build issues (only during dev)" 64 | ); 65 | const dotNextFolder = path.resolve(__dirname, "../../", ".next"); 66 | if (process.NODE_ENV !== "production" && fs.existsSync(dotNextFolder)) { 67 | fs.rmdirSync(dotNextFolder, { recursive: true }); 68 | } 69 | }); 70 | -------------------------------------------------------------------------------- /.vn/scripts/is-monorepo.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/node 2 | function run() { 3 | if ((process.env.PROJECT_CWD || "").match(/vulcan-npm/)) { 4 | process.exit(0); 5 | } else { 6 | process.exit(1); 7 | } 8 | } 9 | run(); 10 | //#  @see https://yarnpkg.com/advanced/lifecycle-scripts/#environment-variables 11 | //[[ "$PROJECT_CWD" == *"vulcan-npm"* ]] 12 | -------------------------------------------------------------------------------- /.vn/scripts/link-vulcan.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # Link other packages that we don't want to duplicate when using Lerna 4 | # yarn link react 5 | # yarn link react-dom 6 | 7 | # Use list-packages script from Vulcan NPM repo to update the list 8 | # We prefer yalc to yarn, because it has a more consistent behaviour regarding linking 9 | # => in particular it will use your local install of React instead of the install from Vulcan NPM 10 | # which prevents duplicating React and thus breaking the rule of hooks 11 | ./node_modules/.bin/yalc link @vulcanjs/core 12 | ./node_modules/.bin/yalc link @vulcanjs/crud 13 | ./node_modules/.bin/yalc link @vulcanjs/graphql 14 | ./node_modules/.bin/yalc link @vulcanjs/i18n 15 | ./node_modules/.bin/yalc link @vulcanjs/mdx 16 | ./node_modules/.bin/yalc link @vulcanjs/meteor-legacy 17 | ./node_modules/.bin/yalc link @vulcanjs/model 18 | ./node_modules/.bin/yalc link @vulcanjs/mongo-apollo 19 | ./node_modules/.bin/yalc link @vulcanjs/mongo 20 | ./node_modules/.bin/yalc link @vulcanjs/multi-env-demo 21 | ./node_modules/.bin/yalc link @vulcanjs/permissions 22 | ./node_modules/.bin/yalc link @vulcanjs/react-ui 23 | ./node_modules/.bin/yalc link @vulcanjs/react-ui-material 24 | ./node_modules/.bin/yalc link @vulcanjs/react-hooks 25 | ./node_modules/.bin/yalc link @vulcanjs/schema 26 | ./node_modules/.bin/yalc link @vulcanjs/utils -------------------------------------------------------------------------------- /.vn/scripts/ts-sources/db/reset.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { 3 | closeDbConnection, 4 | connectToAppDb, 5 | } from "~/core/server/mongoose/connection"; 6 | 7 | async function run() { 8 | await connectToAppDb(); 9 | await mongoose.connection.db.dropDatabase(); 10 | await closeDbConnection(); 11 | } 12 | run(); 13 | -------------------------------------------------------------------------------- /.vn/scripts/ts-sources/db/seed.ts: -------------------------------------------------------------------------------- 1 | import { 2 | closeDbConnection, 3 | connectToAppDb, 4 | } from "~/core/server/mongoose/connection"; 5 | import runSeed from "~/core/server/runSeed"; 6 | 7 | // No top-level async for Node 14 8 | async function run() { 9 | await connectToAppDb(); 10 | await runSeed(); 11 | await closeDbConnection(); 12 | } 13 | run(); 14 | -------------------------------------------------------------------------------- /.vn/scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./**/*.tsx", 5 | "./**/*.ts" 6 | ], 7 | "exclude": [], 8 | "compilerOptions": { 9 | "jsx": "react", // otherwise it fails to build server-side components like mails or components in a schema 10 | "noEmit": false, 11 | "outDir": "./", 12 | "moduleResolution": "node" 13 | } 14 | } -------------------------------------------------------------------------------- /.vn/stories/0-Welcome.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // import { linkTo } from "@storybook/addon-links"; 3 | import { Welcome } from "@storybook/react/demo"; 4 | import { Typography } from "@mui/material"; 5 | 6 | export default { 7 | title: "Welcome", 8 | component: Welcome, 9 | }; 10 | 11 | export const ToStorybook = () => { 12 | return ( 13 |
14 | Welcome to Storybook - VN Edition 15 | 16 | Explore existing stories using the left menu, or{" "} 17 | 22 | read the docs and learn to write your own stories. 23 | 24 | 25 |
26 | ); 27 | 28 | //; 29 | }; 30 | 31 | ToStorybook.story = { 32 | name: "to Storybook", 33 | }; 34 | -------------------------------------------------------------------------------- /.vn/stories/vns/publicImage.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // This pattern works in Storybook, but is not documented in Next.js, that uses a "public" folder instead 3 | // @see https://nextjs.org/docs/basic-features/static-file-serving 4 | // import importedImg from "../../public/vulcan-next-banner_800.png"; 5 | 6 | export default { 7 | title: "VN/publicImage", 8 | }; 9 | 10 | const MyPublicImage = () => ; 11 | 12 | export const publicImg = () => ; 13 | 14 | //const MyImportedImage = () => ; 15 | // export const imported = () => ; 16 | -------------------------------------------------------------------------------- /.vn/stories/vns/typescript.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { action } from "@storybook/addon-actions"; 3 | import { Button } from "@storybook/react/demo"; 4 | 5 | export default { 6 | title: "VN/TypeScript", 7 | component: Button, 8 | }; 9 | 10 | interface MyComponentProps { 11 | children: React.ReactNode; 12 | } 13 | const MyComponent = (props: MyComponentProps) =>

{props.children}

; 14 | 15 | export const Text = () => ( 16 |
17 | This story comes from a .tsx file 18 | 19 |
20 | ); 21 | -------------------------------------------------------------------------------- /.vn/tests/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | // @see https://nextjs.org/docs/testing#setting-up-jest-with-babel 2 | module.exports = { 3 | src: '/img.jpg', 4 | height: 24, 5 | width: 24, 6 | blurDataURL: 'data:image/png;base64,imagedata', 7 | } -------------------------------------------------------------------------------- /.vn/tests/__mocks__/next/router.ts: -------------------------------------------------------------------------------- 1 | // https://klzns.github.io/how-to-use-type-script-and-jest-mocks 2 | // Doesn't work well, so we mock functions one by one 3 | //const router = jest.genMockFromModule("next/router"); 4 | 5 | // FXIME: We can't import the actual lib in the mock, so we can't type it correctly 6 | // import { NextRouter } from "next/router" // TODO: use this type + jest.Mock to improve typings 7 | let routerMock = {}; 8 | 9 | // named export will be used by the actual code when writing "import { useRouter } from "next/router" 10 | export const useRouter = jest.fn(() => ({ 11 | // default mock 12 | query: {}, 13 | pathname: "", 14 | replace: jest.fn(), 15 | push: jest.fn(), 16 | // custom mock 17 | ...routerMock, 18 | })); 19 | 20 | // helpers to change some return values 21 | const __mockUseRouterResult = (mockedRes: Object): void => { 22 | routerMock = mockedRes; 23 | }; 24 | const __resetUseRouterResult = (mockedRes: Object): void => { 25 | routerMock = {}; 26 | }; 27 | 28 | // Export used when we write "import router from "next/router"", eg in tests 29 | // We can add aditionnal imports here, see example test for useMapQueryParams for usage 30 | export default { 31 | useRouter, 32 | __mockUseRouterResult, 33 | __resetUseRouterResult, 34 | }; 35 | -------------------------------------------------------------------------------- /.vn/tests/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | // @see https://nextjs.org/docs/testing#setting-up-jest-with-babel 2 | module.exports = {} -------------------------------------------------------------------------------- /.vn/tests/configTests/exampleClient.test.js: -------------------------------------------------------------------------------- 1 | describe('Default test (collocated with code)', () => { 2 | test('Default test is run in a JSDOM environment', () => { 3 | expect(window).toBeDefined() 4 | }) 5 | }) -------------------------------------------------------------------------------- /.vn/tests/configTests/exampleServer.server.test.js: -------------------------------------------------------------------------------- 1 | describe('Server-only test (collocated with code)', () => { 2 | // Since jsdom also has node definied, this test is more to be sure that server tests will run than to see that it isn't processed as a client. 3 | test('server-only test is run in a Node environment', () => { 4 | expect(process?.versions?.node).toBeDefined() 5 | }) 6 | test('server-only test is NOT run in a JSDOM environment', () => { 7 | expect(typeof window).toBe('undefined') 8 | }) 9 | }) -------------------------------------------------------------------------------- /.vn/tests/configTests/extendEnv.test.js: -------------------------------------------------------------------------------- 1 | describe('Can access to env variables', () => { 2 | test('access to .env.test', () => { 3 | expect(process.env.NEXT_PUBLIC_GRAPHQL_URI).toBeDefined() 4 | }) 5 | test('access to derived variables', () => { 6 | expect(process.env.NEXT_PUBLIC_IS_USING_LOCAL_DATABASE).toBeDefined() 7 | }) 8 | }) -------------------------------------------------------------------------------- /.vn/tests/globalSetup.js: -------------------------------------------------------------------------------- 1 | // NOTE: we are currently (02/2022) using Jest+Babel setup instead of Jest+SWC, 2 | // as we are still waiting for the SWC ecosystem to bring more mature plugins 3 | // (for code coverage, i18n token parsing etc. etc.) 4 | 5 | const { loadEnvConfig } = require("@next/env"); 6 | const extendEnv = require('../nextConfig/extendEnv'); 7 | 8 | module.exports = async () => { 9 | console.info('Jest setupTests script will:'); 10 | console.info("Loading environment variables in Jest from .env files"); 11 | // @see https://github.com/vercel/next.js/issues/17903#issuecomment-708902413 12 | await loadEnvConfig( 13 | // PWD will not be defined on windows, using cwd() instead 14 | process.env.PWD || process.cwd()); 15 | // Compute next.config env => it defines the constructed variables, so we need 16 | // to run it in Jest for the config to work correctly 17 | console.info("Loading derived variables in Jest"); 18 | const envVariables = extendEnv({}).env; 19 | Object.entries(envVariables).forEach(([varName, varValue]) => { 20 | process.env[varName] = varValue; 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /.vn/tests/setupTests.server.js: -------------------------------------------------------------------------------- 1 | // Put here your code configuring or setting up server testing environment -------------------------------------------------------------------------------- /.vn/tests/setupTests.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Common setup for Jest, improve rendering capabilities of JSDOM 3 | * and reduces randomness of tests 4 | * 5 | * Run for each test to guarantee a clean state 6 | */ 7 | 8 | // @see https://github.com/testing-library/jest-dom 9 | import "@testing-library/jest-dom"; -------------------------------------------------------------------------------- /.vn/tests/vn/api/auth.server.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * We use an hackish solution to run Next.js API endpoints, however 3 | * if this fall short, we might move to running the actual dev server + an inmemory mongo 4 | * 5 | * @see https://github.com/vercel/next.js/discussions/15166 6 | * @see 7 | */ 8 | import { connectToDb } from "~/core/server/mongoose/connection"; 9 | import { apiRoutes } from "~/core/server/apiRoutes"; 10 | import { MongoMemoryServer } from "mongodb-memory-server"; // @see https://github.com/nodkz/mongodb-memory-server 11 | import mongoose from "mongoose"; 12 | import request from "supertest"; 13 | 14 | import { spawn } from "child_process"; 15 | 16 | let mongod; 17 | let mongoUri; 18 | let serverUrl = "http://localhost:3000"; 19 | beforeAll(async () => { 20 | // Spin up a dummy mongo server 21 | mongod = await MongoMemoryServer.create(); 22 | mongoUri = mongod.getUri(); 23 | // const port = await mongod.getPort(); 24 | // const dbPath = await mongod.getDbPath(); 25 | // const dbName = await mongod.getDbName(); 26 | // Connect mongoose client 27 | //await mongoose.connect(mongoUri); 28 | await connectToDb(mongoUri); 29 | 30 | // TODO: spin up the Next server as well USING THE LOCAL MONGO_URI 31 | }); 32 | afterAll(async () => { 33 | // remove the collection 34 | // disconnect the client 35 | await mongoose.disconnect(); 36 | // stop mongo server 37 | await mongod.stop(); 38 | }); 39 | 40 | test.skip("signup", async () => { 41 | const user = { 42 | email: "test@test.vulcan-next", 43 | password: "foobar", 44 | }; 45 | //TODO: this tests expects the Next server to already run 46 | // we are not yet able to spin a server elegantly 47 | // @see https://github.com/vercel/next.js/discussions/28173 48 | const res = await request(serverUrl) 49 | .post(apiRoutes.account.signup.href) 50 | .send(user) 51 | .expect(200); 52 | expect(res.body).toEqual({ done: true }); 53 | }); 54 | test.skip("login", () => { 55 | // TODO 56 | }); 57 | test.skip("change password while being logged in", () => { 58 | // TODO 59 | }); 60 | -------------------------------------------------------------------------------- /.vn/tests/vn/mongoConnection.server.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * // @see https://jestjs.io/docs/en/next/configuration#testenvironment-string 3 | * @jest-environment node 4 | */ 5 | import { 6 | connectToDb, 7 | closeDbConnection, 8 | } from "~/core/server/mongoose/connection"; 9 | import mongoose from "mongoose"; 10 | 11 | if (!process.env.MONGO_URI) 12 | throw new Error("MONGO_URI env variable not defined"); 13 | 14 | const mongoUri = process.env.MONGO_URI; 15 | describe("api/middlewares/mongoConnection", () => { 16 | afterEach(async () => { 17 | await closeDbConnection(); 18 | }); 19 | it("connects to mongo db", async () => { 20 | await connectToDb(mongoUri).then(() => { 21 | expect(mongoose.connection.readyState).toEqual(1); 22 | }); 23 | }); 24 | it("connects only once if already connecting", async () => { 25 | const promise = connectToDb(mongoUri); // you can define a .env.test to configure this 26 | const newPromise = connectToDb(mongoUri); // you can define a .env.test to configure this 27 | expect(promise).toEqual(newPromise); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /.vn/tests/vn/nextConfig.test.ts: -------------------------------------------------------------------------------- 1 | import nextConfig from "../../../next.config"; 2 | const { PHASE_DEVELOPMENT_SERVER } = require("next/constants"); 3 | 4 | describe("extend next config", () => { 5 | test("return a valid object", () => { 6 | const extendedConfig = nextConfig(PHASE_DEVELOPMENT_SERVER, { 7 | defaultConfig: {}, 8 | }); 9 | expect(extendedConfig).toHaveProperty("publicRuntimeConfig"); 10 | // expect(extendedConfig).toHaveProperty("serverRuntimeConfig"); 11 | expect(extendedConfig).toHaveProperty("webpack"); 12 | }); 13 | test("include next default config", () => { 14 | const extendedConfig = nextConfig(PHASE_DEVELOPMENT_SERVER, { 15 | defaultConfig: { foobar: true }, 16 | }); 17 | // NOTE: not very robust test, I try to test that our extended config still include the props of the default config 18 | expect(extendedConfig).toHaveProperty("foobar", true); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /.vn/tests/vn/packages.test.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "child_process"; 2 | describe("package.json consistency", () => { 3 | test("do not duplicate styled-jsx", () => { 4 | const why = execSync("yarn why styled-jsx").toString(); 5 | const matches = why.match(/found "styled-jsx/gi); 6 | expect(matches).toHaveLength(1); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /.vn/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../src/tsconfig.json", 3 | "include": ["./**/*.tsx", "./**/*.ts"], 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "silvenon.mdx", 4 | "IronGeek.vscode-env" 5 | ] 6 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "nodeVersionHint": 10, 4 | "configurations": [ 5 | { 6 | "name": "Next.js: debug server-side", 7 | "type": "node-terminal", 8 | "request": "launch", 9 | "command": "yarn run dev" 10 | }, 11 | { 12 | "name": "Next.js: debug client-side", 13 | "type": "pwa-chrome", 14 | "request": "launch", 15 | "url": "http://localhost:3000" 16 | }, 17 | { 18 | "name": "Next.js: debug full stack", 19 | "type": "node-terminal", 20 | "request": "launch", 21 | "command": "yarn run dev", 22 | "console": "integratedTerminal", 23 | "serverReadyAction": { 24 | "pattern": "started server on .+, url: (https?://.+)", 25 | "uriFormat": "%s", 26 | "action": "debugWithChrome" 27 | } 28 | }, 29 | { 30 | "type": "node", 31 | "request": "launch", 32 | "name": "Jest All", 33 | "program": "${workspaceFolder}/node_modules/.bin/jest", 34 | "args": [ 35 | "--runInBand" 36 | ], 37 | "console": "integratedTerminal", 38 | "internalConsoleOptions": "neverOpen", 39 | "disableOptimisticBPs": true, 40 | "windows": { 41 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 42 | } 43 | }, 44 | { 45 | "type": "node", 46 | "request": "launch", 47 | "name": "Jest Current File", 48 | "program": "${workspaceFolder}/node_modules/.bin/jest", 49 | "args": [ 50 | "${fileBasenameNoExtension}", 51 | "--config", 52 | "jest.config.js" 53 | ], 54 | "console": "integratedTerminal", 55 | "internalConsoleOptions": "neverOpen", 56 | "disableOptimisticBPs": true, 57 | "windows": { 58 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 59 | } 60 | } 61 | ] 62 | /* 63 | "compounds": [ 64 | { 65 | "name": "Next: Full", 66 | "configurations": ["Next: Node", "Next: Chrome"] 67 | } 68 | ]*/ 69 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": [ 3 | "javascript", 4 | "javascriptreact", 5 | "typescript", 6 | "typescriptreact" 7 | ], 8 | "search.exclude": { 9 | "cypress/examples/": true 10 | }, 11 | "files.exclude": { 12 | "**/.git": true, // this is a default value 13 | "**/.DS_Store": true, // this is a default value 14 | //"**/node_modules": true, // this excludes all folders 15 | // named "node_modules" from 16 | // the explore tree 17 | ".yalc": true, 18 | ".mongo": true 19 | }, 20 | // syntax highlight .env 21 | "files.associations": { 22 | "*.env.development": "env", 23 | "*.env.production": "env", 24 | "*.env.development.local": "env", 25 | "*.env.production.local": "env", 26 | ".env.local": "env", 27 | ".env.test": "env", 28 | ".env.test.local": "env" 29 | } 30 | } -------------------------------------------------------------------------------- /.vscode/vns.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | // Place your vulcan-next workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and 3 | // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope 4 | // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is 5 | // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: 6 | // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. 7 | // Placeholders with the same ids are connected. 8 | // Example: 9 | // "Print to console": { 10 | // "scope": "javascript,typescript", 11 | // "prefix": "log", 12 | // "body": [ 13 | // "console.log('$1');", 14 | // "$2" 15 | // ], 16 | // "description": "Log output to console" 17 | // } 18 | "style-jsx": { 19 | "prefix": "style-jsx", 20 | "scope": "typescriptreact,javascriptreact", // tsx, jsx 21 | "body": [""], 22 | "description": "Local Styled JSX tag" 23 | }, 24 | "Storybook story": { 25 | "scope": "typescriptreact", 26 | "prefix": "story", 27 | "body": [ 28 | "import React from \"react\"", 29 | "import { $1, $1Props } from \"../$1\"", 30 | "// import { action } from \"@storybook/addon-actions\";", 31 | "import { Story, Meta} from \"@storybook/react\"", 32 | "", 33 | "export default {", 34 | " title: \"$2/$1\",", 35 | " component: $1,", 36 | "// decorators: [(Story) =>
,", 37 | " args: {}", 38 | "} as Meta<$1Props>;", 39 | "", 40 | "const Template: Story<$1Props> = (args) => (<$1 {...args} />)", 41 | "", 42 | "// please keep this default story as is => it serves as a basis for Jest unit tests as well", 43 | "export const Default$1 = Template.bind({})", 44 | "", 45 | "// export const Basic = Template.bind({})", 46 | "// Basic.args = { ...Default$1.args/*, add other props here */ }" 47 | ] 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 VulcanJS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:3000", 3 | "video": false, 4 | "screenshotOnRunFailure": false 5 | } 6 | -------------------------------------------------------------------------------- /cypress/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:3000" 3 | } -------------------------------------------------------------------------------- /cypress/examples/aliasing.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Aliasing', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/aliasing') 6 | }) 7 | 8 | it('.as() - alias a DOM element for later use', () => { 9 | // https://on.cypress.io/as 10 | 11 | // Alias a DOM element for use later 12 | // We don't have to traverse to the element 13 | // later in our code, we reference it with @ 14 | 15 | cy.get('.as-table').find('tbody>tr') 16 | .first().find('td').first() 17 | .find('button').as('firstBtn') 18 | 19 | // when we reference the alias, we place an 20 | // @ in front of its name 21 | cy.get('@firstBtn').click() 22 | 23 | cy.get('@firstBtn') 24 | .should('have.class', 'btn-success') 25 | .and('contain', 'Changed') 26 | }) 27 | 28 | it('.as() - alias a route for later use', () => { 29 | 30 | // Alias the route to wait for its response 31 | cy.server() 32 | cy.route('GET', 'comments/*').as('getComment') 33 | 34 | // we have code that gets a comment when 35 | // the button is clicked in scripts.js 36 | cy.get('.network-btn').click() 37 | 38 | // https://on.cypress.io/wait 39 | cy.wait('@getComment').its('status').should('eq', 200) 40 | 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /cypress/examples/local_storage.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Local Storage', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/local-storage') 6 | }) 7 | // Although local storage is automatically cleared 8 | // in between tests to maintain a clean state 9 | // sometimes we need to clear the local storage manually 10 | 11 | it('cy.clearLocalStorage() - clear all data in local storage', () => { 12 | // https://on.cypress.io/clearlocalstorage 13 | cy.get('.ls-btn').click().should(() => { 14 | expect(localStorage.getItem('prop1')).to.eq('red') 15 | expect(localStorage.getItem('prop2')).to.eq('blue') 16 | expect(localStorage.getItem('prop3')).to.eq('magenta') 17 | }) 18 | 19 | // clearLocalStorage() yields the localStorage object 20 | cy.clearLocalStorage().should((ls) => { 21 | expect(ls.getItem('prop1')).to.be.null 22 | expect(ls.getItem('prop2')).to.be.null 23 | expect(ls.getItem('prop3')).to.be.null 24 | }) 25 | 26 | // Clear key matching string in Local Storage 27 | cy.get('.ls-btn').click().should(() => { 28 | expect(localStorage.getItem('prop1')).to.eq('red') 29 | expect(localStorage.getItem('prop2')).to.eq('blue') 30 | expect(localStorage.getItem('prop3')).to.eq('magenta') 31 | }) 32 | 33 | cy.clearLocalStorage('prop1').should((ls) => { 34 | expect(ls.getItem('prop1')).to.be.null 35 | expect(ls.getItem('prop2')).to.eq('blue') 36 | expect(ls.getItem('prop3')).to.eq('magenta') 37 | }) 38 | 39 | // Clear keys matching regex in Local Storage 40 | cy.get('.ls-btn').click().should(() => { 41 | expect(localStorage.getItem('prop1')).to.eq('red') 42 | expect(localStorage.getItem('prop2')).to.eq('blue') 43 | expect(localStorage.getItem('prop3')).to.eq('magenta') 44 | }) 45 | 46 | cy.clearLocalStorage(/prop1|2/).should((ls) => { 47 | expect(ls.getItem('prop1')).to.be.null 48 | expect(ls.getItem('prop2')).to.be.null 49 | expect(ls.getItem('prop3')).to.eq('magenta') 50 | }) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /cypress/examples/location.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Location', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/location') 6 | }) 7 | 8 | it('cy.hash() - get the current URL hash', () => { 9 | // https://on.cypress.io/hash 10 | cy.hash().should('be.empty') 11 | }) 12 | 13 | it('cy.location() - get window.location', () => { 14 | // https://on.cypress.io/location 15 | cy.location().should((location) => { 16 | expect(location.hash).to.be.empty 17 | expect(location.href).to.eq('https://example.cypress.io/commands/location') 18 | expect(location.host).to.eq('example.cypress.io') 19 | expect(location.hostname).to.eq('example.cypress.io') 20 | expect(location.origin).to.eq('https://example.cypress.io') 21 | expect(location.pathname).to.eq('/commands/location') 22 | expect(location.port).to.eq('') 23 | expect(location.protocol).to.eq('https:') 24 | expect(location.search).to.be.empty 25 | }) 26 | }) 27 | 28 | it('cy.url() - get the current URL', () => { 29 | // https://on.cypress.io/url 30 | cy.url().should('eq', 'https://example.cypress.io/commands/location') 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /cypress/examples/navigation.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Navigation', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io') 6 | cy.get('.navbar-nav').contains('Commands').click() 7 | cy.get('.dropdown-menu').contains('Navigation').click() 8 | }) 9 | 10 | it('cy.go() - go back or forward in the browser\'s history', () => { 11 | // https://on.cypress.io/go 12 | 13 | cy.location('pathname').should('include', 'navigation') 14 | 15 | cy.go('back') 16 | cy.location('pathname').should('not.include', 'navigation') 17 | 18 | cy.go('forward') 19 | cy.location('pathname').should('include', 'navigation') 20 | 21 | // clicking back 22 | cy.go(-1) 23 | cy.location('pathname').should('not.include', 'navigation') 24 | 25 | // clicking forward 26 | cy.go(1) 27 | cy.location('pathname').should('include', 'navigation') 28 | }) 29 | 30 | it('cy.reload() - reload the page', () => { 31 | // https://on.cypress.io/reload 32 | cy.reload() 33 | 34 | // reload the page without using the cache 35 | cy.reload(true) 36 | }) 37 | 38 | it('cy.visit() - visit a remote url', () => { 39 | // https://on.cypress.io/visit 40 | 41 | // Visit any sub-domain of your current domain 42 | 43 | // Pass options to the visit 44 | cy.visit('https://example.cypress.io/commands/navigation', { 45 | timeout: 50000, // increase total time for the visit to resolve 46 | onBeforeLoad (contentWindow) { 47 | // contentWindow is the remote page's window object 48 | expect(typeof contentWindow === 'object').to.be.true 49 | }, 50 | onLoad (contentWindow) { 51 | // contentWindow is the remote page's window object 52 | expect(typeof contentWindow === 'object').to.be.true 53 | }, 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /cypress/examples/viewport.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Viewport', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/viewport') 6 | }) 7 | 8 | it('cy.viewport() - set the viewport size and dimension', () => { 9 | // https://on.cypress.io/viewport 10 | 11 | cy.get('#navbar').should('be.visible') 12 | cy.viewport(320, 480) 13 | 14 | // the navbar should have collapse since our screen is smaller 15 | cy.get('#navbar').should('not.be.visible') 16 | cy.get('.navbar-toggle').should('be.visible').click() 17 | cy.get('.nav').find('a').should('be.visible') 18 | 19 | // lets see what our app looks like on a super large screen 20 | cy.viewport(2999, 2999) 21 | 22 | // cy.viewport() accepts a set of preset sizes 23 | // to easily set the screen to a device's width and height 24 | 25 | // We added a cy.wait() between each viewport change so you can see 26 | // the change otherwise it is a little too fast to see :) 27 | 28 | cy.viewport('macbook-15') 29 | cy.wait(200) 30 | cy.viewport('macbook-13') 31 | cy.wait(200) 32 | cy.viewport('macbook-11') 33 | cy.wait(200) 34 | cy.viewport('ipad-2') 35 | cy.wait(200) 36 | cy.viewport('ipad-mini') 37 | cy.wait(200) 38 | cy.viewport('iphone-6+') 39 | cy.wait(200) 40 | cy.viewport('iphone-6') 41 | cy.wait(200) 42 | cy.viewport('iphone-5') 43 | cy.wait(200) 44 | cy.viewport('iphone-4') 45 | cy.wait(200) 46 | cy.viewport('iphone-3') 47 | cy.wait(200) 48 | 49 | // cy.viewport() accepts an orientation for all presets 50 | // the default orientation is 'portrait' 51 | cy.viewport('ipad-2', 'portrait') 52 | cy.wait(200) 53 | cy.viewport('iphone-4', 'landscape') 54 | cy.wait(200) 55 | 56 | // The viewport will be reset back to the default dimensions 57 | // in between tests (the default can be set in cypress.json) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /cypress/examples/waiting.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Waiting', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/waiting') 6 | }) 7 | // BE CAREFUL of adding unnecessary wait times. 8 | // https://on.cypress.io/best-practices#Unnecessary-Waiting 9 | 10 | // https://on.cypress.io/wait 11 | it('cy.wait() - wait for a specific amount of time', () => { 12 | cy.get('.wait-input1').type('Wait 1000ms after typing') 13 | cy.wait(1000) 14 | cy.get('.wait-input2').type('Wait 1000ms after typing') 15 | cy.wait(1000) 16 | cy.get('.wait-input3').type('Wait 1000ms after typing') 17 | cy.wait(1000) 18 | }) 19 | 20 | it('cy.wait() - wait for a specific route', () => { 21 | cy.server() 22 | 23 | // Listen to GET to comments/1 24 | cy.route('GET', 'comments/*').as('getComment') 25 | 26 | // we have code that gets a comment when 27 | // the button is clicked in scripts.js 28 | cy.get('.network-btn').click() 29 | 30 | // wait for GET comments/1 31 | cy.wait('@getComment').its('status').should('eq', 200) 32 | }) 33 | 34 | }) 35 | -------------------------------------------------------------------------------- /cypress/examples/window.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Window', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/window') 6 | }) 7 | 8 | it('cy.window() - get the global window object', () => { 9 | // https://on.cypress.io/window 10 | cy.window().should('have.property', 'top') 11 | }) 12 | 13 | it('cy.document() - get the document object', () => { 14 | // https://on.cypress.io/document 15 | cy.document().should('have.property', 'charset').and('eq', 'UTF-8') 16 | }) 17 | 18 | it('cy.title() - get the title', () => { 19 | // https://on.cypress.io/title 20 | cy.title().should('include', 'Kitchen Sink') 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /cypress/fixtures/profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 8739, 3 | "name": "Jane", 4 | "email": "jane@example.com" 5 | } -------------------------------------------------------------------------------- /cypress/integration/integration/auth.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NOTE: those are integration test, so actual requests can be mocked 3 | * See e2e folder for testing against the actual api 4 | */ 5 | import { apiRoutes } from "~/core/server/apiRoutes"; 6 | import { routes } from "~/core/routes"; 7 | it("redirect back to from page after login", () => { 8 | // TODO: mock the auth request with MSW and Cypress instead of actually login in 9 | cy.intercept( 10 | apiRoutes.account.login.method as any, 11 | `${apiRoutes.account.login.href}`, 12 | { 13 | statusCode: 200, 14 | headers: { 15 | "Set-Cookie": 16 | "token=fake-token; Max-Age=28800; Path=/; Expires=Thu, 02 Dec 2021 01:32:07 GMT; HttpOnly; SameSite=Lax", 17 | }, 18 | body: { done: true }, 19 | } 20 | ); 21 | cy.intercept( 22 | apiRoutes.account.user.method as any, 23 | `${apiRoutes.account.user.href}`, 24 | { 25 | statusCode: 200, 26 | body: { user: null }, 27 | } 28 | ); 29 | cy.visit(`${routes.account.login.href}?from=%2Fvn%2Fadmin`); 30 | cy.findByLabelText(/email/i).type(Cypress.env("ADMIN_EMAIL")); 31 | cy.findByLabelText(/password/i).type(Cypress.env("ADMIN_INITIAL_PASSWORD")); 32 | cy.findByRole("button").click(); 33 | cy.url().should("match", /admin$/); 34 | // NOTE: since we do a fake auth its ok that user is redirected back to login at the end 35 | }); 36 | -------------------------------------------------------------------------------- /cypress/integration/integration/basic.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | import helper from "~/core/lib/example-helper"; 3 | 4 | describe("basic (vanilla JavaScript test file)", () => { 5 | it("runs the app on port 3000", () => { 6 | cy.visit("http://localhost:3000"); 7 | expect(true).to.equal(true); 8 | }); 9 | it("runs a custom command", () => { 10 | cy.on("window:alert", (str) => { 11 | expect(str).to.equal(`Hello`); 12 | }); 13 | cy.openAlert("Hello"); 14 | }); 15 | it("loads an helper from src/", () => { 16 | expect(helper()).to.equal("foobar"); 17 | }); 18 | it("uses a React Testing command", () => { 19 | cy.visit("http://localhost:3000"); 20 | cy.findAllByText("Vulcan Next").should("exist"); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /cypress/integration/integration/basic.spec.ts: -------------------------------------------------------------------------------- 1 | import helper from "~/core/lib/example-helper"; 2 | 3 | describe("basic (TypeScript test file)", () => { 4 | it("runs the app on port 3000", () => { 5 | cy.visit("http://localhost:3000"); 6 | expect(true).to.equal(true); 7 | }); 8 | it("runs a custom command", () => { 9 | cy.on("window:alert", (str: string) => { 10 | expect(str).to.equal(`Hello`); 11 | }); 12 | cy.openAlert("Hello"); 13 | }); 14 | it("loads an helper from src/", () => { 15 | expect(helper()).to.equal("foobar"); 16 | }); 17 | it("uses a React Testing command", () => { 18 | cy.visit("http://localhost:3000"); 19 | cy.findAllByText("Vulcan Next").should("exist"); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /cypress/integration/integration/doc.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Here we test nested markdown files 3 | */ 4 | const docsRoot = "/vn/docs"; 5 | describe("doc", () => { 6 | it("navigate through folders", () => { 7 | cy.visit(docsRoot + "/"); 8 | cy.findByText(/Features/i).click(); 9 | cy.url().should("include", "/docs/features"); 10 | }); 11 | /* 12 | Doc are now moved to Docusaurus so we only left the setup 13 | it("access to the nested markdown file", () => { 14 | cy.visit(docsRoot + "/features"); 15 | cy.findByText(/Not Yet Implemented Features/i).click(); 16 | cy.url().should("include", "/docs/features/not-yet-implemented-features"); 17 | cy.findByText(/NOT YET IMPLEMENTED:/i).should("exist"); 18 | }); 19 | it("files and folders contains indexLink", () => { 20 | cy.visit(docsRoot + "/features"); 21 | cy.findAllByText(/Back to documentation index/i).should("exist"); 22 | cy.visit(docsRoot + "/features/not-yet-implemented-features"); 23 | cy.findAllByText(/Back to documentation index/i) 24 | .first() 25 | .click(); 26 | cy.url().should( 27 | "not.include", 28 | "/docs/features/not-yet-implemented-features" 29 | ); 30 | }); 31 | it("previousPageLink is where it should", () => { 32 | cy.visit(docsRoot + "/features"); 33 | cy.findByText(/Previous page/i).should("not.exist"); 34 | cy.visit(docsRoot + "/features/not-yet-implemented-features"); 35 | cy.findAllByText(/Previous page/i) 36 | .first() 37 | .click(); 38 | cy.url().should( 39 | "not.include", 40 | "/docs/features/not-yet-implemented-features" 41 | ); 42 | }); 43 | */ 44 | }); 45 | -------------------------------------------------------------------------------- /cypress/integration/integration/mongo.spec.ts: -------------------------------------------------------------------------------- 1 | describe("connect to mongo", () => { 2 | it("get the best restaurants in town", () => { 3 | cy.visit("/vn/debug/mongo"); 4 | cy.get(".restaurants li").should("have.length", 5); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /cypress/integration/integration/mui-styled-components.spec.ts: -------------------------------------------------------------------------------- 1 | //import colors from "~/core/style/colors"; 2 | const orange = "rgb(255, 166, 22)"; // hex color will be translated to RGB during render so directly testing the HEX value won't work 3 | describe("material-ui", () => { 4 | describe("ssr", () => { 5 | it("does render a button with emotion styles applied", () => { 6 | cy.visit("/vn/debug/emotion-mui"); 7 | cy.get("button", { timeout: 0 }).should( 8 | "have.css", 9 | "background-color", 10 | orange 11 | ); //colors.orangeVulcan); 12 | cy.contains("material ui", { matchCase: false, timeout: 0 }).should( 13 | "exist" 14 | ); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /cypress/integration/integration/mui.spec.ts: -------------------------------------------------------------------------------- 1 | describe("material-ui", () => { 2 | describe("ssr", () => { 3 | it("does render a page", () => { 4 | cy.visit("/vn/debug/mui"); 5 | cy.contains("material ui", { matchCase: false, timeout: 0 }).should( 6 | "exist" 7 | ); 8 | }); 9 | }); 10 | describe("client-side", () => { 11 | it("does render a page", () => { 12 | cy.visit("/vn/debug/mui"); 13 | cy.contains("material ui", { matchCase: false }).should("exist"); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /cypress/integration/integration/packageInfo.spec.ts: -------------------------------------------------------------------------------- 1 | describe("package.json info", () => { 2 | it("adds app version in the html header", () => { 3 | cy.visitAsHtml("/"); 4 | cy.get("html").should("have.attr", "data-app-version"); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /cypress/integration/integration/ssr.spec.ts: -------------------------------------------------------------------------------- 1 | describe("ssr", () => { 2 | describe("apollo ssr enabled", () => { 3 | before(() => { 4 | // check that the API call is ok before testing 5 | // other 6 | cy.visit("/vn/debug/apolloSsr"); 7 | cy.contains("data").should("exist"); 8 | }); 9 | it("does not server-side render in loading state", () => { 10 | cy.visit("/vn/debug/apolloSsr"); 11 | cy.contains("loading", { timeout: 0 }).should("not.exist"); 12 | cy.contains("data").should("exist"); 13 | }); 14 | }); 15 | describe("apollo ssr disabled", () => { 16 | it("does server-side render in loading state", () => { 17 | cy.visit("/vn/debug/noApolloSsr"); 18 | cy.contains("loading", { timeout: 0 }).should("exist"); 19 | cy.contains("loading").should("not.exist"); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /cypress/plugins/cy-webpack-preprocessor.js: -------------------------------------------------------------------------------- 1 | const wp = require("@cypress/webpack-preprocessor"); 2 | const config = require("../webpack.config"); 3 | 4 | const options = { 5 | webpackOptions: config, 6 | }; 7 | 8 | module.exports = wp(options); 9 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | const cypressTypeScriptPreprocessor = require("./cy-webpack-preprocessor"); 13 | const loadEnv = require("./load-env"); 14 | const mail = require("./mail"); 15 | 16 | // This function is called when a project is opened or re-opened (e.g. due to 17 | // the project's config changing) 18 | 19 | /** 20 | * @type {Cypress.PluginConfig} 21 | */ 22 | module.exports = (on, config) => { 23 | // `on` is used to hook into various events Cypress emits 24 | // `config` is the resolved Cypress config 25 | const fileProcessors = []; 26 | fileProcessors.push(cypressTypeScriptPreprocessor); 27 | const hasCoverage = process.env.COVERAGE && process.env.COVERAGE !== "false"; 28 | const debug = require("debug")("coverage"); 29 | debug("hasCoverage " + hasCoverage + " " + process.env.COVERAGE); 30 | if (hasCoverage) { 31 | debug("adding coverage task in Cypress"); 32 | fileProcessors.push( 33 | require("@cypress/code-coverage/use-browserify-istanbul") 34 | //require("@cypress/code-coverage/use-babelrc") // on the fly instrumentation 35 | ); 36 | require("@cypress/code-coverage/task")(on, config); 37 | } 38 | 39 | on("file:preprocessor", ...fileProcessors); 40 | 41 | loadEnv(on, config); 42 | mail(on, config); 43 | 44 | return config; 45 | }; 46 | -------------------------------------------------------------------------------- /cypress/plugins/load-env.js: -------------------------------------------------------------------------------- 1 | // plugins/index.js 2 | // pure dotenv version 3 | // require('dotenv').config() 4 | const { loadEnvConfig } = require("@next/env"); 5 | module.exports = async (on, config) => { 6 | // Please leave this message (and update it if it has gone stales), it explains why the testing environment 7 | // might have some difference with the real code 8 | console.info( 9 | `Loading environment variables in Cypress, from Next .env files` 10 | ); 11 | // @see https://github.com/vercel/next.js/issues/17903#issuecomment-708902413 12 | const { combinedEnv } = await loadEnvConfig( 13 | // needed to find the files 14 | process.env.PWD || process.cwd(), 15 | // will load either from .env.development or .env.production 16 | process.env.NODE_ENV === "development" 17 | ); 18 | config.env = { ...config.env, ...combinedEnv }; 19 | }; 20 | -------------------------------------------------------------------------------- /cypress/plugins/mail.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @see https://www.cypress.io/blog/2021/05/11/testing-html-emails-using-cypress/ 3 | const ms = require("smtp-tester"); 4 | 5 | const getRecipientKey = (recipientEmailAddress) => { 6 | if (Array.isArray(recipientEmailAddress)) { 7 | return recipientEmailAddress.join(","); 8 | } 9 | return recipientEmailAddress; 10 | }; 11 | /** 12 | * Will start a mail server on port 7777 13 | * @type {Cypress.PluginConfig} 14 | */ 15 | module.exports = (on, config) => { 16 | // starts the SMTP server at localhost:7777 17 | const port = 7777; 18 | const mailServer = ms.init(port); 19 | console.log("mail server at port %d", port); 20 | 21 | let lastEmailPerRecipient = {}; 22 | let lastEmail = null; 23 | mailServer.bind((addr, id, email) => { 24 | console.log("--- email ---"); 25 | console.log(addr, id, email); 26 | // store the body for the email adress 27 | // If there is a least of expeditors, store the list 28 | const to = getRecipientKey(email.headers.to); 29 | console.log("Set last email", to); 30 | lastEmail = email.html || email.body; 31 | lastEmailPerRecipient[to] = lastEmail; 32 | }); 33 | on("task", { 34 | resetEmails(recipientEmail) { 35 | console.log("Reset all emails"); 36 | if (recipientEmail) { 37 | const key = getRecipientKey(recipientEmail); 38 | delete lastEmailPerRecipient[key]; 39 | } else { 40 | // reset for all users 41 | lastEmail = null; 42 | lastEmailPerRecipient = {}; 43 | } 44 | return null; 45 | }, 46 | /** 47 | * Get last email sent for a given user 48 | * @param {*} email User's email 49 | * @returns The email HTML body 50 | */ 51 | getLastEmail(recipientEmail) { 52 | console.log("\tGet last email", recipientEmail, lastEmail); 53 | // cy.task cannot return undefined 54 | // thus we return null as a fallback 55 | if (recipientEmail) { 56 | const key = getRecipientKey(recipientEmail); 57 | return lastEmailPerRecipient[key] || null; 58 | } 59 | return lastEmail || null; 60 | }, 61 | }); 62 | }; 63 | -------------------------------------------------------------------------------- /cypress/support/after.ts: -------------------------------------------------------------------------------- 1 | after(() => { 2 | console.info("Running after hook from 'cypress/support/after.ts'"); 3 | // disable debugging 4 | window.localStorage.removeItem("debug"); 5 | }); 6 | -------------------------------------------------------------------------------- /cypress/support/before.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Initialize debug with NPM debug 3 | * 4 | * Note: this is a client-side file, prefix environment variables with CYPRESS_ if necessary 5 | */ 6 | before(() => { 7 | console.info("Running Cypress before hook from 'cypress/support/before.ts'"); 8 | const debugEnv = Cypress.env("DEBUG"); // Set CYPRESS_DEBUG if you want this variable to be defined (the CYPRESS_ prefix is scrapped out automatically) 9 | if (!(debugEnv && debugEnv === "false")) { 10 | const debugLevel = debugEnv || "vns:*"; 11 | console.info("Enabling debug with namespace", debugLevel); 12 | console.info( 13 | "Run Cypress with CYPRESS_DEBUG=false to disable, or CYPRESS_DEBUG= to change the debug namespace" 14 | ); 15 | window.localStorage.setItem("debug", debugLevel); 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | import "@testing-library/cypress/add-commands"; 27 | 28 | import "./commands/example"; 29 | import "./commands/i18n"; 30 | import "./commands/ssr"; 31 | -------------------------------------------------------------------------------- /cypress/support/commands/example.ts: -------------------------------------------------------------------------------- 1 | Cypress.Commands.add("dataCy", (value) => { 2 | return cy.get(`[data-cy=${value}]`); 3 | }); 4 | Cypress.Commands.add("openAlert", (message) => { 5 | cy.window().then((w) => w.alert(message)); 6 | }); 7 | -------------------------------------------------------------------------------- /cypress/support/commands/i18n.ts: -------------------------------------------------------------------------------- 1 | // NOTE: this will set the language cookie but won't automatically redirect users 2 | Cypress.Commands.add("setLanguage", (language: string) => { 3 | cy.setCookie("NEXT_LOCALE", language); 4 | }); 5 | Cypress.Commands.add("resetDefaultLanguage", () => { 6 | cy.clearCookie("NEXT_LOCALE"); 7 | }); 8 | -------------------------------------------------------------------------------- /cypress/support/commands/ssr.ts: -------------------------------------------------------------------------------- 1 | // @see https://glebbahmutov.com/blog/ssr-e2e/#removing-application-bundle 2 | /** 3 | * Load the server rendered page, with no JavaScript 4 | * Allows to test SSR rendering with normal Cypress methods (cy.get etc.), 5 | * without having to mess around with jQuery 6 | */ 7 | Cypress.Commands.add("visitAsHtml", (route: string) => { 8 | cy.request(route) 9 | .its("body") 10 | .then((html) => { 11 | // remove the application code JS bundle 12 | html = html.replace( 13 | /)<[^<]*)*<\/script>/gi, 14 | "" 15 | ); 16 | // FIXME: https://github.com/cypress-io/cypress/issues/1611 17 | // It doesn't actually work if you run this command multiple times 18 | console.log("setting html", html.slice(0, 300)); 19 | cy.document().invoke({ log: false }, "write", html); 20 | }); 21 | // now we can use "normal" Cypress api on the page 22 | }); 23 | -------------------------------------------------------------------------------- /cypress/support/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Cypress { 2 | interface Chainable { 3 | /** 4 | * Custom command to select DOM element by data-cy attribute. 5 | * @example cy.dataCy('greeting') 6 | */ 7 | dataCy(value: string): Chainable; 8 | openAlert(message: string): void; 9 | 10 | // i18n commands 11 | setLanguage(language: string): void; 12 | resetDefaultLanguage(): void; 13 | 14 | visitAsHtml(route: string): void; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import "./commands"; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | 22 | // code coverage 23 | // NOTE: this file is passed to the browser => we can't access process.env.COVERAGE 24 | import "@cypress/code-coverage/support"; 25 | 26 | // code run before/after all tests 27 | import "./before"; 28 | import "./after"; 29 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.common.json", 3 | "outDir": "./dist/", 4 | "compilerOptions": { 5 | "types": ["cypress", "@testing-library/cypress"], 6 | "noEmit": false, 7 | "sourceMap": false, 8 | "isolatedModules": false 9 | }, 10 | "include": ["./**/*.ts", "./**/*.tsx"] 11 | } 12 | -------------------------------------------------------------------------------- /cypress/webpack.config.js: -------------------------------------------------------------------------------- 1 | // @see https://github.com/cypress-io/cypress-webpack-preprocessor/tree/master/examples/use-ts-loader 2 | const path = require("path"); 3 | const extendWebpackConfig = require("../packages/@vulcanjs/webpack/extendWebpackConfig"); 4 | 5 | const config = { 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.tsx?$/, 10 | use: [ 11 | { 12 | loader: "ts-loader", 13 | options: { 14 | transpileOnly: true, 15 | }, 16 | }, 17 | ], 18 | exclude: /node_modules/, 19 | }, 20 | ], 21 | }, 22 | resolve: { 23 | extensions: [".tsx", ".ts", ".js"], 24 | modules: ["node_modules"], 25 | }, 26 | output: { 27 | filename: "bundle.js", 28 | path: path.resolve(__dirname, "dist"), 29 | }, 30 | }; 31 | 32 | const extended = extendWebpackConfig()(config); 33 | 34 | // TODO: does not seem reckognized by Cypress, right now we still use webpack 4 35 | delete extended.resolve.fallback; 36 | 37 | module.exports = extended; 38 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /next-i18next.config.js: -------------------------------------------------------------------------------- 1 | // NOTE: please do not move this file, it has to be at the root 2 | // @see https://github.com/isaachinman/next-i18next 3 | // It accepts the same props as a "normal" Next.js i18n config 4 | // @see https://nextjs.org/docs/advanced-features/i18n-routing 5 | module.exports = { 6 | i18n: { 7 | locales: ["en", "fr"], 8 | defaultLocale: "en", 9 | // We disable automated locale based redirection, because it leads to a confusing user 10 | // experience depending on where the user initially lands (only "/" will redirect, but not pages) 11 | localeDetection: false, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /nyc.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: "@istanbuljs/nyc-config-typescript", 3 | "report-dir": "coverage-e2e", 4 | all: true, 5 | include: ["src/**/*.{js,jsx,ts,tsx}"], 6 | }; 7 | -------------------------------------------------------------------------------- /packages/@vulcanjs/next-apollo/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./apolloClient"; 2 | -------------------------------------------------------------------------------- /packages/@vulcanjs/next-apollo/links/error.ts: -------------------------------------------------------------------------------- 1 | import { onError } from "@apollo/client/link/error"; 2 | import { GraphQLError } from "graphql"; 3 | 4 | const locationsToStr = (locations: GraphQLError["locations"] = []) => 5 | locations.map(({ column, line }) => `line ${line}, col ${column}`).join(";"); 6 | 7 | const errorLink = onError((error) => { 8 | const { graphQLErrors, networkError } = error; 9 | if (graphQLErrors) 10 | graphQLErrors.forEach(({ message, locations, path }) => { 11 | // eslint-disable-next-line no-console 12 | console.log( 13 | `[GraphQL error]: Message: ${message}, Location: ${locationsToStr( 14 | locations 15 | )}, Path: ${path}` 16 | ); 17 | }); 18 | if (networkError) { 19 | // eslint-disable-next-line no-console 20 | console.log(`[Network error]: ${networkError}`); 21 | } 22 | }); 23 | 24 | export default errorLink; 25 | -------------------------------------------------------------------------------- /packages/@vulcanjs/next-apollo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vulcanjs/next-apollo", 3 | "version": "0.0.1", 4 | "description": "Vulcan Next Apollo bindings", 5 | "main": "./dist/index.js", 6 | "author": "eric-burel ", 7 | "homepage": "https://github.com/VulcanJS/vulcan-npm#readme", 8 | "license": "MIT", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/VulcanJS/vulcan-npm.git" 12 | }, 13 | "scripts": { 14 | "test": "echo \"Error: run tests from root\" && exit 1" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/VulcanJS/vulcan-npm/issues" 18 | }, 19 | "dependencies": { 20 | "@apollo/client": "^3.2.0", 21 | "@vulcanjs/next-utils": "*" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/@vulcanjs/next-apollo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "outDir": "./dist" 6 | }, 7 | "include": ["*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/@vulcanjs/next-config/extendNextConfig.js: -------------------------------------------------------------------------------- 1 | const { extendWebpackConfig } = require("../webpack"); // TODO: load from @vulcanjs/webpack NPM package 2 | 3 | // type NextConfig = { 4 | // webpack?: Object | Function 5 | // } 6 | 7 | // type WebpackConfig = { 8 | // resolve: { 9 | // mainFiles: Array 10 | // } 11 | // } 12 | 13 | // type WebpackOptions = { 14 | // isServer: Boolean 15 | // } 16 | 17 | module.exports = (nextConfig = {}) => { 18 | const vulcanNextConfig = { 19 | // Use .env instead 20 | //env: { 21 | // ROOT_URL: process.env.ROOT_URL || "http://localhost:3000", 22 | // GRAPHQL_URL: 23 | // process.env.NEXT_PUBLIC_GRAPHQL_URI || 24 | // "http://localhost:3000/api/graphql", 25 | // ...nextConfig.env, 26 | //}, 27 | webpack: (config, options) => { 28 | if (!options.isServer) { 29 | config = extendWebpackConfig("client")(config); 30 | } else { 31 | config = extendWebpackConfig("server")(config); 32 | } 33 | 34 | if (typeof nextConfig.webpack === "function") { 35 | return nextConfig.webpack(config, options); 36 | } 37 | const debug = require("debug")("webpack"); 38 | debug("extended config", config); 39 | 40 | return config; 41 | }, 42 | }; 43 | 44 | return Object.assign({}, nextConfig, vulcanNextConfig); 45 | }; 46 | -------------------------------------------------------------------------------- /packages/@vulcanjs/next-config/index.js: -------------------------------------------------------------------------------- 1 | const extendNextConfig = require("./extendNextConfig"); 2 | 3 | module.exports = { 4 | extendNextConfig, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/@vulcanjs/next-mui/components/NextMuiButton.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | ButtonProps, 4 | ListItemButton, 5 | ListItemButtonProps, 6 | } from "@mui/material"; 7 | import NextLink, { LinkProps as NextLinkProps } from "next/link"; 8 | import pick from "lodash/pick.js"; 9 | import omit from "lodash/omit.js"; 10 | 11 | const nextLinkProps: Array = [ 12 | "href", 13 | "as", 14 | "replace", 15 | "scroll", 16 | "shallow", 17 | "passHref", 18 | "prefetch", 19 | "locale", 20 | ]; 21 | /** 22 | * Button to be used when using href and pointing toward a local page 23 | */ 24 | export const NextMuiButton = ({ 25 | children, 26 | ...props 27 | }: ButtonProps & NextLinkProps) => { 28 | const linkProps = pick(props, nextLinkProps); 29 | const buttonProps = omit(props, nextLinkProps); 30 | return ( 31 | 32 | 33 | 34 | ); 35 | }; 36 | /** 37 | * Button to be used when using href and pointing toward a local page 38 | * 39 | * Do not pass href directly to Button or ListItemButton, this leads to bad UX 40 | * Use a Next link for better consistency (will use an SPA link) 41 | */ 42 | export const NextMuiListItemButton = ( 43 | props: ListItemButtonProps & NextLinkProps 44 | ) => { 45 | const linkProps = pick(props, nextLinkProps); 46 | const buttonProps = omit(props, nextLinkProps) as ListItemButtonProps<"a">; 47 | return ( 48 | 49 | 50 | 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /packages/@vulcanjs/next-mui/components/stories/Link.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NextLinkComposed, NextLinkComposedProps } from "../Link"; 3 | // import { action } from "@storybook/addon-actions"; 4 | import { Story, Meta } from "@storybook/react"; 5 | 6 | export default { 7 | title: "next-mui/NextMuiLink", 8 | component: NextLinkComposed, 9 | // decorators: [(Story) =>
, 10 | args: {}, 11 | } as Meta; 12 | 13 | const Template: Story = (args) => ( 14 | 15 | ); 16 | 17 | // please keep this default story as is => it serves as a basis for Jest unit tests as well 18 | export const DefaultNextMuiLink = Template.bind({}); 19 | 20 | // export const Basic = Template.bind({}) 21 | // Basic.args = { ...DefaultNextMuiLink.args/*, add other props here */ } 22 | -------------------------------------------------------------------------------- /packages/@vulcanjs/next-mui/emotion/createEmotionCache.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * For SSR with Emotion, cache contains the styles 3 | */ 4 | 5 | import createCache from "@emotion/cache"; 6 | 7 | export function createEmotionCache() { 8 | return createCache({ key: "css" }); 9 | } 10 | -------------------------------------------------------------------------------- /packages/@vulcanjs/next-mui/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./emotion/createEmotionCache"; 2 | 3 | export { default as Link } from "./components/Link"; // deprecated alias 4 | export { default as NextMuiLink } from "./components/Link"; // recommanded alias to use 5 | 6 | export { 7 | NextMuiButton, 8 | NextMuiListItemButton, 9 | } from "./components/NextMuiButton"; 10 | -------------------------------------------------------------------------------- /packages/@vulcanjs/next-mui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vulcanjs/next-mui", 3 | "version": "0.0.1", 4 | "description": "Vulcan Next Material UI binding", 5 | "main": "./dist/index.js", 6 | "author": "eric-burel ", 7 | "homepage": "https://github.com/VulcanJS/vulcan-npm#readme", 8 | "license": "MIT", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/VulcanJS/vulcan-npm.git" 12 | }, 13 | "scripts": { 14 | "test": "echo \"Error: run tests from root\" && exit 1", 15 | "build": "webpack --config ./webpack.config.js" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/VulcanJS/vulcan-npm/issues" 19 | }, 20 | "dependencies": { 21 | "@material-ui/core": "^4.11.0", 22 | "clsx": "^1.1.1", 23 | "next": "^9.5.3", 24 | "react-dom": "^17.0.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/@vulcanjs/next-mui/server.ts: -------------------------------------------------------------------------------- 1 | export * from "./server/getAppEnhancer"; 2 | -------------------------------------------------------------------------------- /packages/@vulcanjs/next-style-collector/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generic helpers to build a lib that will collect styles in the Next application, 3 | * such as Material UI, Styled Components, Emotion... 4 | * 5 | * May help keeping the ugly logic out of your _document 6 | */ 7 | 8 | export interface Sheets { 9 | getStyleElements: (html: string) => Array; //Element | Array; 10 | } 11 | // Generic interface to respect 12 | export interface AppSheetsCollector { 13 | sheets: Sheets; // minimum spec to respect for a collector 14 | enhanceApp: Function; // function used in ctx.renderPage, so the sheet can collect styles 15 | finally?: Function; 16 | } 17 | -------------------------------------------------------------------------------- /packages/@vulcanjs/next-style-collector/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vulcanjs/next-style-collector", 3 | "version": "0.0.1", 4 | "description": "Vulcan stylesheets collecting for SSR (Material UI, Styled Components)", 5 | "main": "./dist/index.js", 6 | "author": "eric-burel ", 7 | "homepage": "https://github.com/VulcanJS/vulcan-npm#readme", 8 | "license": "MIT", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/VulcanJS/vulcan-npm.git" 12 | }, 13 | "scripts": { 14 | "test": "echo \"Error: run tests from root\" && exit 1" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/VulcanJS/vulcan-npm/issues" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/@vulcanjs/next-utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ssr"; 2 | // export * from "./routing.tsx.old"; 3 | -------------------------------------------------------------------------------- /packages/@vulcanjs/next-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vulcanjs/next-utils", 3 | "version": "0.0.1", 4 | "description": "Vulcan Next related helpers", 5 | "main": "./dist/index.js", 6 | "author": "eric-burel ", 7 | "homepage": "https://github.com/VulcanJS/vulcan-npm#readme", 8 | "license": "MIT", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/VulcanJS/vulcan-npm.git" 12 | }, 13 | "scripts": { 14 | "test": "echo \"Error: run tests from root\" && exit 1" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/VulcanJS/vulcan-npm/issues" 18 | }, 19 | "dependencies": { 20 | "react": "^17.0.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/@vulcanjs/next-utils/ssr.ts: -------------------------------------------------------------------------------- 1 | import { NextPageContext } from "next"; 2 | 3 | export const isServerRenderCtx = (ctx?: NextPageContext) => 4 | !!(ctx && ctx.res && ctx.res.writeHead); 5 | 6 | export const isStaticExportCtx = (ctx?: NextPageContext) => 7 | !!(ctx && ctx.res && !ctx.res.writeHead); 8 | 9 | export const isClientRender = () => typeof window !== "undefined"; 10 | -------------------------------------------------------------------------------- /packages/@vulcanjs/next-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "lib": ["es5", "dom"] 6 | }, 7 | "include": ["*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/@vulcanjs/webpack/index.js: -------------------------------------------------------------------------------- 1 | const extendWebpackConfig = require("./extendWebpackConfig"); 2 | 3 | module.exports = { 4 | extendWebpackConfig, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/README.md: -------------------------------------------------------------------------------- 1 | In this folder, you can isolate code that can be structed in packages. 2 | 3 | Usually, your code may evolve like this: 4 | 5 | - at first, it lives in `src/core` 6 | - if you identify a feature that can be isolated, you can move it to `src/myFeature` 7 | - if this feature might be reusable for multiple Next.js app, you can move it to `packages/myFeature` 8 | - finally, you may want to publish it as an NPM package. You can reach us out to include it in the [Vulcan NPM monorepo](https://github.com/VulcanJS/vulcan-npm) -------------------------------------------------------------------------------- /packages/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.common.json", 3 | "include": ["./**/*.tsx", "./**/*.ts"], 4 | } 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | //require('autoprefixer'), 4 | "postcss-nested": {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VulcanJS/vulcan-next/2dbf747581c96e575713ef6fd4b06c742863198c/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VulcanJS/vulcan-next/2dbf747581c96e575713ef6fd4b06c742863198c/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VulcanJS/vulcan-next/2dbf747581c96e575713ef6fd4b06c742863198c/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VulcanJS/vulcan-next/2dbf747581c96e575713ef6fd4b06c742863198c/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VulcanJS/vulcan-next/2dbf747581c96e575713ef6fd4b06c742863198c/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VulcanJS/vulcan-next/2dbf747581c96e575713ef6fd4b06c742863198c/public/favicon.ico -------------------------------------------------------------------------------- /public/img/docs/env_token_secret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VulcanJS/vulcan-next/2dbf747581c96e575713ef6fd4b06c742863198c/public/img/docs/env_token_secret.png -------------------------------------------------------------------------------- /public/img/letter-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VulcanJS/vulcan-next/2dbf747581c96e575713ef6fd4b06c742863198c/public/img/letter-96x96.png -------------------------------------------------------------------------------- /public/img/vn-logo-full-1280-640.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VulcanJS/vulcan-next/2dbf747581c96e575713ef6fd4b06c742863198c/public/img/vn-logo-full-1280-640.png -------------------------------------------------------------------------------- /public/img/vn-logo-full-600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VulcanJS/vulcan-next/2dbf747581c96e575713ef6fd4b06c742863198c/public/img/vn-logo-full-600.png -------------------------------------------------------------------------------- /public/img/vn-logo-full-padded-720.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VulcanJS/vulcan-next/2dbf747581c96e575713ef6fd4b06c742863198c/public/img/vn-logo-full-padded-720.png -------------------------------------------------------------------------------- /public/img/vn-logo-full-padded-840.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VulcanJS/vulcan-next/2dbf747581c96e575713ef6fd4b06c742863198c/public/img/vn-logo-full-padded-840.png -------------------------------------------------------------------------------- /public/img/vn-logo-full-padded-dark-840.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VulcanJS/vulcan-next/2dbf747581c96e575713ef6fd4b06c742863198c/public/img/vn-logo-full-padded-dark-840.png -------------------------------------------------------------------------------- /public/img/vns-logo-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VulcanJS/vulcan-next/2dbf747581c96e575713ef6fd4b06c742863198c/public/img/vns-logo-32.png -------------------------------------------------------------------------------- /public/img/vns-logo-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VulcanJS/vulcan-next/2dbf747581c96e575713ef6fd4b06c742863198c/public/img/vns-logo-64.png -------------------------------------------------------------------------------- /public/img/vns-logo-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VulcanJS/vulcan-next/2dbf747581c96e575713ef6fd4b06c742863198c/public/img/vns-logo-96.png -------------------------------------------------------------------------------- /public/locales/en/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "__Debug message": "Hi, I am in English 🇬🇧" 3 | } 4 | -------------------------------------------------------------------------------- /public/locales/fr/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "__Debug message": "Bonjour, je suis en français 🇫🇷" 3 | } 4 | -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VulcanJS/vulcan-next/2dbf747581c96e575713ef6fd4b06c742863198c/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 15 | 20 | 26 | 32 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vulcan Next", 3 | "short_name": "Vulcan Next", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /public/vulcan-next-banner_800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VulcanJS/vulcan-next/2dbf747581c96e575713ef6fd4b06c742863198c/public/vulcan-next-banner_800.png -------------------------------------------------------------------------------- /src/account/components/ErrorSuccessMessages.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from "@mui/material"; 2 | 3 | export const ErrorSuccessMessages = ({ 4 | errorMsg, 5 | successMsg, 6 | }: { 7 | errorMsg?: string | null; 8 | successMsg?: string | null; 9 | }) => { 10 | if (!(errorMsg || successMsg)) return null; 11 | return ( 12 | <> 13 | {errorMsg && ( 14 | {errorMsg} 15 | )} 16 | {successMsg && ( 17 | {successMsg} 18 | )} 19 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/account/components/hooks.ts: -------------------------------------------------------------------------------- 1 | // It won't reload if there are no remount => we need to find a way to mutate on login 2 | // @see https://github.com/vercel/next.js/discussions/19601 3 | import { useEffect } from "react"; 4 | import Router, { useRouter } from "next/router"; 5 | import useSWR from "swr"; 6 | import { UserType } from "~/account/models/user"; 7 | import { apiRoutes } from "~/core/server/apiRoutes"; 8 | 9 | const fetcher = (url) => 10 | fetch(url) 11 | .then((r) => r.json()) 12 | .then((data) => { 13 | return { user: data?.user || null }; 14 | }); 15 | 16 | /** 17 | * Get the current user, optionally redirect if not found 18 | */ 19 | export function useUser({ 20 | redirectTo, 21 | rememberCurrentRoute, 22 | redirectIfFound, 23 | }: { 24 | redirectTo?: string; 25 | /** 26 | * Will add from=currentRoute parameter so auth can redirect back to the page 27 | */ 28 | rememberCurrentRoute?: boolean; 29 | redirectIfFound?: boolean; 30 | } = {}) { 31 | const { data, error } = useSWR<{ user?: UserType }>( 32 | apiRoutes.account.user.href, 33 | fetcher 34 | ); 35 | const router = useRouter(); 36 | const user = data?.user; 37 | const finished = Boolean(data); 38 | const hasUser = Boolean(user); 39 | 40 | useEffect(() => { 41 | if (!redirectTo || !finished) return; 42 | if ( 43 | // If redirectTo is set, redirect if the user was not found. 44 | (redirectTo && !redirectIfFound && !hasUser) || 45 | // If redirectIfFound is also set, redirect if the user was found 46 | (redirectIfFound && hasUser) 47 | ) { 48 | const redirectUrl = rememberCurrentRoute 49 | ? `${redirectTo}?from=${encodeURIComponent(router.pathname)}` 50 | : redirectTo; 51 | Router.push(redirectUrl); 52 | } 53 | }, [redirectTo, redirectIfFound, finished, hasUser]); 54 | 55 | return error ? null : user; 56 | } 57 | -------------------------------------------------------------------------------- /src/account/components/layout.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import Link from "next/link"; 3 | 4 | const Layout = (props) => ( 5 | <> 6 | 7 | Auth 8 | 9 | 10 |
11 |
12 | {process.env.NEXT_PUBLIC_IS_USING_DEMO_DATABASE ? ( 13 |
14 |

You are using LBKE read-only demo database.

15 |

16 | To enable authentication features, please setup your own local 17 | database. 18 |

19 |

20 | See{" "} 21 | 22 | home README 23 | {" "} 24 | for relevant instructions. 25 |

26 |
27 | ) : ( 28 | props.children 29 | )} 30 |
31 |
32 | 33 | {/*