├── .contentful └── vault-secrets.yaml ├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── feedback.md │ └── proposal.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── auto-merge.yml │ ├── eslint-tsc.yml │ └── sast.yaml ├── .gitignore ├── .husky ├── pre-commit └── pre-push ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .vercelignore ├── @types ├── catchify │ └── index.d.ts ├── mui.d.ts └── next-env.d.ts ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── bin └── setup.sh ├── catalog-info.yaml ├── codegen.ts ├── config ├── headers.js ├── includePolyfills.js └── plugins.js ├── contentful.config.js ├── docs └── tutorials │ └── contentful-and-the-starter-template.md ├── marketing-starter-template.jpg ├── netlify.toml ├── next-env.d.ts ├── next-i18next.config.js ├── next.config.js ├── package.json ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── locales │ ├── de-DE │ │ └── common.json │ └── en-US │ │ └── common.json ├── mstile-150x150.png ├── robots.txt ├── safari-pinned-tab.svg └── site.webmanifest ├── src ├── components │ ├── features │ │ ├── author │ │ │ ├── author.tsx │ │ │ └── index.ts │ │ ├── avatar │ │ │ ├── avatar.tsx │ │ │ └── index.ts │ │ ├── card-leadership │ │ │ ├── card-leadership.tsx │ │ │ └── index.ts │ │ ├── card-person │ │ │ ├── card-person.tsx │ │ │ └── index.ts │ │ ├── ctf-components │ │ │ ├── ctf-asset │ │ │ │ ├── __generated │ │ │ │ │ └── ctf-asset.generated.ts │ │ │ │ ├── ctf-asset.graphql │ │ │ │ └── ctf-asset.tsx │ │ │ ├── ctf-business-info │ │ │ │ ├── __generated │ │ │ │ │ └── business-info.generated.ts │ │ │ │ ├── business-info.graphql │ │ │ │ ├── ctf-business-info-gql.tsx │ │ │ │ └── ctf-business-info.tsx │ │ │ ├── ctf-cta │ │ │ │ ├── __generated │ │ │ │ │ └── ctf-cta.generated.ts │ │ │ │ ├── ctf-cta-gql.tsx │ │ │ │ ├── ctf-cta.graphql │ │ │ │ └── ctf-cta.tsx │ │ │ ├── ctf-duplex │ │ │ │ ├── __generated │ │ │ │ │ └── ctf-duplex.generated.ts │ │ │ │ ├── ctf-duplex-gql.tsx │ │ │ │ ├── ctf-duplex.graphql │ │ │ │ └── ctf-duplex.tsx │ │ │ ├── ctf-footer │ │ │ │ ├── __generated │ │ │ │ │ └── ctf-footer.generated.ts │ │ │ │ ├── ctf-footer-gql.tsx │ │ │ │ ├── ctf-footer.graphql │ │ │ │ └── ctf-footer.tsx │ │ │ ├── ctf-hero-banner │ │ │ │ ├── __generated │ │ │ │ │ └── ctf-hero-banner.generated.ts │ │ │ │ ├── ctf-hero-banner-gql.tsx │ │ │ │ ├── ctf-hero-banner.graphql │ │ │ │ └── ctf-hero-banner.tsx │ │ │ ├── ctf-image │ │ │ │ └── ctf-image.tsx │ │ │ ├── ctf-info-block │ │ │ │ ├── __generated │ │ │ │ │ └── ctf-info-block.generated.ts │ │ │ │ ├── ctf-info-block-gql.tsx │ │ │ │ ├── ctf-info-block.graphql │ │ │ │ └── ctf-info-block.tsx │ │ │ ├── ctf-mobile-menu │ │ │ │ ├── ctf-mobile-menu-gql.tsx │ │ │ │ └── ctf-mobile-menu.tsx │ │ │ ├── ctf-navigation │ │ │ │ ├── __generated │ │ │ │ │ └── ctf-navigation.generated.ts │ │ │ │ ├── ctf-navigation-gql.tsx │ │ │ │ ├── ctf-navigation.graphql │ │ │ │ ├── ctf-navigation.tsx │ │ │ │ └── utils.ts │ │ │ ├── ctf-page │ │ │ │ ├── __generated │ │ │ │ │ └── ctf-page.generated.ts │ │ │ │ ├── ctf-page-gql.tsx │ │ │ │ ├── ctf-page.graphql │ │ │ │ └── ctf-page.tsx │ │ │ ├── ctf-person │ │ │ │ ├── __generated │ │ │ │ │ └── ctf-person.generated.ts │ │ │ │ ├── ctf-person-gql.tsx │ │ │ │ ├── ctf-person.graphql │ │ │ │ └── ctf-person.tsx │ │ │ ├── ctf-product-feature │ │ │ │ ├── __generated │ │ │ │ │ └── ctf-product-feature.generated.ts │ │ │ │ └── ctf-product-feature.graphql │ │ │ ├── ctf-product-table │ │ │ │ ├── __generated │ │ │ │ │ └── ctf-product-table.generated.ts │ │ │ │ ├── ctf-product-table-gql.tsx │ │ │ │ ├── ctf-product-table.graphql │ │ │ │ └── ctf-product-table.tsx │ │ │ ├── ctf-product │ │ │ │ ├── __generated │ │ │ │ │ └── ctf-product.generated.ts │ │ │ │ ├── ctf-product-gql.tsx │ │ │ │ ├── ctf-product.graphql │ │ │ │ └── ctf-product.tsx │ │ │ ├── ctf-quote │ │ │ │ ├── __generated │ │ │ │ │ └── ctf-quote.generated.ts │ │ │ │ ├── ctf-quote-gql.tsx │ │ │ │ ├── ctf-quote.graphql │ │ │ │ └── ctf-quote.tsx │ │ │ ├── ctf-richtext │ │ │ │ ├── __generated │ │ │ │ │ └── ctf-richtext.generated.ts │ │ │ │ ├── ctf-richtext.graphql │ │ │ │ └── ctf-richtext.tsx │ │ │ ├── ctf-text-block │ │ │ │ ├── __generated │ │ │ │ │ └── ctf-text-block.generated.ts │ │ │ │ ├── ctf-text-block-gql.tsx │ │ │ │ ├── ctf-text-block.graphql │ │ │ │ └── ctf-text-block.tsx │ │ │ └── ctf-video │ │ │ │ └── ctf-video.tsx │ │ ├── errors │ │ │ ├── entry-not-found.tsx │ │ │ ├── page-error.tsx │ │ │ └── page-graphql-error.tsx │ │ ├── format-currency │ │ │ ├── format-currency.tsx │ │ │ └── index.ts │ │ ├── language-selector │ │ │ ├── LanguageSelector.tsx │ │ │ └── index.ts │ │ ├── markdown.tsx │ │ ├── page-link │ │ │ ├── __generated │ │ │ │ ├── category-link.generated.ts │ │ │ │ ├── page-link.generated.ts │ │ │ │ └── post-link.generated.ts │ │ │ ├── index.ts │ │ │ ├── page-link.graphql │ │ │ └── page-link.tsx │ │ ├── section-headlines │ │ │ ├── index.ts │ │ │ └── section-headlines.tsx │ │ └── settings │ │ │ ├── index.ts │ │ │ ├── settings-form.tsx │ │ │ └── settings.tsx │ ├── shared │ │ ├── component-resolver.tsx │ │ ├── error-box.tsx │ │ ├── graphql-error.tsx │ │ └── link.tsx │ └── templates │ │ ├── header │ │ ├── header.tsx │ │ └── index.ts │ │ ├── layout │ │ ├── index.ts │ │ └── layout.tsx │ │ └── page-container │ │ ├── index.ts │ │ └── page-container.tsx ├── contentful-context.tsx ├── icons │ ├── colorful-coin-logo.svg │ ├── logo-tagline.svg │ └── settings-icon.svg ├── layout-context.ts ├── lib │ ├── __generated │ │ ├── graphql.schema.graphql │ │ ├── graphql.schema.json │ │ └── graphql.types.ts │ ├── fetchConfig.ts │ ├── get-serverside-translations.ts │ ├── gql-client.ts │ ├── prefetch-mappings.ts │ ├── prefetch-promise-array.ts │ └── shared-fragments │ │ ├── __generated │ │ ├── ctf-componentMap.generated.ts │ │ └── ctf-menuGroup.generated.ts │ │ ├── ctf-componentMap.graphql │ │ └── ctf-menuGroup.graphql ├── mappings.ts ├── pages │ ├── 404.tsx │ ├── [slug].tsx │ ├── _app.tsx │ ├── _document.tsx │ ├── _error.tsx │ └── index.tsx ├── polyfills.ts ├── theme.ts └── utils.ts ├── tsconfig.json ├── vercel.json └── yarn.lock /.contentful/vault-secrets.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | services: 3 | github-action: 4 | policies: 5 | - dependabot 6 | - packages-read -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | 17 | # Ignore paths 18 | [/.next/**] 19 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | BUNDLE_ANALYZE=false 2 | ENVIRONMENT_NAME=local 3 | 4 | # The URL for the domain your app is hosted on (used for generating the urls needed for SEO) 5 | # If you deploy to Vercel or Netlify it is configured through the respective config files (`vercel.json` and `netlify.toml`) 6 | NEXT_PUBLIC_BASE_URL=http://localhost:3000/ 7 | 8 | # Your current space ID: https://www.contentful.com/help/find-space-id/ 9 | CONTENTFUL_SPACE_ID= 10 | 11 | # Your current space Content Delivery API access token: https://www.contentful.com/developers/docs/references/content-delivery-api/ 12 | CONTENTFUL_ACCESS_TOKEN= 13 | 14 | # Your current space Content Preview API access token: https://www.contentful.com/developers/docs/references/content-preview-api/ 15 | CONTENTFUL_PREVIEW_ACCESS_TOKEN= 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | scripts/**/*.js 2 | src/**/__generated 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@babel/eslint-parser', 4 | parserOptions: { 5 | requireConfigFile: false, 6 | }, 7 | env: { 8 | browser: true, 9 | commonjs: true, 10 | es6: true, 11 | jest: true, 12 | node: true, 13 | }, 14 | plugins: ['react-hooks'], 15 | settings: { 16 | react: { 17 | version: 'detect', 18 | }, 19 | }, 20 | extends: [ 21 | 'plugin:prettier/recommended', 22 | 'plugin:import/errors', 23 | 'plugin:import/warnings', 24 | 'plugin:import/typescript', 25 | 'plugin:@typescript-eslint/eslint-recommended', 26 | 'plugin:@typescript-eslint/recommended', 27 | 'plugin:react/recommended', 28 | 'plugin:jsx-a11y/recommended', 29 | 'plugin:@next/next/recommended', 30 | ], 31 | rules: { 32 | '@typescript-eslint/no-var-requires': 'off', 33 | '@typescript-eslint/explicit-function-return-type': 'off', 34 | '@typescript-eslint/no-non-null-assertion': 'off', 35 | '@typescript-eslint/no-explicit-any': 'off', 36 | '@typescript-eslint/no-use-before-define': 'off', 37 | '@typescript-eslint/explicit-module-boundary-types': 'off', 38 | '@typescript-eslint/ban-ts-comment': 'off', 39 | '@typescript-eslint/no-unused-vars': [ 40 | 'warn', 41 | { 42 | argsIgnorePattern: '^_|req|res|next|err|ctx|args|context|info', 43 | }, 44 | ], 45 | '@typescript-eslint/no-object-literal-type-assertion': 'off', 46 | '@typescript-eslint/explicit-member-accessibility': 'off', 47 | '@typescript-eslint/camelcase': 'off', 48 | '@typescript-eslint/no-empty-interface': 'off', 49 | '@typescript-eslint/ban-ts-ignore': 'off', 50 | 'prettier/prettier': 'off', 51 | 'import/no-named-as-default': 'off', 52 | 'import/no-named-as-default-member': 'off', 53 | 'import/default': 'off', 54 | 'import/named': 'off', 55 | 'import/namespace': 'off', 56 | 'import/order': [ 57 | 'warn', 58 | { 59 | groups: ['builtin', 'external', ['parent', 'sibling'], 'index'], 60 | 'newlines-between': 'always', 61 | pathGroups: [ 62 | { 63 | pattern: '@/**', 64 | group: 'external', 65 | position: 'after', 66 | }, 67 | { 68 | pattern: '@test/**', 69 | group: 'external', 70 | position: 'after', 71 | }, 72 | ], 73 | alphabetize: { 74 | order: 'asc' /* sort in ascending order. Options: ['ignore', 'asc', 'desc'] */, 75 | caseInsensitive: true /* ignore case. Options: [true, false] */, 76 | }, 77 | }, 78 | ], 79 | 'jsx-a11y/click-events-have-key-events': 'off', 80 | 'jsx-a11y/anchor-is-valid': 'off', 81 | 'react/prop-types': 'off', 82 | 'react/display-name': 'off', 83 | 'react-hooks/rules-of-hooks': 'error', 84 | 'react-hooks/exhaustive-deps': 'warn', 85 | 'react/jsx-uses-react': 'off', 86 | 'react/react-in-jsx-scope': 'off', 87 | 'react/self-closing-comp': 'warn', 88 | }, 89 | overrides: [ 90 | { 91 | files: ['**/*.ts?(x)'], 92 | parser: '@typescript-eslint/parser', 93 | parserOptions: { 94 | ecmaVersion: 2018, 95 | sourceType: 'module', 96 | ecmaFeatures: { 97 | jsx: true, 98 | }, 99 | // typescript-eslint specific options 100 | warnOnUnsupportedTypeScriptVersion: true, 101 | }, 102 | settings: { 103 | 'import/parsers': { 104 | '@typescript-eslint/parser': ['.ts', '.tsx'], 105 | }, 106 | 'import/resolver': { 107 | typescript: { 108 | alwaysTryTypes: true, 109 | }, 110 | }, 111 | }, 112 | }, 113 | ], 114 | globals: { 115 | React: 'writable', 116 | }, 117 | }; 118 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @contentful/team-isotopes 2 | package.json 3 | yarn.lock 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug Report" 3 | about: Spotted a bug? Add a report to help us improve this template 4 | title: "\U0001F41B Bug - " 5 | labels: bug, needs triage 6 | --- 7 | 8 | # Bug report 9 | 10 | ## Summary 11 | 12 | 15 | 16 | ## Environment 17 | 18 | 21 | 22 | ## Steps to reproduce 23 | 24 | 27 | 28 | ## Expected results 29 | 30 | 33 | 34 | ## Actual results 35 | 36 | 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feedback.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4AC Give feedback on a component" 3 | about: Give us feedback to help us improve this template 4 | title: "\U0001F4AC Feedback - " 5 | labels: needs triage 6 | --- 7 | 8 | 12 | 13 | # Template feedback 14 | 15 | 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/proposal.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4A1 Proposal" 3 | about: Want to extend the template or modify existing functionality? Send us a proposal 4 | title: "\U0001F4A1 Proposal - " 5 | labels: needs review, needs triage, proposal 6 | --- 7 | 8 | 12 | 13 | # Template contribution proposal 14 | 15 | ## The problem 16 | 17 | 20 | 21 | ## The proposed solution 22 | 23 | 26 | 27 | ## Breaking changes 28 | 29 | 32 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose of PR 2 | 3 | * Stuff that is going to change 4 | * even more interesting stuff that is going to change 5 | 6 | 12 | 13 | 25 | 26 | ## Security 27 | 28 | _Before you click Merge, take a step back and think how someone could break the [Confidentiality, Integrity and Availability](https://docs.google.com/presentation/d/1YdFlYBLnbNoiSAMOKjopiF4u34StXTK2qYdOLkMsEKo/edit?usp=sharing) of the code you've just written. Are secrets secret? Is there any sensitive information disclosed in logs or error messages? How does your code ensure that information is accurate, complete and protected from modification? Will your code keep Contentful working and functioning?_ 29 | 30 | 34 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | registries: 4 | npm-registry-registry-npmjs-org: 5 | type: npm-registry 6 | url: https://registry.npmjs.org 7 | token: '${{secrets.NPM_REGISTRY_REGISTRY_NPMJS_ORG_TOKEN}}' 8 | 9 | updates: 10 | - package-ecosystem: 'npm' 11 | directory: '/' 12 | schedule: 13 | interval: 'daily' 14 | time: '05:00' 15 | timezone: UTC 16 | commit-message: 17 | prefix: build 18 | include: scope 19 | labels: 20 | - 'dependencies' 21 | - 'dependabot' 22 | open-pull-requests-limit: 2 23 | reviewers: 24 | - 'contentful/team-tolkien' 25 | registries: 26 | - npm-registry-registry-npmjs-org 27 | allow: 28 | - dependency-name: '@contentful/live-preview' 29 | 30 | - package-ecosystem: github-actions 31 | directory: '/' 32 | schedule: 33 | interval: daily 34 | time: '05:00' 35 | timezone: UTC 36 | open-pull-requests-limit: 15 37 | commit-message: 38 | prefix: build 39 | include: scope 40 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: 'dependabot approve-and-request-merge' 2 | 3 | on: pull_request_target 4 | 5 | jobs: 6 | worker: 7 | permissions: 8 | contents: write 9 | id-token: write 10 | runs-on: ubuntu-latest 11 | if: github.actor == 'dependabot[bot]' 12 | steps: 13 | - uses: contentful/github-auto-merge@v1 14 | with: 15 | VAULT_URL: ${{ secrets.VAULT_URL }} 16 | -------------------------------------------------------------------------------- /.github/workflows/eslint-tsc.yml: -------------------------------------------------------------------------------- 1 | # Workflow name 2 | name: ESLint & TSC 3 | 4 | # Event for the workflow 5 | on: [pull_request] 6 | 7 | # List of jobs 8 | jobs: 9 | eslint-tsc: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Read .nvmrc 18 | run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)" 19 | id: nvm 20 | 21 | - name: Use Node.js (.nvmrc) 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: "${{ steps.nvm.outputs.NVMRC }}" 25 | 26 | - name: Get Yarn cache directory 27 | id: yarn-cache-dir-path 28 | run: echo "::set-output name=dir::$(yarn cache dir)" 29 | 30 | - name: Use Yarn cache 31 | uses: actions/cache@v4.2.3 32 | id: yarn-cache 33 | with: 34 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 35 | key: ${{ runner.os }}-yarn-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }} 36 | 37 | - name: Install dependencies 38 | run: yarn install --frozen-lockfile --prefer-offline 39 | # `--prefer-offline` gives cache priority 40 | 41 | - name: ESLint 42 | run: yarn lint 43 | 44 | - name: TSC 45 | run: yarn type-check 46 | -------------------------------------------------------------------------------- /.github/workflows/sast.yaml: -------------------------------------------------------------------------------- 1 | name: SAST (Static Application Security Testing) 2 | 3 | on: 4 | push: 5 | branches: [master, main, main-private] 6 | pull_request: 7 | branches: [master, main, main-private] 8 | 9 | jobs: 10 | polaris: 11 | name: polaris / code-scan 12 | continue-on-error: true 13 | runs-on: ubuntu-latest 14 | if: (github.repository_owner == 'contentful') && (endsWith(github.actor, '[bot]') == false) 15 | steps: 16 | - name: Clone repo 17 | uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Synopsys Polaris 22 | uses: contentful/polaris-action@master 23 | with: 24 | github_token: ${{ secrets.GITHUB_TOKEN }} 25 | polaris_url: ${{ secrets.POLARIS_SERVER_URL }} 26 | polaris_access_token: ${{ secrets.POLARIS_ACCESS_TOKEN }} 27 | debug: true 28 | polaris_command: analyze -w --coverity-ignore-capture-failure 29 | security_gate_filters: '{ "severity": ["High", "Medium"] }' 30 | fail_on_error: false 31 | report_url: "https://github.com/contentful/security-tools-config/issues/new?title=False%20positive%20in%20Polaris" 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Custom ### 2 | contentful.config.secret.json 3 | dist 4 | .next 5 | out 6 | tsconfig.tsbuildinfo 7 | 8 | 9 | ### Paw ### 10 | *.paw 11 | 12 | # Created by https://www.gitignore.io/api/linux,osx,visualstudiocode,sublimetext,intellij,node,bower 13 | 14 | ### Linux ### 15 | *~ 16 | 17 | # temporary files which can be created if a process still has a handle open of a deleted file 18 | .fuse_hidden* 19 | 20 | # KDE directory preferences 21 | .directory 22 | 23 | # Linux trash folder which might appear on any partition or disk 24 | .Trash-* 25 | 26 | ### OSX ### 27 | *.DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Icon must end with two \r 32 | Icon 33 | 34 | 35 | # Thumbnails 36 | ._* 37 | 38 | # Files that might appear in the root of a volume 39 | .DocumentRevisions-V100 40 | .fseventsd 41 | .Spotlight-V100 42 | .TemporaryItems 43 | .Trashes 44 | .VolumeIcon.icns 45 | .com.apple.timemachine.donotpresent 46 | 47 | # Directories potentially created on remote AFP share 48 | .AppleDB 49 | .AppleDesktop 50 | Network Trash Folder 51 | Temporary Items 52 | .apdisk 53 | 54 | 55 | ### VisualStudioCode ### 56 | .vscode/settings.json 57 | 58 | ### SublimeText ### 59 | # cache files for sublime text 60 | *.tmlanguage.cache 61 | *.tmPreferences.cache 62 | *.stTheme.cache 63 | 64 | # workspace files are user-specific 65 | *.sublime-workspace 66 | 67 | # project files should be checked into the repository, unless a significant 68 | # proportion of contributors will probably not be using SublimeText 69 | # *.sublime-project 70 | 71 | # sftp configuration file 72 | sftp-config.json 73 | 74 | # Package control specific files 75 | Package Control.last-run 76 | Package Control.ca-list 77 | Package Control.ca-bundle 78 | Package Control.system-ca-bundle 79 | Package Control.cache/ 80 | Package Control.ca-certs/ 81 | bh_unicode_properties.cache 82 | 83 | # Sublime-github package stores a github token in this file 84 | # https://packagecontrol.io/packages/sublime-github 85 | GitHub.sublime-settings 86 | 87 | ### Intellij ### 88 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 89 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 90 | 91 | # User-specific stuff: 92 | .idea/workspace.xml 93 | .idea/tasks.xml 94 | .idea/dictionaries 95 | .idea/vcs.xml 96 | .idea/jsLibraryMappings.xml 97 | 98 | # Sensitive or high-churn files: 99 | .idea/dataSources.ids 100 | .idea/dataSources.xml 101 | .idea/dataSources.local.xml 102 | .idea/sqlDataSources.xml 103 | .idea/dynamic.xml 104 | .idea/uiDesigner.xml 105 | 106 | # Gradle: 107 | .idea/gradle.xml 108 | .idea/libraries 109 | 110 | # Mongo Explorer plugin: 111 | .idea/mongoSettings.xml 112 | 113 | ## File-based project format: 114 | *.iws 115 | 116 | ## Plugin-specific files: 117 | 118 | # IntelliJ 119 | /out/ 120 | 121 | # mpeltonen/sbt-idea plugin 122 | .idea_modules/ 123 | 124 | # JIRA plugin 125 | atlassian-ide-plugin.xml 126 | 127 | # Crashlytics plugin (for Android Studio and IntelliJ) 128 | com_crashlytics_export_strings.xml 129 | crashlytics.properties 130 | crashlytics-build.properties 131 | fabric.properties 132 | 133 | ### Intellij Patch ### 134 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 135 | 136 | # *.iml 137 | # modules.xml 138 | # .idea/misc.xml 139 | # *.ipr 140 | 141 | ### Node ### 142 | # Logs 143 | logs 144 | *.log 145 | npm-debug.log* 146 | 147 | # Runtime data 148 | pids 149 | *.pid 150 | *.seed 151 | *.pid.lock 152 | 153 | # Directory for instrumented libs generated by jscoverage/JSCover 154 | lib-cov 155 | 156 | # Coverage directory used by tools like istanbul 157 | coverage 158 | 159 | # nyc test coverage 160 | .nyc_output 161 | 162 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 163 | .grunt 164 | 165 | # node-waf configuration 166 | .lock-wscript 167 | 168 | # Compiled binary addons (http://nodejs.org/api/addons.html) 169 | build/Release 170 | 171 | # Dependency directories 172 | node_modules 173 | jspm_packages 174 | 175 | # Optional npm cache directory 176 | .npm 177 | 178 | # Optional REPL history 179 | .node_repl_history 180 | 181 | ### Bower ### 182 | bower_components 183 | .bower-cache 184 | .bower-registry 185 | .bower-tmp 186 | /public/data/mind.json 187 | 188 | .env 189 | 190 | ### Intellij ### 191 | .idea/ 192 | 193 | ### Install artifacts ### 194 | package-lock.json 195 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn type-check && yarn lint-staged -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn type-check && yarn lint-staged -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @contentful:registry=https://registry.yarnpkg.com 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.16.1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | src/**/__generated 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "semi": true, 4 | "arrowParens": "avoid", 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "jsxBracketSameLine": true 8 | } 9 | -------------------------------------------------------------------------------- /.vercelignore: -------------------------------------------------------------------------------- 1 | .env 2 | -------------------------------------------------------------------------------- /@types/catchify/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'catchify' { 2 | function catchify(a: Promise): Promise<[E, T]>; 3 | export default catchify; 4 | } 5 | -------------------------------------------------------------------------------- /@types/mui.d.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from '@mui/material/styles'; 2 | 3 | declare module '@mui/styles/defaultTheme' { 4 | interface DefaultTheme extends Theme {} 5 | } 6 | -------------------------------------------------------------------------------- /@types/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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | The changelog is automatically updated using 4 | [semantic-release](https://github.com/semantic-release/semantic-release). You 5 | can see it on the [releases page](https://github.com/contentful/template-marketing-webapp-nextjs/releases). 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contentful Community Code of Conduct 2 | 3 | The **Contentful Community** and the **Code of Conduct** governs all interactions with the contentful community. 4 | 5 | The **Contentful Community** is dedicated to providing a safe, inclusive, welcoming, and harassment-free space and experience for all community participants, regardless of gender identity and expression, sexual orientation, disability, physical appearance, socioeconomic status, body size, ethnicity, nationality, level of experience, age, religion (or lack thereof), or other identity markers. 6 | 7 | Our **Code of Conduct** exists because of that dedication, and we do not tolerate harassment in any form. See our full Code of Conduct and reporting guidelines at this [link](https://www.contentful.com/developers/code-of-conduct/). 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | We appreciate any community contributions to this project, whether in the form of issues or Pull Requests. 4 | 5 | This document outlines what we'd like you to follow in terms of commit messages and code style. 6 | 7 | It also explains what to do in case you want to set up the project locally. 8 | 9 | If you have any questions or concerns please reach out to us either by filing an issue in the relevant repository or posting in the [Contentful Community Slack](https://www.contentful.com/slack/). 10 | 11 | ## How Can I Contribute? 12 | 13 | Before creating a new issue; please check out the open issues as someone might have already created one for you! Please use the recommended templates for each section as helps us resolve issues faster. 14 | 15 | ### Reporting Bugs 16 | 17 | Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/). If you are sure there's currently not an issue that describes the bug you're experiencing, create an issue on the repository and provide the following information by filling in [the template](https://github.com/contentful/template-marketing-webapp-nextjs/tree/main/.github/ISSUE_TEMPLATE/bug-report.md). 18 | 19 | > **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one. 20 | 21 | ### Proposals 22 | 23 | Want to make an enhancement proposal, including completely new features and minor improvements to existing functionality? Please create an issue with [the proposal template](https://github.com/contentful/template-marketing-webapp-nextjs/tree/main/.github/ISSUE_TEMPLATE/proposal.md). 24 | 25 | ### General feedback 26 | 27 | Not experiencing a bug or wanting to make a proposal, but still want to reach out? A lot of our colleagues are hanging out in [Contentful Community Slack](https://www.contentful.com/slack/) and might be able to help. Can't find your answer there? You can file a feedback issue through [this template](https://github.com/contentful/template-marketing-webapp-nextjs/tree/main/.github/ISSUE_TEMPLATE/feedback.md). 28 | 29 | ## Pull Requests 30 | 31 | The process described here has several goals: 32 | 33 | - Maintain the quality of the repository 34 | - Fix problems that are important to users 35 | - Enable a sustainable system for the maintainers from Contentful to review contributions 36 | 37 | Please follow these steps to have your contribution considered by the maintainers: 38 | 39 | 1. Follow all instructions in [the template](https://github.com/contentful/template-marketing-webapp-nextjs/tree/main/.github/PULL_REQUEST_TEMPLATE.md) 40 | 2. After you submit your pull request, verify that all [status checks](https://help.github.com/articles/about-status-checks/) are passing
What if the status checks are failing?If a status check is failing, and you believe that the failure is unrelated to your change, please leave a comment on the pull request explaining why you believe the failure is unrelated. A maintainer will re-run the status check for you. If we conclude that the failure was a false positive, then we will open an issue to track that problem with our status check suite.
41 | 42 | While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted. 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Contentful 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 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security Policy 2 | 3 | Security at Contentful 4 | 5 | Security being just important to us is a huge understatement. Security is a top priority at Contentful, and we live it in our day-to-day activities. 6 | 7 | If you believe you have found a security vulnerability in any Contentful-owned repository, please report it to us as described below. 8 | 9 | ## Supported Versions 10 | 11 | Refer to individual repositories for supported versions. 12 | 13 | ## Reporting a Vulnerability 14 | 15 | Contentful engages with the community via our Responsible Disclosure Program, also known as our Bug Bounty Program. Our community plays an important role in helping us stay bug-free and secure. 16 | 17 | Found a vulnerability? Would you like to report a bug or something interesting that you found? The best way to reach out to us is via the submission form at the end of the [page](https://www.contentful.com/security/). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | Report security vulnerabilities in third-party modules to the person or team maintaining the module. 32 | 33 | -------------------------------------------------------------------------------- /bin/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if .env file already exists 4 | if [ -f .env ]; then 5 | echo ".env.example file does not exist. Aborting." 6 | exit 1 7 | fi 8 | 9 | # Check if .env.example file exists 10 | if [ ! -f .env.example ]; then 11 | echo ".env.example file does not exist. Aborting." 12 | exit 1 13 | fi 14 | 15 | # Copy .env.example to .env 16 | cp .env.example .env 17 | 18 | # Read the values of the variables from command-line arguments 19 | for arg in "$@"; do 20 | if [[ "$arg" != *"="* ]]; then 21 | echo "Invalid argument: $arg. Arguments must be in the format KEY=VALUE. Aborting." 22 | exit 1 23 | fi 24 | 25 | key=$(echo "$arg" | cut -d'=' -f1) 26 | value=$(echo "$arg" | cut -d'=' -f2) 27 | 28 | # Sanitize inputs 29 | if [[ "$key" != *[![:alnum:]_]* ]]; then 30 | sed -e "s/^${key}=.*/${key}=${value}/" .env > temp && mv temp .env 31 | else 32 | echo "Invalid key: $key. Keys must contain only letters, numbers, and underscores. Aborting." 33 | exit 1 34 | fi 35 | done 36 | 37 | # install node_modules and run dev 38 | echo Installing dependencies 39 | yarn 40 | yarn dev 41 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | # Backstage documentation 2 | # https://backstage.io/docs/features/software-catalog/descriptor-format/ 3 | 4 | apiVersion: backstage.io/v1alpha1 5 | kind: Template 6 | metadata: 7 | name: template-marketing-webapp-nextjs 8 | description: Next.js marketing website starter template 9 | annotations: 10 | github.com/project-slug: contentful/template-marketing-webapp-nextjs 11 | contentful.com/service-tier: "4" 12 | 13 | tags: 14 | - starter-templates 15 | - marketing-website 16 | - nextjs 17 | - graphql 18 | - typescript 19 | - material-ui 20 | #need to add sast.yaml to .github/workflows and enable it in polaris dashboard 21 | #once that is done this can be changed to sast-enabled 22 | - sast-disabled 23 | - tier-4 24 | spec: 25 | type: template 26 | lifecycle: production 27 | owner: group:team-isotopes 28 | -------------------------------------------------------------------------------- /codegen.ts: -------------------------------------------------------------------------------- 1 | import { CodegenConfig } from '@graphql-codegen/cli'; 2 | 3 | import { fetchConfig } from './src/lib/fetchConfig'; 4 | 5 | export const config: CodegenConfig = { 6 | overwrite: true, 7 | ignoreNoDocuments: true, 8 | schema: [ 9 | { 10 | [fetchConfig.endpoint]: fetchConfig.params, 11 | }, 12 | ], 13 | generates: { 14 | './src/lib/__generated/graphql.schema.json': { 15 | plugins: ['introspection'], 16 | }, 17 | './src/lib/__generated/graphql.schema.graphql': { 18 | plugins: ['schema-ast'], 19 | }, 20 | './src/lib/__generated/graphql.types.ts': { 21 | plugins: ['typescript', 'typescript-operations'], 22 | documents: ['./src/**/*.graphql'], 23 | }, 24 | './src/': { 25 | documents: ['./src/**/*.graphql'], 26 | preset: 'near-operation-file', 27 | presetConfig: { 28 | extension: '.generated.ts', 29 | baseTypesPath: 'lib/__generated/graphql.types.ts', 30 | folder: '__generated', 31 | }, 32 | plugins: [ 33 | 'typescript-operations', 34 | 'typescript-react-query', 35 | ], 36 | config: { 37 | exposeQueryKeys: true, 38 | exposeFetcher: true, 39 | rawRequest: false, 40 | inlineFragmentTypes: 'combine', 41 | skipTypename: false, 42 | exportFragmentSpreadSubTypes: true, 43 | dedupeFragments: true, 44 | preResolveTypes: true, 45 | withHooks: true, 46 | fetcher: '@src/lib/fetchConfig#customFetcher', 47 | }, 48 | }, 49 | }, 50 | }; 51 | 52 | export default config; 53 | -------------------------------------------------------------------------------- /config/headers.js: -------------------------------------------------------------------------------- 1 | const securityHeaders = [ 2 | { 3 | key: 'X-DNS-Prefetch-Control', 4 | value: 'on', 5 | }, 6 | { 7 | key: 'Strict-Transport-Security', 8 | value: 'max-age=63072000; includeSubDomains; preload', 9 | }, 10 | { 11 | key: 'X-Frame-Options', 12 | value: 'SAMEORIGIN', 13 | }, 14 | { 15 | key: 'Content-Security-Policy', 16 | value: `frame-ancestors 'self' https://app.contentful.com https://app.eu.contentful.com`, 17 | }, 18 | { 19 | key: 'X-Content-Type-Options', 20 | value: 'nosniff', 21 | }, 22 | { 23 | key: 'X-XSS-Protection', 24 | value: '1; mode=block', 25 | }, 26 | { 27 | key: 'Referrer-Policy', 28 | value: 'no-referrer', 29 | }, 30 | ]; 31 | 32 | module.exports = async () => { 33 | return [ 34 | { 35 | // Apply these headers to all routes in your application. 36 | source: '/:path*', 37 | headers: securityHeaders, 38 | }, 39 | ]; 40 | }; 41 | -------------------------------------------------------------------------------- /config/includePolyfills.js: -------------------------------------------------------------------------------- 1 | module.exports = function includePolyfills(config) { 2 | const originalEntry = config.entry; 3 | 4 | config.entry = async () => { 5 | const entries = await originalEntry(); 6 | 7 | if (entries['main.js'] && !entries['main.js'].includes('./src/polyfills.ts')) { 8 | entries['main.js'].unshift('./src/polyfills.ts'); 9 | } 10 | 11 | return entries; 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /config/plugins.js: -------------------------------------------------------------------------------- 1 | const withBundleAnalyzer = require('@next/bundle-analyzer'); 2 | const withPWA = require('next-pwa'); 3 | 4 | module.exports = [ 5 | [ 6 | withPWA, 7 | { 8 | pwa: { 9 | disable: process.env.NODE_ENV !== 'production', 10 | dest: `public`, 11 | register: false, 12 | swSrc: './service-worker.js', 13 | publicExcludes: ['!favicon/**/*'], 14 | }, 15 | }, 16 | ], 17 | [ 18 | withBundleAnalyzer, 19 | { 20 | enabled: process.env.BUNDLE_ANALYZE !== 'true', 21 | }, 22 | ], 23 | ]; 24 | -------------------------------------------------------------------------------- /contentful.config.js: -------------------------------------------------------------------------------- 1 | const url = process.env.NEXT_PUBLIC_BASE_URL; 2 | 3 | module.exports = { 4 | contentful: { 5 | space_id: process.env.CONTENTFUL_SPACE_ID || '', 6 | cda_token: process.env.CONTENTFUL_ACCESS_TOKEN || '', 7 | cpa_token: process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN || '', 8 | }, 9 | meta: { 10 | title: 'Digital banking for the new generation | Colorful Coin', 11 | description: `Enjoy premium banking services wherever you go: instant transfers and best exchange rates, exclusive offers and priority customer support. Apply online at ${url 12 | .replace('https://', '') 13 | .replace('http://', '')}`, 14 | url, 15 | image: 16 | 'https://images.ctfassets.net/w8vf7dk7f259/4bucno7z1xAyVI5MOkU6Pu/ded83d0ec1eb732ae3a81ddab7a18877/fallback-image-03.jpg', 17 | }, 18 | icon: { 19 | light: 20 | 'https://images.ctfassets.net/w8vf7dk7f259/llZXwDCnl9NqdyuVvjn1n/d20cea90225e7f53dfbf8a18a46e972d/gocoin-icon-light.svg', 21 | dark: 'https://images.ctfassets.net/w8vf7dk7f259/i9iu6GU6dFWQJJwJzwxCT/952cc3bab415e28f521c22933072a09c/gocoin-icon.svg', 22 | width: 66, 23 | height: 64, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /marketing-starter-template.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/template-marketing-webapp-nextjs/5d95361a69f0453a7263efffbefac6dbe02b6e3a/marketing-starter-template.jpg -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "NEXT_PUBLIC_BASE_URL=\"${NEXT_PUBLIC_BASE_URL:-$URL}\" yarn build" 3 | -------------------------------------------------------------------------------- /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 | const path = require('path'); 2 | 3 | module.exports = { 4 | i18n: { 5 | defaultLocale: 'en-US', 6 | locales: ['en-US', 'de-DE'], 7 | localeDetection: false, 8 | localePath: path.resolve('./public/locales'), 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const nextComposePlugins = require('next-compose-plugins'); 3 | 4 | const headers = require('./config/headers'); 5 | const includePolyfills = require('./config/includePolyfills'); 6 | const plugins = require('./config/plugins'); 7 | const { i18n } = require('./next-i18next.config.js'); 8 | 9 | /** 10 | * https://github.com/cyrilwanner/next-compose-plugins/issues/59 11 | */ 12 | const { withPlugins } = nextComposePlugins.extend(() => ({})); 13 | 14 | /** 15 | * Next config 16 | * documentation: https://nextjs.org/docs/api-reference/next.config.js/introduction 17 | */ 18 | module.exports = withPlugins(plugins, { 19 | i18n, 20 | /** 21 | * add the environment variables you would like exposed to the client here 22 | * documentation: https://nextjs.org/docs/api-reference/next.config.js/environment-variables 23 | */ 24 | env: { 25 | ENVIRONMENT_NAME: process.env.ENVIRONMENT_NAME, 26 | CONTENTFUL_SPACE_ID: process.env.CONTENTFUL_SPACE_ID, 27 | CONTENTFUL_ACCESS_TOKEN: process.env.CONTENTFUL_ACCESS_TOKEN, 28 | CONTENTFUL_PREVIEW_ACCESS_TOKEN: process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN, 29 | }, 30 | 31 | /** 32 | * The experimental option allows you to enable future/experimental options 33 | * like React 18 concurrent features. 34 | */ 35 | experimental: { 36 | // urlImports: true, 37 | // concurrentFeatures: true, 38 | // serverComponents: true, 39 | }, 40 | 41 | /** 42 | * SWC minification opt-in 43 | * Please note that while not in experimental, the swcMinification may cause issues in your build. 44 | * example: https://github.com/vercel/next.js/issues/30429 (Yup email validation causes an exception) 45 | */ 46 | // swcMinify: true, 47 | 48 | poweredByHeader: false, 49 | reactStrictMode: false, 50 | compress: true, 51 | 52 | /** 53 | * add the headers you would like your next server to use 54 | * documentation: https://nextjs.org/docs/api-reference/next.config.js/headers 55 | * https://nextjs.org/docs/advanced-features/security-headers 56 | */ 57 | headers, 58 | 59 | /** 60 | * https://nextjs.org/docs/basic-features/image-optimization 61 | * Settings are the defaults 62 | */ 63 | images: { 64 | deviceSizes: [320, 420, 768, 1024, 1200, 1600], 65 | domains: ['images.ctfassets.net','images.eu.ctfassets.net'], 66 | path: '/_next/image', 67 | loader: 'default', 68 | }, 69 | 70 | webpack(config, options) { 71 | if (!options.isServer || process.env.circularDependencies) { 72 | import('circular-dependency-plugin').then(({ default: CircularDependencyPlugin }) => { 73 | config.plugins.push( 74 | new CircularDependencyPlugin({ 75 | exclude: /a\.js|node_modules/, 76 | failOnError: false, 77 | allowAsyncCycles: true, 78 | cwd: process.cwd(), 79 | }), 80 | ); 81 | }); 82 | } 83 | 84 | config.module.rules.push({ 85 | test: /\.svg$/, 86 | use: ['@svgr/webpack'], 87 | }); 88 | 89 | includePolyfills(config); 90 | 91 | return config; 92 | }, 93 | }); 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "template-marketing-webapp-nextjs", 3 | "description": "", 4 | "private": true, 5 | "version": "0.0.1", 6 | "engines": { 7 | "node": ">=18", 8 | "npm": "Please use Yarn" 9 | }, 10 | "scripts": { 11 | "start": "next start", 12 | "dev": "next dev", 13 | "build": "next build", 14 | "export": "next build && next export", 15 | "prepare": "husky install", 16 | "type-check": "tsc --noEmit", 17 | "test": "echo \"Error: no test specified\" && exit 1", 18 | "migrate:init": "ctf-migrate init", 19 | "migrate:content-model": "ctf-migrate up -a", 20 | "lint": "eslint './src/**/*.{js,jsx,ts,tsx}'", 21 | "lint-fix": "yarn lint --fix", 22 | "graphql-codegen:generate": "graphql-codegen -r dotenv/config --config codegen.ts", 23 | "graphql-codegen:watch": "graphql-codegen --watch -r dotenv/config --config codegen.ts", 24 | "setup": "./bin/setup.sh" 25 | }, 26 | "lint-staged": { 27 | "src/**/*.{js,jsx,ts,tsx}": [ 28 | "eslint --quiet --fix" 29 | ], 30 | "src/**/*.{json,js,ts,jsx,tsx}": [ 31 | "prettier --write" 32 | ] 33 | }, 34 | "license": "MIT", 35 | "dependencies": { 36 | "@contentful/live-preview": "^4.6.21", 37 | "@contentful/rich-text-react-renderer": "^15.12.1", 38 | "@contentful/rich-text-types": "^15.12.1", 39 | "@emotion/react": "^11.10.4", 40 | "@emotion/styled": "^11.10.4", 41 | "@mui/icons-material": "^5.10.9", 42 | "@mui/material": "^5.10.9", 43 | "@mui/styles": "^5.10.9", 44 | "@next/bundle-analyzer": "^12.3.0", 45 | "@svgr/webpack": "^6.3.1", 46 | "@tanstack/react-query": "^4.3.9", 47 | "clsx": "^1.2.1", 48 | "dotenv": "^16.0.1", 49 | "graphql": "^16.5.0", 50 | "intersection-observer": "^0.12.2", 51 | "lodash": "^4.17.21", 52 | "next": "12.3.1", 53 | "next-compose-plugins": "^2.2.1", 54 | "next-i18next": "^12.0.1", 55 | "next-pwa": "^5.6.0", 56 | "query-string": "^7.1.1", 57 | "react": "^18.2.0", 58 | "react-dom": "^18.2.0", 59 | "react-kawaii": "^0.17.0", 60 | "react-transition-group": "^4.4.5", 61 | "rehype-parse": "^8.0.4", 62 | "rehype-react": "^7.1.1", 63 | "remark-breaks": "^3.0.2", 64 | "styled-jsx": "^5.0.2" 65 | }, 66 | "devDependencies": { 67 | "@babel/core": "^7.18.10", 68 | "@babel/eslint-parser": "^7.19.1", 69 | "@graphql-codegen/cli": "^2.12.1", 70 | "@graphql-codegen/introspection": "2.2.1", 71 | "@graphql-codegen/near-operation-file-preset": "^2.4.1", 72 | "@graphql-codegen/typescript": "2.7.3", 73 | "@graphql-codegen/typescript-operations": "2.5.3", 74 | "@graphql-codegen/typescript-react-query": "^4.0.1", 75 | "@tanstack/react-query-devtools": "^4.12.0", 76 | "@types/imagesloaded": "^4.1.2", 77 | "@types/lodash": "^4.14.182", 78 | "@types/react": "^18.0.15", 79 | "@types/react-transition-group": "^4.4.5", 80 | "@typescript-eslint/eslint-plugin": "^5.0.0", 81 | "@typescript-eslint/parser": "^5.32.0", 82 | "babel-jest": "^27.0.2", 83 | "babel-loader": "^8.2.5", 84 | "babel-plugin-import": "^1.13.3", 85 | "circular-dependency-plugin": "^5.2.2", 86 | "contentful-import": "^9.4.34", 87 | "eslint": "8.22.0", 88 | "eslint-config-next": "^12.3.1", 89 | "eslint-config-prettier": "^8.3.0", 90 | "eslint-import-resolver-typescript": "^2.4.0", 91 | "eslint-plugin-import": "^2.23.4", 92 | "eslint-plugin-jsx-a11y": "^6.4.1", 93 | "eslint-plugin-prettier": "^3.4.0", 94 | "eslint-plugin-react": "^7.22.0", 95 | "eslint-plugin-react-hooks": "^4.2.0", 96 | "husky": "^8.0.0", 97 | "i18next": "^21.9.2", 98 | "lint-staged": "^13.0.3", 99 | "postcss": "^8.4.14", 100 | "prettier": "^2.7.1", 101 | "react-i18next": "^11.18.6", 102 | "typescript": "4.8.4" 103 | }, 104 | "resolutions": { 105 | "**/trim": "^1.0.0", 106 | "**/trim-newlines": "^3.0.1", 107 | "**/glob-parent": "^5.1.2" 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/template-marketing-webapp-nextjs/5d95361a69f0453a7263efffbefac6dbe02b6e3a/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/template-marketing-webapp-nextjs/5d95361a69f0453a7263efffbefac6dbe02b6e3a/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/template-marketing-webapp-nextjs/5d95361a69f0453a7263efffbefac6dbe02b6e3a/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #2b5797 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/template-marketing-webapp-nextjs/5d95361a69f0453a7263efffbefac6dbe02b6e3a/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/template-marketing-webapp-nextjs/5d95361a69f0453a7263efffbefac6dbe02b6e3a/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/template-marketing-webapp-nextjs/5d95361a69f0453a7263efffbefac6dbe02b6e3a/public/favicon.ico -------------------------------------------------------------------------------- /public/locales/de-DE/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "homepage": "Startseite", 4 | "logoImageAltText": "Logo" 5 | }, 6 | "price": { 7 | "free": "Kostenloss" 8 | }, 9 | "navigation": { 10 | "mobileMenuButton": "Menü öffnen" 11 | }, 12 | "time": { 13 | "month": "Monat" 14 | }, 15 | "socials": { 16 | "findUsOn": "Finden Sie uns auf", 17 | "twitter": "Twitter", 18 | "facebook": "Facebook", 19 | "linkedin": "LinkedIn", 20 | "instagram": "Instagram" 21 | }, 22 | "legal": { 23 | "copyright": "© Copyright {{ year }}" 24 | }, 25 | "error": { 26 | "code": "{{code}} error", 27 | "componentNotFound": "Komponente nicht gefunden", 28 | "somethingWentWrong": "50 / 5,000\nTranslation results\nEtwas ist schief gelaufen, überprüfen Sie Ihre URL und versuchen Sie es erneut" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /public/locales/en-US/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "homepage": "Homepage", 4 | "logoImageAltText": "Logo" 5 | }, 6 | "price": { 7 | "free": "Free" 8 | }, 9 | "navigation": { 10 | "mobileMenuButton": "Open menu" 11 | }, 12 | "time": { 13 | "month": "Month" 14 | }, 15 | "socials": { 16 | "findUsOn": "Find us on", 17 | "twitter": "Twitter", 18 | "facebook": "Facebook", 19 | "linkedin": "LinkedIn", 20 | "instagram": "Instagram" 21 | }, 22 | "legal": { 23 | "copyright": "© Copyright {{ year }}" 24 | }, 25 | "error": { 26 | "code": "{{code}} error", 27 | "componentNotFound": "Component not found.", 28 | "somethingWentWrong": "Something went wrong, check your url and try again" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/template-marketing-webapp-nextjs/5d95361a69f0453a7263efffbefac6dbe02b6e3a/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # As soon as you are ready to go public, change the `Disallow: /` to `Allow: /` 2 | User-agent: * 3 | Disallow: / 4 | -------------------------------------------------------------------------------- /public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 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 | -------------------------------------------------------------------------------- /src/components/features/author/author.tsx: -------------------------------------------------------------------------------- 1 | import { Theme } from '@mui/material'; 2 | import { makeStyles } from '@mui/styles'; 3 | 4 | import { Avatar } from '@src/components/features/avatar'; 5 | import { PersonFieldsFragment } from '@src/components/features/ctf-components/ctf-person/__generated/ctf-person.generated'; 6 | 7 | const useStyles = makeStyles((theme: Theme) => ({ 8 | avatar: { 9 | display: 'inline-block', 10 | width: '11.4rem', 11 | }, 12 | name: { 13 | fontSize: '2.5rem', 14 | lineHeight: 1.52, 15 | marginBottom: 0, 16 | marginTop: theme.spacing(3), 17 | }, 18 | })); 19 | 20 | interface AuthorPropsInterface extends PersonFieldsFragment {} 21 | 22 | export const Author = (props: AuthorPropsInterface) => { 23 | const { name, avatar } = props; 24 | 25 | const classes = useStyles(); 26 | 27 | return ( 28 |
29 | {avatar && ( 30 |
31 | 32 |
33 | )} 34 | {name &&

{name}

} 35 |
36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/features/author/index.ts: -------------------------------------------------------------------------------- 1 | export * from './author' 2 | -------------------------------------------------------------------------------- /src/components/features/avatar/avatar.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar as MuiAvatar } from '@mui/material'; 2 | import { makeStyles } from '@mui/styles'; 3 | import React, { useMemo } from 'react'; 4 | 5 | import { AssetFieldsFragment } from '@src/lib/__generated/graphql.types'; 6 | 7 | const useStyles = makeStyles(() => ({ 8 | avatarRoot: { 9 | width: `100%`, 10 | height: 0, 11 | padding: `50%`, 12 | position: `relative`, 13 | }, 14 | avatar: { 15 | width: '100%', 16 | height: '100%', 17 | position: `absolute`, 18 | top: 0, 19 | left: 0, 20 | }, 21 | })); 22 | 23 | interface AvatarPropsInterface { 24 | asset: AssetFieldsFragment; 25 | widthPx?: number; 26 | } 27 | 28 | export const Avatar = (props: AvatarPropsInterface) => { 29 | const { asset, widthPx = 250 } = props; 30 | const url = useMemo(() => `${asset.url}?w=${widthPx}`, [asset.url, widthPx]); 31 | const classes = useStyles(); 32 | return ( 33 |
34 | 35 |
36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/features/avatar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './avatar' 2 | -------------------------------------------------------------------------------- /src/components/features/card-leadership/card-leadership.tsx: -------------------------------------------------------------------------------- 1 | import { useContentfulInspectorMode } from '@contentful/live-preview/react'; 2 | import { Theme, Typography } from '@mui/material'; 3 | import { makeStyles } from '@mui/styles'; 4 | import clsx from 'clsx'; 5 | import React from 'react'; 6 | 7 | import { CtfAsset } from '@src/components/features/ctf-components/ctf-asset/ctf-asset'; 8 | import { PersonFieldsFragment } from '@src/components/features/ctf-components/ctf-person/__generated/ctf-person.generated'; 9 | import { CtfRichtext } from '@src/components/features/ctf-components/ctf-richtext/ctf-richtext'; 10 | import LayoutContext, { defaultLayout } from '@src/layout-context'; 11 | 12 | const useStyles = makeStyles((theme: Theme) => ({ 13 | root: { 14 | alignItems: 'flex-start', 15 | display: 'flex', 16 | flexDirection: 'column', 17 | marginLeft: 'auto', 18 | marginRight: 'auto', 19 | maxWidth: '93.4rem', 20 | [theme.breakpoints.up('md')]: { 21 | flexDirection: 'row', 22 | }, 23 | }, 24 | rootIncreasedSpacing: { 25 | marginTop: theme.spacing(7), 26 | [theme.breakpoints.up('md')]: { 27 | marginTop: theme.spacing(10), 28 | }, 29 | }, 30 | avatar: { 31 | borderRadius: '50%', 32 | flexShrink: 0, 33 | marginBottom: theme.spacing(5), 34 | marginRight: theme.spacing(10), 35 | maxWidth: '9.8rem', 36 | overflow: 'hidden', 37 | [theme.breakpoints.up('md')]: { 38 | marginBottom: 0, 39 | }, 40 | }, 41 | name: { 42 | fontSize: '2.1rem', 43 | fontWeight: 500, 44 | lineHeight: 1.333, 45 | marginBottom: theme.spacing(1), 46 | }, 47 | role: { 48 | fontSize: '1.8rem', 49 | }, 50 | bio: { 51 | color: '#6E6E6E', 52 | marginTop: theme.spacing(5), 53 | '& p': { 54 | fontSize: '1.8rem', 55 | lineHeight: 1.333, 56 | }, 57 | '& .MuiContainer-root:last-child p:last-child': { 58 | marginBottom: 0, 59 | }, 60 | }, 61 | })); 62 | 63 | interface CardLeadershipPropsInterface extends PersonFieldsFragment { 64 | previousComponent: string | null; 65 | } 66 | 67 | export const CardLeadership = (props: CardLeadershipPropsInterface) => { 68 | const { 69 | name, 70 | bio, 71 | avatar, 72 | previousComponent, 73 | sys: { id: entryId }, 74 | } = props; 75 | const nameSplit = name?.split(', '); 76 | 77 | const classes = useStyles(); 78 | const inspectorMode = useContentfulInspectorMode({ entryId }); 79 | 80 | return ( 81 |
87 | {avatar && ( 88 |
89 | 90 |
91 | )} 92 |
93 |
94 | {nameSplit && {nameSplit[0]}} 95 | {nameSplit && nameSplit.length === 2 && ( 96 | {nameSplit[1]} 97 | )} 98 |
99 | {bio && ( 100 | 101 |
102 | 103 |
104 |
105 | )} 106 |
107 |
108 | ); 109 | }; 110 | -------------------------------------------------------------------------------- /src/components/features/card-leadership/index.ts: -------------------------------------------------------------------------------- 1 | export * from './card-leadership' 2 | -------------------------------------------------------------------------------- /src/components/features/card-person/card-person.tsx: -------------------------------------------------------------------------------- 1 | import { Theme } from '@mui/material'; 2 | import { makeStyles } from '@mui/styles'; 3 | 4 | import { Avatar } from '@src/components/features/avatar'; 5 | import { PersonFieldsFragment } from '@src/components/features/ctf-components/ctf-person/__generated/ctf-person.generated'; 6 | import { CtfRichtext } from '@src/components/features/ctf-components/ctf-richtext/ctf-richtext'; 7 | import LayoutContext, { defaultLayout } from '@src/layout-context'; 8 | 9 | const useStyles = makeStyles((theme: Theme) => ({ 10 | root: { 11 | display: 'flex', 12 | }, 13 | avatar: { 14 | flexShrink: 0, 15 | marginRight: theme.spacing(13), 16 | width: '10rem', 17 | }, 18 | name: { 19 | fontSize: '1.8rem', 20 | lineHeight: 1.333, 21 | marginBottom: theme.spacing(2), 22 | marginTop: 0, 23 | }, 24 | bio: { 25 | color: '#797979', 26 | '& p': { 27 | fontSize: '1.8rem', 28 | lineHeight: 1.333, 29 | }, 30 | }, 31 | })); 32 | 33 | export const CardPerson = ({ name, bio, avatar }: PersonFieldsFragment) => { 34 | const classes = useStyles(); 35 | 36 | return ( 37 |
38 | {avatar && ( 39 |
40 | 41 |
42 | )} 43 |
44 | {name &&

{name}

} 45 | {bio && ( 46 | 47 |
48 | 49 |
50 |
51 | )} 52 |
53 |
54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /src/components/features/card-person/index.ts: -------------------------------------------------------------------------------- 1 | export * from './card-person' 2 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-asset/__generated/ctf-asset.generated.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../../../../lib/__generated/graphql.types'; 2 | 3 | export type AssetFieldsFragment = { __typename: 'Asset', contentType?: string | null, title?: string | null, description?: string | null, width?: number | null, height?: number | null, url?: string | null, sys: { __typename?: 'Sys', id: string } }; 4 | 5 | export const AssetFieldsFragmentDoc = ` 6 | fragment AssetFields on Asset { 7 | __typename 8 | sys { 9 | id 10 | } 11 | contentType 12 | title 13 | description 14 | width 15 | height 16 | url 17 | } 18 | `; -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-asset/ctf-asset.graphql: -------------------------------------------------------------------------------- 1 | fragment AssetFields on Asset { 2 | __typename 3 | sys { 4 | id 5 | } 6 | contentType 7 | title 8 | description 9 | width 10 | height 11 | url 12 | } 13 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-asset/ctf-asset.tsx: -------------------------------------------------------------------------------- 1 | import { ImageProps } from 'next/image'; 2 | 3 | import { AssetFieldsFragment } from './__generated/ctf-asset.generated'; 4 | 5 | import { CtfImage } from '@src/components/features/ctf-components/ctf-image/ctf-image'; 6 | import { CtfVideo } from '@src/components/features/ctf-components/ctf-video/ctf-video'; 7 | import { useLayoutContext } from '@src/layout-context'; 8 | 9 | interface CtfAssetPropsInterface 10 | extends AssetFieldsFragment, 11 | Pick { 12 | className?: string; 13 | showDescription?: boolean; 14 | onClick?: () => any; 15 | } 16 | 17 | export const CtfAsset = (props: CtfAssetPropsInterface) => { 18 | const { contentType, url, showDescription = true, title, width, height } = props; 19 | const layout = useLayoutContext(); 20 | 21 | if (!contentType || !url) { 22 | return null; 23 | } 24 | 25 | if (contentType.startsWith('image')) { 26 | return ( 27 | 38 | ); 39 | } 40 | 41 | if (contentType.startsWith('video')) { 42 | return ; 43 | } 44 | 45 | return null; 46 | }; 47 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-business-info/business-info.graphql: -------------------------------------------------------------------------------- 1 | fragment BusinessInfoFields on TopicBusinessInfo { 2 | __typename 3 | sys { 4 | id 5 | } 6 | name 7 | shortDescription 8 | featuredImage { 9 | ...AssetFields 10 | } 11 | body { 12 | json 13 | links { 14 | entries { 15 | block { 16 | ...ComponentReferenceFields 17 | } 18 | } 19 | assets { 20 | block { 21 | ...AssetFields 22 | } 23 | } 24 | } 25 | } 26 | } 27 | 28 | query CtfBusinessInfo($id: String!, $locale: String, $preview: Boolean) { 29 | topicBusinessInfo(id: $id, preview: $preview, locale: $locale) { 30 | ...BusinessInfoFields 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-business-info/ctf-business-info-gql.tsx: -------------------------------------------------------------------------------- 1 | import { useContentfulLiveUpdates } from '@contentful/live-preview/react'; 2 | import { Container } from '@mui/material'; 3 | import Head from 'next/head'; 4 | 5 | import { useCtfBusinessInfoQuery } from './__generated/business-info.generated'; 6 | import CtfBusinessInfo from './ctf-business-info'; 7 | 8 | import { EntryNotFound } from '@src/components/features/errors/entry-not-found'; 9 | import { useContentfulContext } from '@src/contentful-context'; 10 | 11 | interface CtfBusinessInfoGqlPropsInterface { 12 | id: string; 13 | preview?: boolean; 14 | } 15 | 16 | export const CtfBusinessInfoGql = ({ preview, id }: CtfBusinessInfoGqlPropsInterface) => { 17 | const { locale } = useContentfulContext(); 18 | 19 | const { data, isLoading } = useCtfBusinessInfoQuery({ 20 | locale, 21 | id, 22 | preview, 23 | }); 24 | 25 | const topicBusinessInfo = useContentfulLiveUpdates(data?.topicBusinessInfo); 26 | 27 | if (!data || isLoading) { 28 | return null; 29 | } 30 | 31 | if (!topicBusinessInfo) { 32 | return ( 33 | 34 | 35 | 36 | ); 37 | } 38 | 39 | return ( 40 | <> 41 | {topicBusinessInfo.featuredImage && ( 42 | 43 | 48 | 49 | )} 50 | 51 | 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-business-info/ctf-business-info.tsx: -------------------------------------------------------------------------------- 1 | import { useContentfulInspectorMode } from '@contentful/live-preview/react'; 2 | import { Theme, Container } from '@mui/material'; 3 | import Typography from '@mui/material/Typography'; 4 | import { makeStyles } from '@mui/styles'; 5 | import clsx from 'clsx'; 6 | import React, { useMemo } from 'react'; 7 | 8 | import { BusinessInfoFieldsFragment } from './__generated/business-info.generated'; 9 | 10 | import { CtfRichtext } from '@src/components/features/ctf-components/ctf-richtext/ctf-richtext'; 11 | 12 | const useStyles = makeStyles((theme: Theme) => ({ 13 | root: { 14 | paddingBottom: theme.spacing(18), 15 | paddingTop: (props: BusinessInfoFieldsFragment) => 16 | props.name || props.shortDescription ? 0 : theme.spacing(18), 17 | '& .MuiContainer-root + .ComponentInfoBlock': { 18 | marginTop: theme.spacing(18), 19 | }, 20 | '& .ComponentInfoBlock + .MuiContainer-root': { 21 | marginTop: theme.spacing(18), 22 | }, 23 | }, 24 | container: { 25 | marginRight: 'auto', 26 | marginLeft: 'auto', 27 | maxWidth: '126.2rem', 28 | }, 29 | containerNarrow: { 30 | marginRight: 'auto', 31 | marginLeft: 'auto', 32 | maxWidth: '77rem', 33 | }, 34 | hero: { 35 | marginBottom: theme.spacing(18), 36 | position: 'relative', 37 | }, 38 | heroBg: { 39 | backgroundColor: '#000', 40 | backgroundPosition: 'center center', 41 | backgroundSize: 'cover', 42 | bottom: 0, 43 | left: 0, 44 | position: 'absolute', 45 | right: 0, 46 | top: 0, 47 | '&::before': { 48 | backgroundColor: 'rgba(0, 0, 0, 0.5)', 49 | bottom: 0, 50 | content: '""', 51 | display: 'block', 52 | left: 0, 53 | position: 'absolute', 54 | right: 0, 55 | top: 0, 56 | zIndex: 1, 57 | }, 58 | }, 59 | heroInner: { 60 | alignItems: 'center', 61 | color: '#fff', 62 | display: 'flex', 63 | flexDirection: 'column', 64 | justifyContent: 'center', 65 | maxWidth: '55rem', 66 | paddingBottom: theme.spacing(8), 67 | paddingTop: theme.spacing(8), 68 | position: 'relative', 69 | textAlign: 'center', 70 | zIndex: 1, 71 | [theme.breakpoints.up('md')]: { 72 | paddingBottom: theme.spacing(16), 73 | paddingTop: theme.spacing(16), 74 | }, 75 | '@media (min-height: 600px)': { 76 | minHeight: '59rem', 77 | }, 78 | }, 79 | title: { 80 | [theme.breakpoints.up('md')]: { 81 | fontSize: '4.5rem', 82 | }, 83 | }, 84 | subtitle: { 85 | fontSize: '2.5rem', 86 | marginTop: theme.spacing(3), 87 | }, 88 | })); 89 | 90 | const CtfBusinessInfo = (props: BusinessInfoFieldsFragment) => { 91 | const { 92 | body, 93 | name, 94 | shortDescription, 95 | featuredImage, 96 | sys: { id }, 97 | } = props; 98 | const backgroundImage = useMemo( 99 | () => (featuredImage ? `${featuredImage.url}?w=1920` : undefined), 100 | [featuredImage], 101 | ); 102 | 103 | const classes = useStyles(props); 104 | const inspectorMode = useContentfulInspectorMode({ entryId: id }); 105 | 106 | return ( 107 |
108 | {(name || shortDescription) && ( 109 |
110 |
117 | 118 |
119 | {name && ( 120 | 125 | {name} 126 | 127 | )} 128 | {shortDescription && ( 129 | 135 | {shortDescription} 136 | 137 | )} 138 |
139 |
140 |
141 | )} 142 | {body && ( 143 |
144 | 149 |
150 | )} 151 |
152 | ); 153 | }; 154 | 155 | export default CtfBusinessInfo; 156 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-cta/__generated/ctf-cta.generated.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../../../../lib/__generated/graphql.types'; 2 | 3 | import { PageLinkFieldsFragment } from '../../../page-link/__generated/page-link.generated'; 4 | import { PageLinkFieldsFragmentDoc } from '../../../page-link/__generated/page-link.generated'; 5 | import { useQuery, UseQueryOptions } from '@tanstack/react-query'; 6 | import { customFetcher } from '@src/lib/fetchConfig'; 7 | export type CtaFieldsFragment = { __typename: 'ComponentCta', headline?: string | null, ctaText?: string | null, urlParameters?: string | null, colorPalette?: string | null, sys: { __typename?: 'Sys', id: string }, subline?: { __typename?: 'ComponentCtaSubline', json: any } | null, targetPage?: ( 8 | { __typename?: 'Page' } 9 | & PageLinkFieldsFragment 10 | ) | null }; 11 | 12 | export type CtfCtaQueryVariables = Types.Exact<{ 13 | id: Types.Scalars['String']; 14 | locale?: Types.InputMaybe; 15 | preview?: Types.InputMaybe; 16 | }>; 17 | 18 | 19 | export type CtfCtaQuery = { __typename?: 'Query', componentCta?: ( 20 | { __typename?: 'ComponentCta' } 21 | & CtaFieldsFragment 22 | ) | null }; 23 | 24 | export const CtaFieldsFragmentDoc = ` 25 | fragment CtaFields on ComponentCta { 26 | __typename 27 | sys { 28 | id 29 | } 30 | headline 31 | subline { 32 | json 33 | } 34 | ctaText 35 | targetPage { 36 | ...PageLinkFields 37 | } 38 | urlParameters 39 | colorPalette 40 | } 41 | `; 42 | export const CtfCtaDocument = ` 43 | query CtfCta($id: String!, $locale: String, $preview: Boolean) { 44 | componentCta(id: $id, locale: $locale, preview: $preview) { 45 | ...CtaFields 46 | } 47 | } 48 | ${CtaFieldsFragmentDoc} 49 | ${PageLinkFieldsFragmentDoc}`; 50 | export const useCtfCtaQuery = < 51 | TData = CtfCtaQuery, 52 | TError = unknown 53 | >( 54 | variables: CtfCtaQueryVariables, 55 | options?: UseQueryOptions 56 | ) => 57 | useQuery( 58 | ['CtfCta', variables], 59 | customFetcher(CtfCtaDocument, variables), 60 | options 61 | ); 62 | 63 | useCtfCtaQuery.getKey = (variables: CtfCtaQueryVariables) => ['CtfCta', variables]; 64 | ; 65 | 66 | useCtfCtaQuery.fetcher = (variables: CtfCtaQueryVariables, options?: RequestInit['headers']) => customFetcher(CtfCtaDocument, variables, options); -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-cta/ctf-cta-gql.tsx: -------------------------------------------------------------------------------- 1 | import { useContentfulLiveUpdates } from '@contentful/live-preview/react'; 2 | import React from 'react'; 3 | 4 | import { useCtfCtaQuery } from './__generated/ctf-cta.generated'; 5 | import { CtfCta } from './ctf-cta'; 6 | 7 | interface CtfCtaGqlPropsInterface { 8 | id: string; 9 | locale: string; 10 | preview: boolean; 11 | } 12 | 13 | export const CtfCtaGql = ({ id, locale, preview }: CtfCtaGqlPropsInterface) => { 14 | const { data, isLoading } = useCtfCtaQuery({ 15 | id, 16 | locale, 17 | preview, 18 | }); 19 | 20 | const componentCta = useContentfulLiveUpdates(data?.componentCta); 21 | 22 | if (isLoading || !componentCta) { 23 | return null; 24 | } 25 | 26 | return ; 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-cta/ctf-cta.graphql: -------------------------------------------------------------------------------- 1 | fragment CtaFields on ComponentCta { 2 | __typename 3 | sys { 4 | id 5 | } 6 | headline 7 | subline { 8 | json 9 | } 10 | ctaText 11 | targetPage { 12 | ...PageLinkFields 13 | } 14 | urlParameters 15 | colorPalette 16 | } 17 | 18 | query CtfCta($id: String!, $locale: String, $preview: Boolean) { 19 | componentCta(id: $id, locale: $locale, preview: $preview) { 20 | ...CtaFields 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-cta/ctf-cta.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Theme, Typography } from '@mui/material'; 2 | import { makeStyles } from '@mui/styles'; 3 | 4 | import { CtaFieldsFragment } from './__generated/ctf-cta.generated'; 5 | 6 | import { CtfRichtext } from '@src/components/features/ctf-components/ctf-richtext/ctf-richtext'; 7 | import { PageLink } from '@src/components/features/page-link'; 8 | import LayoutContext, { defaultLayout } from '@src/layout-context'; 9 | import { getColorConfigFromPalette } from '@src/theme'; 10 | import { optimizeLineBreak } from '@src/utils'; 11 | 12 | const useStyles = makeStyles((theme: Theme) => ({ 13 | root: { 14 | textAlign: 'center', 15 | }, 16 | innerContainer: { 17 | marginLeft: 'auto', 18 | marginRight: 'auto', 19 | maxWidth: '93.4rem', 20 | padding: theme.spacing(19, 0, 19), 21 | }, 22 | headline: { 23 | fontWeight: 'bold', 24 | }, 25 | subline: { 26 | fontWeight: 400, 27 | lineHeight: 1.52, 28 | marginTop: theme.spacing(8), 29 | }, 30 | ctaContainer: { 31 | marginTop: theme.spacing(8), 32 | }, 33 | })); 34 | 35 | export const CtfCta = (props: CtaFieldsFragment) => { 36 | const { headline, subline, targetPage, ctaText, colorPalette, urlParameters } = props; 37 | const colorConfig = getColorConfigFromPalette(colorPalette || ''); 38 | const classes = useStyles(); 39 | 40 | return ( 41 | 47 |
48 | {headline && ( 49 | 54 | {optimizeLineBreak(headline)} 55 | 56 | )} 57 | {subline && ( 58 | 59 |
60 | 61 |
62 |
63 | )} 64 | {targetPage && targetPage.slug && ( 65 |
66 | 72 | {ctaText} 73 | 74 |
75 | )} 76 |
77 |
78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-duplex/__generated/ctf-duplex.generated.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../../../../lib/__generated/graphql.types'; 2 | 3 | import { PageLinkFieldsFragment } from '../../../page-link/__generated/page-link.generated'; 4 | import { AssetFieldsFragment } from '../../ctf-asset/__generated/ctf-asset.generated'; 5 | import { PageLinkFieldsFragmentDoc } from '../../../page-link/__generated/page-link.generated'; 6 | import { AssetFieldsFragmentDoc } from '../../ctf-asset/__generated/ctf-asset.generated'; 7 | import { useQuery, UseQueryOptions } from '@tanstack/react-query'; 8 | import { customFetcher } from '@src/lib/fetchConfig'; 9 | export type DuplexFieldsFragment = { __typename: 'ComponentDuplex', containerLayout?: boolean | null, headline?: string | null, ctaText?: string | null, imageStyle?: boolean | null, colorPalette?: string | null, sys: { __typename?: 'Sys', id: string }, bodyText?: { __typename?: 'ComponentDuplexBodyText', json: any } | null, targetPage?: ( 10 | { __typename?: 'Page' } 11 | & PageLinkFieldsFragment 12 | ) | null, image?: ( 13 | { __typename?: 'Asset' } 14 | & AssetFieldsFragment 15 | ) | null }; 16 | 17 | export type CtfDuplexQueryVariables = Types.Exact<{ 18 | id: Types.Scalars['String']; 19 | locale?: Types.InputMaybe; 20 | preview?: Types.InputMaybe; 21 | }>; 22 | 23 | 24 | export type CtfDuplexQuery = { __typename?: 'Query', componentDuplex?: ( 25 | { __typename?: 'ComponentDuplex' } 26 | & DuplexFieldsFragment 27 | ) | null }; 28 | 29 | export const DuplexFieldsFragmentDoc = ` 30 | fragment DuplexFields on ComponentDuplex { 31 | __typename 32 | sys { 33 | id 34 | } 35 | containerLayout 36 | headline 37 | bodyText { 38 | json 39 | } 40 | ctaText 41 | targetPage { 42 | ...PageLinkFields 43 | } 44 | image { 45 | ...AssetFields 46 | } 47 | imageStyle 48 | colorPalette 49 | } 50 | `; 51 | export const CtfDuplexDocument = ` 52 | query CtfDuplex($id: String!, $locale: String, $preview: Boolean) { 53 | componentDuplex(id: $id, locale: $locale, preview: $preview) { 54 | ...DuplexFields 55 | } 56 | } 57 | ${DuplexFieldsFragmentDoc} 58 | ${PageLinkFieldsFragmentDoc} 59 | ${AssetFieldsFragmentDoc}`; 60 | export const useCtfDuplexQuery = < 61 | TData = CtfDuplexQuery, 62 | TError = unknown 63 | >( 64 | variables: CtfDuplexQueryVariables, 65 | options?: UseQueryOptions 66 | ) => 67 | useQuery( 68 | ['CtfDuplex', variables], 69 | customFetcher(CtfDuplexDocument, variables), 70 | options 71 | ); 72 | 73 | useCtfDuplexQuery.getKey = (variables: CtfDuplexQueryVariables) => ['CtfDuplex', variables]; 74 | ; 75 | 76 | useCtfDuplexQuery.fetcher = (variables: CtfDuplexQueryVariables, options?: RequestInit['headers']) => customFetcher(CtfDuplexDocument, variables, options); -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-duplex/ctf-duplex-gql.tsx: -------------------------------------------------------------------------------- 1 | import { useContentfulLiveUpdates } from '@contentful/live-preview/react'; 2 | import React from 'react'; 3 | 4 | import { useCtfDuplexQuery } from './__generated/ctf-duplex.generated'; 5 | import { CtfDuplex } from './ctf-duplex'; 6 | 7 | interface CtfDuplexGqlPropsInterface { 8 | id: string; 9 | locale: string; 10 | preview: boolean; 11 | } 12 | 13 | export const CtfDuplexGql = ({ id, locale, preview }: CtfDuplexGqlPropsInterface) => { 14 | const { data, isLoading } = useCtfDuplexQuery({ 15 | id, 16 | locale, 17 | preview, 18 | }); 19 | 20 | const componentDuplex = useContentfulLiveUpdates(data?.componentDuplex); 21 | 22 | if (isLoading || !componentDuplex) { 23 | return null; 24 | } 25 | 26 | return ; 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-duplex/ctf-duplex.graphql: -------------------------------------------------------------------------------- 1 | fragment DuplexFields on ComponentDuplex { 2 | __typename 3 | sys { 4 | id 5 | } 6 | containerLayout 7 | headline 8 | bodyText { 9 | json 10 | } 11 | ctaText 12 | targetPage { 13 | ...PageLinkFields 14 | } 15 | image { 16 | ...AssetFields 17 | } 18 | imageStyle 19 | colorPalette 20 | } 21 | 22 | query CtfDuplex($id: String!, $locale: String, $preview: Boolean) { 23 | componentDuplex(id: $id, locale: $locale, preview: $preview) { 24 | ...DuplexFields 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-footer/__generated/ctf-footer.generated.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../../../../lib/__generated/graphql.types'; 2 | 3 | import { MenuGroupFieldsFragment } from '../../../../../lib/shared-fragments/__generated/ctf-menuGroup.generated'; 4 | import { PageLinkFieldsFragment } from '../../../page-link/__generated/page-link.generated'; 5 | import { MenuGroupFieldsFragmentDoc } from '../../../../../lib/shared-fragments/__generated/ctf-menuGroup.generated'; 6 | import { PageLinkFieldsFragmentDoc } from '../../../page-link/__generated/page-link.generated'; 7 | import { useQuery, UseQueryOptions } from '@tanstack/react-query'; 8 | import { customFetcher } from '@src/lib/fetchConfig'; 9 | export type FooterFieldsFragment = { __typename?: 'FooterMenuCollection', items: Array<{ __typename: 'FooterMenu', twitterLink?: string | null, facebookLink?: string | null, linkedinLink?: string | null, instagramLink?: string | null, sys: { __typename?: 'Sys', id: string }, menuItemsCollection?: { __typename?: 'FooterMenuMenuItemsCollection', items: Array<{ __typename: 'MenuGroup', groupName?: string | null, sys: { __typename?: 'Sys', id: string }, featuredPagesCollection?: ( 10 | { __typename?: 'MenuGroupFeaturedPagesCollection' } 11 | & MenuGroupFieldsFragment 12 | ) | null } | null> } | null, legalLinks?: { __typename?: 'MenuGroup', featuredPagesCollection?: ( 13 | { __typename?: 'MenuGroupFeaturedPagesCollection' } 14 | & MenuGroupFieldsFragment 15 | ) | null } | null } | null> }; 16 | 17 | export type CtfFooterQueryVariables = Types.Exact<{ 18 | locale?: Types.InputMaybe; 19 | preview?: Types.InputMaybe; 20 | }>; 21 | 22 | 23 | export type CtfFooterQuery = { __typename?: 'Query', footerMenuCollection?: ( 24 | { __typename?: 'FooterMenuCollection' } 25 | & FooterFieldsFragment 26 | ) | null }; 27 | 28 | export const FooterFieldsFragmentDoc = ` 29 | fragment FooterFields on FooterMenuCollection { 30 | items { 31 | __typename 32 | sys { 33 | id 34 | } 35 | menuItemsCollection { 36 | items { 37 | __typename 38 | groupName 39 | sys { 40 | id 41 | } 42 | featuredPagesCollection { 43 | ...MenuGroupFields 44 | } 45 | } 46 | } 47 | legalLinks { 48 | featuredPagesCollection { 49 | ...MenuGroupFields 50 | } 51 | } 52 | twitterLink 53 | facebookLink 54 | linkedinLink 55 | instagramLink 56 | } 57 | } 58 | `; 59 | export const CtfFooterDocument = ` 60 | query CtfFooter($locale: String, $preview: Boolean) { 61 | footerMenuCollection(locale: $locale, preview: $preview, limit: 1) { 62 | ...FooterFields 63 | } 64 | } 65 | ${FooterFieldsFragmentDoc} 66 | ${MenuGroupFieldsFragmentDoc} 67 | ${PageLinkFieldsFragmentDoc}`; 68 | export const useCtfFooterQuery = < 69 | TData = CtfFooterQuery, 70 | TError = unknown 71 | >( 72 | variables?: CtfFooterQueryVariables, 73 | options?: UseQueryOptions 74 | ) => 75 | useQuery( 76 | variables === undefined ? ['CtfFooter'] : ['CtfFooter', variables], 77 | customFetcher(CtfFooterDocument, variables), 78 | options 79 | ); 80 | 81 | useCtfFooterQuery.getKey = (variables?: CtfFooterQueryVariables) => variables === undefined ? ['CtfFooter'] : ['CtfFooter', variables]; 82 | ; 83 | 84 | useCtfFooterQuery.fetcher = (variables?: CtfFooterQueryVariables, options?: RequestInit['headers']) => customFetcher(CtfFooterDocument, variables, options); -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-footer/ctf-footer-gql.tsx: -------------------------------------------------------------------------------- 1 | import { useContentfulLiveUpdates } from '@contentful/live-preview/react'; 2 | import React from 'react'; 3 | 4 | import { useCtfFooterQuery } from './__generated/ctf-footer.generated'; 5 | import { CtfFooter } from './ctf-footer'; 6 | 7 | import { useContentfulContext } from '@src/contentful-context'; 8 | 9 | export const CtfFooterGql = () => { 10 | const { locale, previewActive } = useContentfulContext(); 11 | 12 | const { data, isLoading } = useCtfFooterQuery({ 13 | locale, 14 | preview: previewActive, 15 | }); 16 | 17 | const footerMenuCollection = useContentfulLiveUpdates(data?.footerMenuCollection); 18 | 19 | if (!footerMenuCollection || isLoading) return null; 20 | 21 | return ; 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-footer/ctf-footer.graphql: -------------------------------------------------------------------------------- 1 | fragment FooterFields on FooterMenuCollection { 2 | items { 3 | __typename 4 | sys { 5 | id 6 | } 7 | menuItemsCollection { 8 | items { 9 | __typename 10 | groupName 11 | sys { 12 | id 13 | } 14 | featuredPagesCollection { 15 | ...MenuGroupFields 16 | } 17 | } 18 | } 19 | legalLinks { 20 | featuredPagesCollection { 21 | ...MenuGroupFields 22 | } 23 | } 24 | twitterLink 25 | facebookLink 26 | linkedinLink 27 | instagramLink 28 | } 29 | } 30 | 31 | query CtfFooter($locale: String, $preview: Boolean) { 32 | footerMenuCollection(locale: $locale, preview: $preview, limit: 1) { 33 | ...FooterFields 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-hero-banner/__generated/ctf-hero-banner.generated.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../../../../lib/__generated/graphql.types'; 2 | 3 | import { PageLinkFieldsFragment } from '../../../page-link/__generated/page-link.generated'; 4 | import { AssetFieldsFragment } from '../../ctf-asset/__generated/ctf-asset.generated'; 5 | import { PageLinkFieldsFragmentDoc } from '../../../page-link/__generated/page-link.generated'; 6 | import { AssetFieldsFragmentDoc } from '../../ctf-asset/__generated/ctf-asset.generated'; 7 | import { useQuery, UseQueryOptions } from '@tanstack/react-query'; 8 | import { customFetcher } from '@src/lib/fetchConfig'; 9 | export type HeroBannerFieldsFragment = { __typename: 'ComponentHeroBanner', headline?: string | null, ctaText?: string | null, imageStyle?: boolean | null, heroSize?: boolean | null, colorPalette?: string | null, sys: { __typename?: 'Sys', id: string }, bodyText?: { __typename?: 'ComponentHeroBannerBodyText', json: any } | null, targetPage?: ( 10 | { __typename?: 'Page' } 11 | & PageLinkFieldsFragment 12 | ) | null, image?: ( 13 | { __typename?: 'Asset' } 14 | & AssetFieldsFragment 15 | ) | null }; 16 | 17 | export type CtfHeroBannerQueryVariables = Types.Exact<{ 18 | id: Types.Scalars['String']; 19 | locale?: Types.InputMaybe; 20 | preview?: Types.InputMaybe; 21 | }>; 22 | 23 | 24 | export type CtfHeroBannerQuery = { __typename?: 'Query', componentHeroBanner?: ( 25 | { __typename?: 'ComponentHeroBanner' } 26 | & HeroBannerFieldsFragment 27 | ) | null }; 28 | 29 | export const HeroBannerFieldsFragmentDoc = ` 30 | fragment HeroBannerFields on ComponentHeroBanner { 31 | __typename 32 | sys { 33 | id 34 | } 35 | headline 36 | bodyText { 37 | json 38 | } 39 | ctaText 40 | targetPage { 41 | ...PageLinkFields 42 | } 43 | image { 44 | ...AssetFields 45 | } 46 | imageStyle 47 | heroSize 48 | colorPalette 49 | } 50 | `; 51 | export const CtfHeroBannerDocument = ` 52 | query CtfHeroBanner($id: String!, $locale: String, $preview: Boolean) { 53 | componentHeroBanner(id: $id, locale: $locale, preview: $preview) { 54 | ...HeroBannerFields 55 | } 56 | } 57 | ${HeroBannerFieldsFragmentDoc} 58 | ${PageLinkFieldsFragmentDoc} 59 | ${AssetFieldsFragmentDoc}`; 60 | export const useCtfHeroBannerQuery = < 61 | TData = CtfHeroBannerQuery, 62 | TError = unknown 63 | >( 64 | variables: CtfHeroBannerQueryVariables, 65 | options?: UseQueryOptions 66 | ) => 67 | useQuery( 68 | ['CtfHeroBanner', variables], 69 | customFetcher(CtfHeroBannerDocument, variables), 70 | options 71 | ); 72 | 73 | useCtfHeroBannerQuery.getKey = (variables: CtfHeroBannerQueryVariables) => ['CtfHeroBanner', variables]; 74 | ; 75 | 76 | useCtfHeroBannerQuery.fetcher = (variables: CtfHeroBannerQueryVariables, options?: RequestInit['headers']) => customFetcher(CtfHeroBannerDocument, variables, options); -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-hero-banner/ctf-hero-banner-gql.tsx: -------------------------------------------------------------------------------- 1 | import { useContentfulLiveUpdates } from '@contentful/live-preview/react'; 2 | import React from 'react'; 3 | 4 | import { useCtfHeroBannerQuery } from './__generated/ctf-hero-banner.generated'; 5 | import { CtfHeroBanner } from './ctf-hero-banner'; 6 | 7 | interface CtfHeroGqlPropsInterface { 8 | id: string; 9 | locale: string; 10 | preview: boolean; 11 | } 12 | 13 | export const CtfHeroGql = (props: CtfHeroGqlPropsInterface) => { 14 | const { id, locale, preview } = props; 15 | const { data, isLoading } = useCtfHeroBannerQuery({ 16 | id, 17 | locale, 18 | preview, 19 | }); 20 | 21 | const componentHeroBanner = useContentfulLiveUpdates(data?.componentHeroBanner); 22 | 23 | if (!componentHeroBanner || isLoading) return null; 24 | 25 | return ; 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-hero-banner/ctf-hero-banner.graphql: -------------------------------------------------------------------------------- 1 | fragment HeroBannerFields on ComponentHeroBanner { 2 | __typename 3 | sys { 4 | id 5 | } 6 | # Tutorial: uncomment the line below to add the Greeting field to the query 7 | # greeting 8 | headline 9 | bodyText { 10 | json 11 | } 12 | ctaText 13 | targetPage { 14 | ...PageLinkFields 15 | } 16 | image { 17 | ...AssetFields 18 | } 19 | imageStyle 20 | heroSize 21 | colorPalette 22 | } 23 | 24 | query CtfHeroBanner($id: String!, $locale: String, $preview: Boolean) { 25 | componentHeroBanner(id: $id, locale: $locale, preview: $preview) { 26 | ...HeroBannerFields 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-image/ctf-image.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@mui/material'; 2 | import Image, { ImageProps } from 'next/image'; 3 | import React, { useState } from 'react'; 4 | 5 | interface CtfImagePropsInterface extends ImageProps { 6 | description?: string | null; 7 | showDescription?: boolean; 8 | } 9 | 10 | export const CtfImage = ({ 11 | src, 12 | description, 13 | showDescription = true, 14 | width, 15 | height, 16 | layout, 17 | ...rest 18 | }: CtfImagePropsInterface) => { 19 | const [loaded, setLoaded] = useState(false); 20 | 21 | if (!src) return null; 22 | 23 | const blurUrl = new URL(String(src)); 24 | blurUrl.searchParams.set('w', '100'); 25 | 26 | return ( 27 | 36 | { 38 | setLoaded(true); 39 | }} 40 | src={src} 41 | width={width} 42 | height={height} 43 | layout={layout} 44 | placeholder="blur" 45 | blurDataURL={blurUrl.toString()} 46 | {...rest} 47 | /> 48 | {showDescription && description &&
{description}
} 49 |
50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-info-block/__generated/ctf-info-block.generated.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../../../../lib/__generated/graphql.types'; 2 | 3 | import { AssetFieldsFragment } from '../../ctf-asset/__generated/ctf-asset.generated'; 4 | import { AssetFieldsFragmentDoc } from '../../ctf-asset/__generated/ctf-asset.generated'; 5 | import { useQuery, UseQueryOptions } from '@tanstack/react-query'; 6 | import { customFetcher } from '@src/lib/fetchConfig'; 7 | export type InfoBlockFieldsFragment = { __typename: 'ComponentInfoBlock', headline?: string | null, subline?: string | null, colorPalette?: string | null, sys: { __typename?: 'Sys', id: string }, block1Image?: ( 8 | { __typename?: 'Asset' } 9 | & AssetFieldsFragment 10 | ) | null, block1Body?: { __typename?: 'ComponentInfoBlockBlock1Body', json: any } | null, block2Image?: ( 11 | { __typename?: 'Asset' } 12 | & AssetFieldsFragment 13 | ) | null, block2Body?: { __typename?: 'ComponentInfoBlockBlock2Body', json: any } | null, block3Image?: ( 14 | { __typename?: 'Asset' } 15 | & AssetFieldsFragment 16 | ) | null, block3Body?: { __typename?: 'ComponentInfoBlockBlock3Body', json: any } | null }; 17 | 18 | export type CtfInfoBlockQueryVariables = Types.Exact<{ 19 | id: Types.Scalars['String']; 20 | locale?: Types.InputMaybe; 21 | preview?: Types.InputMaybe; 22 | }>; 23 | 24 | 25 | export type CtfInfoBlockQuery = { __typename?: 'Query', componentInfoBlock?: ( 26 | { __typename?: 'ComponentInfoBlock' } 27 | & InfoBlockFieldsFragment 28 | ) | null }; 29 | 30 | export const InfoBlockFieldsFragmentDoc = ` 31 | fragment InfoBlockFields on ComponentInfoBlock { 32 | __typename 33 | sys { 34 | id 35 | } 36 | headline 37 | subline 38 | block1Image { 39 | ...AssetFields 40 | } 41 | block1Body { 42 | json 43 | } 44 | block2Image { 45 | ...AssetFields 46 | } 47 | block2Body { 48 | json 49 | } 50 | block3Image { 51 | ...AssetFields 52 | } 53 | block3Body { 54 | json 55 | } 56 | colorPalette 57 | } 58 | `; 59 | export const CtfInfoBlockDocument = ` 60 | query CtfInfoBlock($id: String!, $locale: String, $preview: Boolean) { 61 | componentInfoBlock(id: $id, locale: $locale, preview: $preview) { 62 | ...InfoBlockFields 63 | } 64 | } 65 | ${InfoBlockFieldsFragmentDoc} 66 | ${AssetFieldsFragmentDoc}`; 67 | export const useCtfInfoBlockQuery = < 68 | TData = CtfInfoBlockQuery, 69 | TError = unknown 70 | >( 71 | variables: CtfInfoBlockQueryVariables, 72 | options?: UseQueryOptions 73 | ) => 74 | useQuery( 75 | ['CtfInfoBlock', variables], 76 | customFetcher(CtfInfoBlockDocument, variables), 77 | options 78 | ); 79 | 80 | useCtfInfoBlockQuery.getKey = (variables: CtfInfoBlockQueryVariables) => ['CtfInfoBlock', variables]; 81 | ; 82 | 83 | useCtfInfoBlockQuery.fetcher = (variables: CtfInfoBlockQueryVariables, options?: RequestInit['headers']) => customFetcher(CtfInfoBlockDocument, variables, options); -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-info-block/ctf-info-block-gql.tsx: -------------------------------------------------------------------------------- 1 | import { useContentfulLiveUpdates } from '@contentful/live-preview/react'; 2 | 3 | import { useCtfInfoBlockQuery } from './__generated/ctf-info-block.generated'; 4 | import { CtfInfoBlock } from './ctf-info-block'; 5 | 6 | interface CtfInfoBlockGqlPropsInterface { 7 | id: string; 8 | locale: string; 9 | preview: boolean; 10 | previousComponent: string | null; 11 | } 12 | 13 | export const CtfInfoBlockGql = ({ 14 | id, 15 | locale, 16 | preview, 17 | previousComponent, 18 | }: CtfInfoBlockGqlPropsInterface) => { 19 | const { isLoading, data } = useCtfInfoBlockQuery({ 20 | id, 21 | locale, 22 | preview, 23 | }); 24 | 25 | const componentInfoBlock = useContentfulLiveUpdates(data?.componentInfoBlock); 26 | 27 | if (isLoading || !componentInfoBlock) { 28 | return null; 29 | } 30 | 31 | return ; 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-info-block/ctf-info-block.graphql: -------------------------------------------------------------------------------- 1 | fragment InfoBlockFields on ComponentInfoBlock { 2 | __typename 3 | sys { 4 | id 5 | } 6 | headline 7 | subline 8 | block1Image { 9 | ...AssetFields 10 | } 11 | block1Body { 12 | json 13 | } 14 | block2Image { 15 | ...AssetFields 16 | } 17 | block2Body { 18 | json 19 | } 20 | block3Image { 21 | ...AssetFields 22 | } 23 | block3Body { 24 | json 25 | } 26 | colorPalette 27 | } 28 | 29 | query CtfInfoBlock($id: String!, $locale: String, $preview: Boolean) { 30 | componentInfoBlock(id: $id, locale: $locale, preview: $preview) { 31 | ...InfoBlockFields 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-mobile-menu/ctf-mobile-menu-gql.tsx: -------------------------------------------------------------------------------- 1 | import { useContentfulLiveUpdates } from '@contentful/live-preview/react'; 2 | 3 | import { CtfMobileMenu } from './ctf-mobile-menu'; 4 | 5 | import { useCtfNavigationQuery } from '@src/components/features/ctf-components/ctf-navigation/__generated/ctf-navigation.generated'; 6 | import { useContentfulContext } from '@src/contentful-context'; 7 | 8 | export const CtfMobileMenuGql = props => { 9 | const { locale, previewActive } = useContentfulContext(); 10 | 11 | const { data, isLoading } = useCtfNavigationQuery({ 12 | locale, 13 | preview: previewActive, 14 | }); 15 | 16 | const navigationMenuCollection = useContentfulLiveUpdates(data?.navigationMenuCollection); 17 | 18 | if (!navigationMenuCollection || isLoading) return null; 19 | 20 | return ; 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-mobile-menu/ctf-mobile-menu.tsx: -------------------------------------------------------------------------------- 1 | import { Drawer } from '@mui/material'; 2 | import { makeStyles } from '@mui/styles'; 3 | 4 | import { NavigationFieldsFragment } from '@src/components/features/ctf-components/ctf-navigation/__generated/ctf-navigation.generated'; 5 | import { 6 | getLinkDisplayText, 7 | getLinkHrefPrefix, 8 | } from '@src/components/features/ctf-components/ctf-navigation/utils'; 9 | import { Link } from '@src/components/shared/link'; 10 | 11 | const useStyles = makeStyles(theme => ({ 12 | menu: { 13 | listStyle: 'none', 14 | margin: 0, 15 | padding: theme.spacing(4, 8), 16 | }, 17 | menuItem: { 18 | cursor: 'default', 19 | display: 'block', 20 | fontSize: '2.1rem', 21 | lineHeight: '1.8', 22 | position: 'relative', 23 | 24 | a: { 25 | cursor: 'pointer', 26 | }, 27 | }, 28 | submenu: { 29 | borderLeft: '1px solid #eee', 30 | listStyle: 'none', 31 | padding: theme.spacing(0, 0, 0, 2), 32 | }, 33 | })); 34 | 35 | interface MobileMenuPropsInterface extends NavigationFieldsFragment { 36 | isOpen?: boolean; 37 | onOpenChange: (isOpen: boolean) => any; 38 | } 39 | 40 | export const CtfMobileMenu = (props: MobileMenuPropsInterface) => { 41 | const classes = useStyles(); 42 | 43 | const { isOpen, onOpenChange } = props; 44 | 45 | const onCloseClick = (e, reason) => { 46 | if (reason === 'backdropClick') { 47 | onOpenChange(false); 48 | } 49 | }; 50 | 51 | const mobileMenuContent = props.items[0]; 52 | 53 | const renderMobileMenuLinks = menuGroup => { 54 | return menuGroup?.items?.map(menuItem => { 55 | const href = getLinkHrefPrefix(menuItem); 56 | const linkText = getLinkDisplayText(menuItem); 57 | return ( 58 |
  • 59 | 60 | {linkText} 61 | 62 |
  • 63 | ); 64 | }); 65 | }; 66 | 67 | return ( 68 | 76 | {mobileMenuContent?.menuItemsCollection?.items.length && ( 77 | 98 | )} 99 | 100 | ); 101 | }; 102 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-navigation/__generated/ctf-navigation.generated.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../../../../lib/__generated/graphql.types'; 2 | 3 | import { PageLinkFieldsFragment } from '../../../page-link/__generated/page-link.generated'; 4 | import { MenuGroupFieldsFragment } from '../../../../../lib/shared-fragments/__generated/ctf-menuGroup.generated'; 5 | import { PageLinkFieldsFragmentDoc } from '../../../page-link/__generated/page-link.generated'; 6 | import { MenuGroupFieldsFragmentDoc } from '../../../../../lib/shared-fragments/__generated/ctf-menuGroup.generated'; 7 | import { useQuery, UseQueryOptions } from '@tanstack/react-query'; 8 | import { customFetcher } from '@src/lib/fetchConfig'; 9 | export type NavigationFieldsFragment = { __typename?: 'NavigationMenuCollection', items: Array<{ __typename?: 'NavigationMenu', menuItemsCollection?: { __typename?: 'NavigationMenuMenuItemsCollection', items: Array<{ __typename: 'MenuGroup', groupName?: string | null, sys: { __typename?: 'Sys', id: string }, link?: ( 10 | { __typename?: 'Page' } 11 | & PageLinkFieldsFragment 12 | ) | null, children?: ( 13 | { __typename?: 'MenuGroupFeaturedPagesCollection' } 14 | & MenuGroupFieldsFragment 15 | ) | null } | null> } | null } | null> }; 16 | 17 | export type CtfNavigationQueryVariables = Types.Exact<{ 18 | locale?: Types.InputMaybe; 19 | preview?: Types.InputMaybe; 20 | }>; 21 | 22 | 23 | export type CtfNavigationQuery = { __typename?: 'Query', navigationMenuCollection?: ( 24 | { __typename?: 'NavigationMenuCollection' } 25 | & NavigationFieldsFragment 26 | ) | null }; 27 | 28 | export const NavigationFieldsFragmentDoc = ` 29 | fragment NavigationFields on NavigationMenuCollection { 30 | items { 31 | menuItemsCollection { 32 | items { 33 | __typename 34 | sys { 35 | id 36 | } 37 | groupName 38 | link: groupLink { 39 | ...PageLinkFields 40 | } 41 | children: featuredPagesCollection { 42 | ...MenuGroupFields 43 | } 44 | } 45 | } 46 | } 47 | } 48 | `; 49 | export const CtfNavigationDocument = ` 50 | query CtfNavigation($locale: String, $preview: Boolean) { 51 | navigationMenuCollection(locale: $locale, preview: $preview, limit: 1) { 52 | ...NavigationFields 53 | } 54 | } 55 | ${NavigationFieldsFragmentDoc} 56 | ${PageLinkFieldsFragmentDoc} 57 | ${MenuGroupFieldsFragmentDoc}`; 58 | export const useCtfNavigationQuery = < 59 | TData = CtfNavigationQuery, 60 | TError = unknown 61 | >( 62 | variables?: CtfNavigationQueryVariables, 63 | options?: UseQueryOptions 64 | ) => 65 | useQuery( 66 | variables === undefined ? ['CtfNavigation'] : ['CtfNavigation', variables], 67 | customFetcher(CtfNavigationDocument, variables), 68 | options 69 | ); 70 | 71 | useCtfNavigationQuery.getKey = (variables?: CtfNavigationQueryVariables) => variables === undefined ? ['CtfNavigation'] : ['CtfNavigation', variables]; 72 | ; 73 | 74 | useCtfNavigationQuery.fetcher = (variables?: CtfNavigationQueryVariables, options?: RequestInit['headers']) => customFetcher(CtfNavigationDocument, variables, options); -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-navigation/ctf-navigation-gql.tsx: -------------------------------------------------------------------------------- 1 | import { useContentfulLiveUpdates } from '@contentful/live-preview/react'; 2 | 3 | import { useCtfNavigationQuery } from './__generated/ctf-navigation.generated'; 4 | import { CtfNavigation } from './ctf-navigation'; 5 | 6 | import { useContentfulContext } from '@src/contentful-context'; 7 | 8 | export const CtfNavigationGql = () => { 9 | const { locale, previewActive } = useContentfulContext(); 10 | 11 | const { data, isLoading } = useCtfNavigationQuery({ 12 | locale, 13 | preview: previewActive, 14 | }); 15 | 16 | const navigationMenuCollection = useContentfulLiveUpdates(data?.navigationMenuCollection); 17 | 18 | if (!navigationMenuCollection || isLoading) return null; 19 | 20 | return ; 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-navigation/ctf-navigation.graphql: -------------------------------------------------------------------------------- 1 | fragment NavigationFields on NavigationMenuCollection { 2 | items { 3 | menuItemsCollection { 4 | items { 5 | __typename 6 | sys { 7 | id 8 | } 9 | groupName 10 | link: groupLink { 11 | ...PageLinkFields 12 | } 13 | children: featuredPagesCollection { 14 | ...MenuGroupFields 15 | } 16 | } 17 | } 18 | } 19 | } 20 | 21 | query CtfNavigation($locale: String, $preview: Boolean) { 22 | navigationMenuCollection(locale: $locale, preview: $preview, limit: 1) { 23 | ...NavigationFields 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-navigation/ctf-navigation.tsx: -------------------------------------------------------------------------------- 1 | import { useContentfulInspectorMode } from '@contentful/live-preview/react'; 2 | import { Theme } from '@mui/material'; 3 | import { makeStyles } from '@mui/styles'; 4 | 5 | import { NavigationFieldsFragment } from './__generated/ctf-navigation.generated'; 6 | import { getLinkDisplayText, getLinkHrefPrefix } from './utils'; 7 | 8 | import { Link } from '@src/components/shared/link'; 9 | 10 | const useStyles = makeStyles((theme: Theme) => ({ 11 | menu: { 12 | alignItems: 'center', 13 | display: 'flex', 14 | listStyle: 'none', 15 | margin: 0, 16 | padding: 0, 17 | }, 18 | menuItem: { 19 | alignItems: 'center', 20 | cursor: 'default', 21 | display: 'inline-flex', 22 | fontSize: '1.7rem', 23 | fontWeight: 400, 24 | height: '8rem', 25 | lineHeight: 1.9, 26 | marginRight: theme.spacing(8), 27 | position: 'relative', 28 | 29 | [theme.breakpoints.up('lg')]: { 30 | marginRight: theme.spacing(10), 31 | }, 32 | 33 | '& a': { 34 | cursor: 'pointer', 35 | display: 'inline-block', 36 | transition: 'transform 0.2s ease-in-out', 37 | }, 38 | 39 | '&:hover, &:focus, &:focus-within': { 40 | '& > a': { 41 | transform: 'translateY(-4px)', 42 | }, 43 | '& $submenu': { 44 | opacity: 1, 45 | pointerEvents: 'all', 46 | transform: 'translateY(0)', 47 | }, 48 | }, 49 | }, 50 | submenu: { 51 | backgroundColor: '#fff', 52 | boxShadow: '0 3px 6px #00000029', 53 | borderRadius: '14px', 54 | left: theme.spacing(10 * -1), 55 | listStyle: 'none', 56 | opacity: 0, 57 | padding: theme.spacing(4, 10), 58 | pointerEvents: 'none', 59 | position: 'absolute', 60 | top: 'calc(100% - 2rem)', 61 | transform: 'translateY(20%)', 62 | transition: 'all 0.3s ease-in-out', 63 | }, 64 | submenuItem: { 65 | '&:hover, &:focus, &:focus-within': { 66 | '& > a': { 67 | transform: 'translateY(-4px)', 68 | }, 69 | }, 70 | }, 71 | })); 72 | 73 | export const CtfNavigation = (props: NavigationFieldsFragment) => { 74 | const classes = useStyles(); 75 | const inspectorMode = useContentfulInspectorMode(); 76 | 77 | const navigationContent = props.items[0]; 78 | 79 | const renderNavigationLinks = (menuGroup, listClassName) => { 80 | return menuGroup?.items?.map(menuItem => { 81 | const href = getLinkHrefPrefix(menuItem); 82 | const linkText = getLinkDisplayText(menuItem); 83 | 84 | return ( 85 |
  • 93 | {linkText} 94 |
  • 95 | ); 96 | }); 97 | }; 98 | 99 | return ( 100 | <> 101 | {navigationContent?.menuItemsCollection?.items.length && ( 102 | 130 | )} 131 | 132 | ); 133 | }; 134 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-navigation/utils.ts: -------------------------------------------------------------------------------- 1 | export const getLinkDisplayText = menuItem => { 2 | if ('pageName' in menuItem) { 3 | return menuItem.pageName; 4 | } 5 | if ('categoryName' in menuItem) { 6 | return menuItem.categoryName; 7 | } 8 | if ('postName' in menuItem) { 9 | return menuItem.postName; 10 | } 11 | return menuItem.slug; 12 | }; 13 | 14 | export const getLinkHrefPrefix = menuItem => { 15 | if ('pageName' in menuItem) { 16 | return `/${menuItem.slug}`; 17 | } 18 | 19 | if ('categoryName' in menuItem) { 20 | return `/category/${menuItem.slug}`; 21 | } 22 | 23 | if ('postName' in menuItem) { 24 | return `/post/${menuItem.slug}`; 25 | } 26 | 27 | return `/${menuItem.slug}`; 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-page/ctf-page-gql.tsx: -------------------------------------------------------------------------------- 1 | import { useContentfulLiveUpdates } from '@contentful/live-preview/react'; 2 | import Head from 'next/head'; 3 | 4 | import CtfPage from './ctf-page'; 5 | 6 | import { useCtfPageQuery } from '@src/components/features/ctf-components/ctf-page/__generated/ctf-page.generated'; 7 | import { PageError } from '@src/components/features/errors/page-error'; 8 | import { useContentfulContext } from '@src/contentful-context'; 9 | import { tryget } from '@src/utils'; 10 | import contentfulConfig from 'contentful.config'; 11 | 12 | interface Props { 13 | topic?: string; 14 | slug: string; 15 | } 16 | 17 | const CtfPageGgl = ({ slug: slugFromProps }: Props) => { 18 | const slug = !slugFromProps || slugFromProps === '/' ? 'home' : slugFromProps; 19 | 20 | const { previewActive, locale } = useContentfulContext(); 21 | 22 | const { isLoading, data } = useCtfPageQuery({ 23 | slug, 24 | locale, 25 | preview: previewActive, 26 | }); 27 | 28 | const page = useContentfulLiveUpdates(tryget(() => data?.pageCollection!.items[0])); 29 | 30 | if (isLoading) return <>; 31 | if (!page) { 32 | const error = { 33 | code: 404, 34 | message: 35 | 'We were not able to locate the content you were looking for, please check the url for possible typos', 36 | }; 37 | return ; 38 | } 39 | 40 | const { seo } = page || {}; 41 | 42 | const metaTags = { 43 | title: seo?.title ?? page.pageName, 44 | description: seo?.description, 45 | image: seo?.image, 46 | no_index: seo?.noIndex, 47 | no_follow: seo?.noFollow, 48 | }; 49 | 50 | const robots = [ 51 | metaTags.no_index === true ? 'noindex' : undefined, 52 | metaTags.no_follow === true ? 'nofollow' : undefined, 53 | ].filter((x): x is string => x !== undefined); 54 | 55 | return ( 56 | <> 57 | 58 | {metaTags.title && ( 59 | <> 60 | {metaTags.title} 61 | 62 | 63 | )} 64 | {metaTags.description && ( 65 | <> 66 | 67 | 68 | 69 | )} 70 | {robots.length > 0 && } 71 | {metaTags.image && ( 72 | 77 | )} 78 | {page.slug && ( 79 | 84 | )} 85 | 86 | 87 | 88 | 89 | ); 90 | }; 91 | 92 | export default CtfPageGgl; 93 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-page/ctf-page.graphql: -------------------------------------------------------------------------------- 1 | fragment PageTopSectionFields on PageTopSectionItem { 2 | __typename 3 | } 4 | 5 | fragment PageContentFields on PagePageContent { 6 | __typename 7 | } 8 | 9 | fragment PageExtraSectionItemFields on PageExtraSectionItem { 10 | __typename 11 | } 12 | 13 | 14 | fragment CtfPageFields on Page { 15 | __typename 16 | sys { 17 | id 18 | } 19 | pageName 20 | internalName: pageName 21 | slug 22 | seo { 23 | title 24 | description 25 | image { 26 | ...AssetFields 27 | } 28 | noIndex 29 | noFollow 30 | } 31 | topSectionCollection(limit: 20) { 32 | items { 33 | ... on Entry { 34 | __typename 35 | sys { 36 | id 37 | } 38 | } 39 | ...PageTopSectionFields 40 | } 41 | } 42 | pageContent { 43 | ... on Entry { 44 | __typename 45 | sys { 46 | id 47 | } 48 | } 49 | ...PageContentFields 50 | } 51 | extraSectionCollection(limit: 20) { 52 | items { 53 | ... on Entry { 54 | __typename 55 | sys { 56 | id 57 | } 58 | } 59 | ...PageExtraSectionItemFields 60 | } 61 | } 62 | } 63 | 64 | query CtfPage($slug: String!, $locale: String, $preview: Boolean) { 65 | pageCollection(where: { slug: $slug }, locale: $locale, preview: $preview, limit: 1) { 66 | items { 67 | ...CtfPageFields 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-page/ctf-page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { CtfPageFieldsFragment } from '@src/components/features/ctf-components/ctf-page/__generated/ctf-page.generated'; 4 | import { ComponentResolver } from '@src/components/shared/component-resolver'; 5 | import { PageContainer } from '@src/components/templates/page-container'; 6 | import LayoutContext, { defaultLayout } from '@src/layout-context'; 7 | 8 | const CtfPage = (props: CtfPageFieldsFragment) => { 9 | const topSection = 10 | props.topSectionCollection && props.topSectionCollection.items.filter(it => !!it); 11 | const content = props.pageContent; 12 | const extraSection = 13 | props.extraSectionCollection && props.extraSectionCollection.items.filter(it => !!it); 14 | 15 | const layoutConfig = { 16 | ...defaultLayout, 17 | containerWidth: 1262, 18 | }; 19 | 20 | return ( 21 | 22 | {topSection && 23 | topSection.map(entry => ( 24 | 25 | 26 | 27 | ))} 28 | 29 | {content && ( 30 | 31 | 32 | 33 | )} 34 | 35 | {extraSection && 36 | extraSection.map(entry => ( 37 | 38 | 39 | 40 | ))} 41 | 42 | ); 43 | }; 44 | 45 | export default CtfPage; 46 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-person/__generated/ctf-person.generated.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../../../../lib/__generated/graphql.types'; 2 | 3 | import { AssetFieldsFragment } from '../../ctf-asset/__generated/ctf-asset.generated'; 4 | import { AssetFieldsFragmentDoc } from '../../ctf-asset/__generated/ctf-asset.generated'; 5 | import { useQuery, UseQueryOptions } from '@tanstack/react-query'; 6 | import { customFetcher } from '@src/lib/fetchConfig'; 7 | export type PersonFieldsFragment = { __typename: 'TopicPerson', name?: string | null, website?: string | null, location?: string | null, cardStyle?: boolean | null, sys: { __typename?: 'Sys', id: string }, bio?: { __typename?: 'TopicPersonBio', json: any } | null, avatar?: ( 8 | { __typename?: 'Asset' } 9 | & AssetFieldsFragment 10 | ) | null }; 11 | 12 | export type CtfPersonQueryVariables = Types.Exact<{ 13 | id: Types.Scalars['String']; 14 | locale?: Types.InputMaybe; 15 | preview?: Types.InputMaybe; 16 | }>; 17 | 18 | 19 | export type CtfPersonQuery = { __typename?: 'Query', topicPerson?: ( 20 | { __typename?: 'TopicPerson' } 21 | & PersonFieldsFragment 22 | ) | null }; 23 | 24 | export const PersonFieldsFragmentDoc = ` 25 | fragment PersonFields on TopicPerson { 26 | __typename 27 | sys { 28 | id 29 | } 30 | name 31 | bio { 32 | json 33 | } 34 | avatar { 35 | ...AssetFields 36 | } 37 | website 38 | location 39 | cardStyle 40 | } 41 | `; 42 | export const CtfPersonDocument = ` 43 | query CtfPerson($id: String!, $locale: String, $preview: Boolean) { 44 | topicPerson(id: $id, preview: $preview, locale: $locale) { 45 | ...PersonFields 46 | } 47 | } 48 | ${PersonFieldsFragmentDoc} 49 | ${AssetFieldsFragmentDoc}`; 50 | export const useCtfPersonQuery = < 51 | TData = CtfPersonQuery, 52 | TError = unknown 53 | >( 54 | variables: CtfPersonQueryVariables, 55 | options?: UseQueryOptions 56 | ) => 57 | useQuery( 58 | ['CtfPerson', variables], 59 | customFetcher(CtfPersonDocument, variables), 60 | options 61 | ); 62 | 63 | useCtfPersonQuery.getKey = (variables: CtfPersonQueryVariables) => ['CtfPerson', variables]; 64 | ; 65 | 66 | useCtfPersonQuery.fetcher = (variables: CtfPersonQueryVariables, options?: RequestInit['headers']) => customFetcher(CtfPersonDocument, variables, options); -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-person/ctf-person-gql.tsx: -------------------------------------------------------------------------------- 1 | import { useContentfulLiveUpdates } from '@contentful/live-preview/react'; 2 | 3 | import { CtfPerson } from './ctf-person'; 4 | 5 | import { useCtfPersonQuery } from '@src/components/features/ctf-components/ctf-person/__generated/ctf-person.generated'; 6 | 7 | interface CtfPersonGqlPropsInterface { 8 | id: string; 9 | locale: string; 10 | preview: boolean; 11 | previousComponent: string | null; 12 | } 13 | 14 | export const CtfPersonGql = (props: CtfPersonGqlPropsInterface) => { 15 | const { id, locale, preview, previousComponent } = props; 16 | 17 | const { isLoading, data } = useCtfPersonQuery({ 18 | id, 19 | locale, 20 | preview, 21 | }); 22 | 23 | const topicPerson = useContentfulLiveUpdates(data?.topicPerson); 24 | 25 | if (isLoading || !topicPerson) { 26 | return null; 27 | } 28 | 29 | return ; 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-person/ctf-person.graphql: -------------------------------------------------------------------------------- 1 | fragment PersonFields on TopicPerson { 2 | __typename 3 | sys { 4 | id 5 | } 6 | name 7 | bio { 8 | json 9 | } 10 | avatar { 11 | ...AssetFields 12 | } 13 | website 14 | location 15 | cardStyle 16 | } 17 | 18 | query CtfPerson($id: String!, $locale: String, $preview: Boolean) { 19 | topicPerson(id: $id, preview: $preview, locale: $locale) { 20 | ...PersonFields 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-person/ctf-person.tsx: -------------------------------------------------------------------------------- 1 | import { Theme, Container } from '@mui/material'; 2 | import { makeStyles } from '@mui/styles'; 3 | 4 | import { PersonFieldsFragment } from './__generated/ctf-person.generated'; 5 | 6 | import { Author } from '@src/components/features/author'; 7 | import { CardLeadership } from '@src/components/features/card-leadership'; 8 | import { CardPerson } from '@src/components/features/card-person'; 9 | import { useLayoutContext } from '@src/layout-context'; 10 | 11 | const useStyles = makeStyles((theme: Theme) => ({ 12 | root: { 13 | marginLeft: 'auto', 14 | marginRight: 'auto', 15 | paddingBottom: theme.spacing(3), 16 | paddingTop: theme.spacing(3), 17 | }, 18 | })); 19 | 20 | interface CtfPersonPropsInterface extends PersonFieldsFragment { 21 | previousComponent: string | null; 22 | } 23 | 24 | export const CtfPerson = (props: CtfPersonPropsInterface) => { 25 | const layout = useLayoutContext(); 26 | const classes = useStyles(); 27 | const isLeadership = props.cardStyle === false; 28 | 29 | return layout.parent === 'quote' ? ( 30 |
    31 | 32 |
    33 | ) : ( 34 | 35 |
    36 | {isLeadership ? : } 37 |
    38 |
    39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-product-feature/__generated/ctf-product-feature.generated.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../../../../lib/__generated/graphql.types'; 2 | 3 | import { AssetFieldsFragment } from '../../ctf-asset/__generated/ctf-asset.generated'; 4 | import { AssetFieldsFragmentDoc } from '../../ctf-asset/__generated/ctf-asset.generated'; 5 | import { useQuery, UseQueryOptions } from '@tanstack/react-query'; 6 | import { customFetcher } from '@src/lib/fetchConfig'; 7 | export type ProductFeatureFieldsFragment = { __typename: 'TopicProductFeature', name?: string | null, sys: { __typename?: 'Sys', id: string }, longDescription?: { __typename?: 'TopicProductFeatureLongDescription', json: any, links: { __typename?: 'TopicProductFeatureLongDescriptionLinks', assets: { __typename?: 'TopicProductFeatureLongDescriptionAssets', block: Array<( 8 | { __typename?: 'Asset' } 9 | & AssetFieldsFragment 10 | ) | null> } } } | null, shortDescription?: { __typename?: 'TopicProductFeatureShortDescription', json: any, links: { __typename?: 'TopicProductFeatureShortDescriptionLinks', assets: { __typename?: 'TopicProductFeatureShortDescriptionAssets', block: Array<( 11 | { __typename?: 'Asset' } 12 | & AssetFieldsFragment 13 | ) | null> } } } | null }; 14 | 15 | export type CtfProductFeatureQueryVariables = Types.Exact<{ 16 | id: Types.Scalars['String']; 17 | locale?: Types.InputMaybe; 18 | preview?: Types.InputMaybe; 19 | }>; 20 | 21 | 22 | export type CtfProductFeatureQuery = { __typename?: 'Query', topicProductFeature?: ( 23 | { __typename?: 'TopicProductFeature' } 24 | & ProductFeatureFieldsFragment 25 | ) | null }; 26 | 27 | export const ProductFeatureFieldsFragmentDoc = ` 28 | fragment ProductFeatureFields on TopicProductFeature { 29 | __typename 30 | sys { 31 | id 32 | } 33 | name 34 | longDescription { 35 | json 36 | links { 37 | assets { 38 | block { 39 | ...AssetFields 40 | } 41 | } 42 | } 43 | } 44 | shortDescription { 45 | json 46 | links { 47 | assets { 48 | block { 49 | ...AssetFields 50 | } 51 | } 52 | } 53 | } 54 | } 55 | `; 56 | export const CtfProductFeatureDocument = ` 57 | query CtfProductFeature($id: String!, $locale: String, $preview: Boolean) { 58 | topicProductFeature(id: $id, preview: $preview, locale: $locale) { 59 | ...ProductFeatureFields 60 | } 61 | } 62 | ${ProductFeatureFieldsFragmentDoc} 63 | ${AssetFieldsFragmentDoc}`; 64 | export const useCtfProductFeatureQuery = < 65 | TData = CtfProductFeatureQuery, 66 | TError = unknown 67 | >( 68 | variables: CtfProductFeatureQueryVariables, 69 | options?: UseQueryOptions 70 | ) => 71 | useQuery( 72 | ['CtfProductFeature', variables], 73 | customFetcher(CtfProductFeatureDocument, variables), 74 | options 75 | ); 76 | 77 | useCtfProductFeatureQuery.getKey = (variables: CtfProductFeatureQueryVariables) => ['CtfProductFeature', variables]; 78 | ; 79 | 80 | useCtfProductFeatureQuery.fetcher = (variables: CtfProductFeatureQueryVariables, options?: RequestInit['headers']) => customFetcher(CtfProductFeatureDocument, variables, options); -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-product-feature/ctf-product-feature.graphql: -------------------------------------------------------------------------------- 1 | fragment ProductFeatureFields on TopicProductFeature { 2 | __typename 3 | sys { 4 | id 5 | } 6 | name 7 | longDescription { 8 | json 9 | links { 10 | assets { 11 | block { 12 | ...AssetFields 13 | } 14 | } 15 | } 16 | } 17 | shortDescription { 18 | json 19 | links { 20 | assets { 21 | block { 22 | ...AssetFields 23 | } 24 | } 25 | } 26 | } 27 | } 28 | 29 | query CtfProductFeature($id: String!, $locale: String, $preview: Boolean) { 30 | topicProductFeature(id: $id, preview: $preview, locale: $locale) { 31 | ...ProductFeatureFields 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-product-table/__generated/ctf-product-table.generated.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../../../../lib/__generated/graphql.types'; 2 | 3 | import { ProductFieldsFragment } from '../../ctf-product/__generated/ctf-product.generated'; 4 | import { AssetFieldsFragment } from '../../ctf-asset/__generated/ctf-asset.generated'; 5 | import { ProductFeatureFieldsFragment } from '../../ctf-product-feature/__generated/ctf-product-feature.generated'; 6 | import { ProductFieldsFragmentDoc } from '../../ctf-product/__generated/ctf-product.generated'; 7 | import { AssetFieldsFragmentDoc } from '../../ctf-asset/__generated/ctf-asset.generated'; 8 | import { ProductFeatureFieldsFragmentDoc } from '../../ctf-product-feature/__generated/ctf-product-feature.generated'; 9 | import { useQuery, UseQueryOptions } from '@tanstack/react-query'; 10 | import { customFetcher } from '@src/lib/fetchConfig'; 11 | export type ProductTableFieldsFragment = { __typename: 'ComponentProductTable', headline?: string | null, subline?: string | null, sys: { __typename?: 'Sys', id: string }, productsCollection?: { __typename?: 'ComponentProductTableProductsCollection', items: Array<( 12 | { __typename?: 'TopicProduct' } 13 | & ProductFieldsFragment 14 | ) | null> } | null }; 15 | 16 | export type CtfProductTableQueryVariables = Types.Exact<{ 17 | id: Types.Scalars['String']; 18 | locale?: Types.InputMaybe; 19 | preview?: Types.InputMaybe; 20 | }>; 21 | 22 | 23 | export type CtfProductTableQuery = { __typename?: 'Query', componentProductTable?: ( 24 | { __typename?: 'ComponentProductTable' } 25 | & ProductTableFieldsFragment 26 | ) | null }; 27 | 28 | export const ProductTableFieldsFragmentDoc = ` 29 | fragment ProductTableFields on ComponentProductTable { 30 | __typename 31 | sys { 32 | id 33 | } 34 | headline 35 | subline 36 | productsCollection(limit: 3) { 37 | items { 38 | ...ProductFields 39 | } 40 | } 41 | } 42 | `; 43 | export const CtfProductTableDocument = ` 44 | query CtfProductTable($id: String!, $locale: String, $preview: Boolean) { 45 | componentProductTable(id: $id, preview: $preview, locale: $locale) { 46 | ...ProductTableFields 47 | } 48 | } 49 | ${ProductTableFieldsFragmentDoc} 50 | ${ProductFieldsFragmentDoc} 51 | ${AssetFieldsFragmentDoc} 52 | ${ProductFeatureFieldsFragmentDoc}`; 53 | export const useCtfProductTableQuery = < 54 | TData = CtfProductTableQuery, 55 | TError = unknown 56 | >( 57 | variables: CtfProductTableQueryVariables, 58 | options?: UseQueryOptions 59 | ) => 60 | useQuery( 61 | ['CtfProductTable', variables], 62 | customFetcher(CtfProductTableDocument, variables), 63 | options 64 | ); 65 | 66 | useCtfProductTableQuery.getKey = (variables: CtfProductTableQueryVariables) => ['CtfProductTable', variables]; 67 | ; 68 | 69 | useCtfProductTableQuery.fetcher = (variables: CtfProductTableQueryVariables, options?: RequestInit['headers']) => customFetcher(CtfProductTableDocument, variables, options); -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-product-table/ctf-product-table-gql.tsx: -------------------------------------------------------------------------------- 1 | import { useContentfulLiveUpdates } from '@contentful/live-preview/react'; 2 | import { Container } from '@mui/material'; 3 | 4 | import { useCtfProductTableQuery } from './__generated/ctf-product-table.generated'; 5 | import { CtfProductTable } from './ctf-product-table'; 6 | 7 | import { EntryNotFound } from '@src/components/features/errors/entry-not-found'; 8 | 9 | interface CtfProductTableGqlPropsInterface { 10 | id: string; 11 | locale: string; 12 | preview?: boolean; 13 | } 14 | 15 | export const CtfProductTableGql = (props: CtfProductTableGqlPropsInterface) => { 16 | const { isLoading, data } = useCtfProductTableQuery({ 17 | id: props.id, 18 | locale: props.locale, 19 | preview: props.preview, 20 | }); 21 | 22 | const componentProductTable = useContentfulLiveUpdates(data?.componentProductTable); 23 | 24 | if (isLoading || !componentProductTable) { 25 | return null; 26 | } 27 | 28 | if (!componentProductTable) { 29 | return ( 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | return ; 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-product-table/ctf-product-table.graphql: -------------------------------------------------------------------------------- 1 | fragment ProductTableFields on ComponentProductTable { 2 | __typename 3 | sys { 4 | id 5 | } 6 | headline 7 | subline 8 | productsCollection(limit: 3) { 9 | items { 10 | ...ProductFields 11 | } 12 | } 13 | } 14 | 15 | query CtfProductTable($id: String!, $locale: String, $preview: Boolean) { 16 | componentProductTable(id: $id, preview: $preview, locale: $locale) { 17 | ...ProductTableFields 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-product/__generated/ctf-product.generated.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../../../../lib/__generated/graphql.types'; 2 | 3 | import { AssetFieldsFragment } from '../../ctf-asset/__generated/ctf-asset.generated'; 4 | import { ProductFeatureFieldsFragment } from '../../ctf-product-feature/__generated/ctf-product-feature.generated'; 5 | import { AssetFieldsFragmentDoc } from '../../ctf-asset/__generated/ctf-asset.generated'; 6 | import { ProductFeatureFieldsFragmentDoc } from '../../ctf-product-feature/__generated/ctf-product-feature.generated'; 7 | import { useQuery, UseQueryOptions } from '@tanstack/react-query'; 8 | import { customFetcher } from '@src/lib/fetchConfig'; 9 | export type ProductFieldsFragment = { __typename: 'TopicProduct', name?: string | null, price?: number | null, sys: { __typename?: 'Sys', id: string }, featuredImage?: ( 10 | { __typename?: 'Asset' } 11 | & AssetFieldsFragment 12 | ) | null, description?: { __typename?: 'TopicProductDescription', json: any } | null, featuresCollection?: { __typename?: 'TopicProductFeaturesCollection', items: Array<( 13 | { __typename?: 'TopicProductFeature' } 14 | & ProductFeatureFieldsFragment 15 | ) | null> } | null }; 16 | 17 | export type CtfProductQueryVariables = Types.Exact<{ 18 | id: Types.Scalars['String']; 19 | locale?: Types.InputMaybe; 20 | preview?: Types.InputMaybe; 21 | }>; 22 | 23 | 24 | export type CtfProductQuery = { __typename?: 'Query', topicProduct?: ( 25 | { __typename?: 'TopicProduct' } 26 | & ProductFieldsFragment 27 | ) | null }; 28 | 29 | export const ProductFieldsFragmentDoc = ` 30 | fragment ProductFields on TopicProduct { 31 | __typename 32 | sys { 33 | id 34 | } 35 | name 36 | featuredImage { 37 | ...AssetFields 38 | } 39 | description { 40 | json 41 | } 42 | price 43 | featuresCollection(limit: 30) { 44 | items { 45 | ...ProductFeatureFields 46 | } 47 | } 48 | } 49 | `; 50 | export const CtfProductDocument = ` 51 | query CtfProduct($id: String!, $locale: String, $preview: Boolean) { 52 | topicProduct(id: $id, preview: $preview, locale: $locale) { 53 | ...ProductFields 54 | } 55 | } 56 | ${ProductFieldsFragmentDoc} 57 | ${AssetFieldsFragmentDoc} 58 | ${ProductFeatureFieldsFragmentDoc}`; 59 | export const useCtfProductQuery = < 60 | TData = CtfProductQuery, 61 | TError = unknown 62 | >( 63 | variables: CtfProductQueryVariables, 64 | options?: UseQueryOptions 65 | ) => 66 | useQuery( 67 | ['CtfProduct', variables], 68 | customFetcher(CtfProductDocument, variables), 69 | options 70 | ); 71 | 72 | useCtfProductQuery.getKey = (variables: CtfProductQueryVariables) => ['CtfProduct', variables]; 73 | ; 74 | 75 | useCtfProductQuery.fetcher = (variables: CtfProductQueryVariables, options?: RequestInit['headers']) => customFetcher(CtfProductDocument, variables, options); -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-product/ctf-product-gql.tsx: -------------------------------------------------------------------------------- 1 | import { useContentfulLiveUpdates } from '@contentful/live-preview/react'; 2 | import { Container } from '@mui/material'; 3 | import Head from 'next/head'; 4 | 5 | import { useCtfProductQuery } from './__generated/ctf-product.generated'; 6 | import { CtfProduct } from './ctf-product'; 7 | 8 | import { EntryNotFound } from '@src/components/features/errors/entry-not-found'; 9 | 10 | interface CtfProductGqlPropsInterface { 11 | id: string; 12 | locale: string; 13 | preview?: boolean; 14 | } 15 | 16 | export const CtfProductGql = (props: CtfProductGqlPropsInterface) => { 17 | const { isLoading, data } = useCtfProductQuery({ 18 | id: props.id, 19 | locale: props.locale, 20 | preview: props.preview, 21 | }); 22 | 23 | const topicProduct = useContentfulLiveUpdates(data?.topicProduct); 24 | 25 | if (!data || isLoading) { 26 | return null; 27 | } 28 | 29 | if (!topicProduct) { 30 | return ( 31 | 32 | 33 | 34 | ); 35 | } 36 | 37 | return ( 38 | <> 39 | {topicProduct?.featuredImage && ( 40 | 41 | 46 | 47 | )} 48 | 49 | 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-product/ctf-product.graphql: -------------------------------------------------------------------------------- 1 | fragment ProductFields on TopicProduct { 2 | __typename 3 | sys { 4 | id 5 | } 6 | name 7 | featuredImage { 8 | ...AssetFields 9 | } 10 | description { 11 | json 12 | } 13 | price 14 | featuresCollection(limit: 30) { 15 | items { 16 | ...ProductFeatureFields 17 | } 18 | } 19 | } 20 | 21 | query CtfProduct($id: String!, $locale: String, $preview: Boolean) { 22 | topicProduct(id: $id, preview: $preview, locale: $locale) { 23 | ...ProductFields 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-quote/ctf-quote-gql.tsx: -------------------------------------------------------------------------------- 1 | import { useContentfulLiveUpdates } from '@contentful/live-preview/react'; 2 | 3 | import { useCtfQuoteQuery } from './__generated/ctf-quote.generated'; 4 | import { CtfQuote } from './ctf-quote'; 5 | 6 | interface CtfQuoteGqlPropsInterface { 7 | id: string; 8 | locale: string; 9 | preview: boolean; 10 | } 11 | 12 | export const CtfQuoteGql = (props: CtfQuoteGqlPropsInterface) => { 13 | const { id, locale, preview } = props; 14 | 15 | const { isLoading, data } = useCtfQuoteQuery({ 16 | id, 17 | locale, 18 | preview, 19 | }); 20 | 21 | const componentQuote = useContentfulLiveUpdates(data?.componentQuote); 22 | 23 | if (isLoading || !componentQuote) { 24 | return null; 25 | } 26 | 27 | return ; 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-quote/ctf-quote.graphql: -------------------------------------------------------------------------------- 1 | fragment QuoteFields on ComponentQuote { 2 | __typename 3 | sys { 4 | id 5 | } 6 | quote { 7 | json 8 | links { 9 | entries { 10 | block { 11 | ...ComponentReferenceFields 12 | } 13 | } 14 | assets { 15 | block { 16 | ...AssetFields 17 | } 18 | } 19 | } 20 | } 21 | quoteAlignment 22 | image { 23 | ...AssetFields 24 | } 25 | imagePosition 26 | colorPalette 27 | } 28 | 29 | query CtfQuote($id: String!, $locale: String, $preview: Boolean) { 30 | componentQuote(id: $id, locale: $locale, preview: $preview) { 31 | ...QuoteFields 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-richtext/__generated/ctf-richtext.generated.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../../../../lib/__generated/graphql.types'; 2 | 3 | import { PageLinkFieldsFragment } from '../../../page-link/__generated/page-link.generated'; 4 | import { PageLinkFieldsFragmentDoc } from '../../../page-link/__generated/page-link.generated'; 5 | import { useQuery, UseQueryOptions } from '@tanstack/react-query'; 6 | import { customFetcher } from '@src/lib/fetchConfig'; 7 | export type RichTextHyperlinkFieldsFragment = { __typename?: 'Query', page?: ( 8 | { __typename?: 'Page' } 9 | & PageLinkFieldsFragment 10 | ) | null }; 11 | 12 | export type CtfRichTextHyperlinkQueryVariables = Types.Exact<{ 13 | id: Types.Scalars['String']; 14 | locale?: Types.InputMaybe; 15 | preview?: Types.InputMaybe; 16 | }>; 17 | 18 | 19 | export type CtfRichTextHyperlinkQuery = ( 20 | { __typename?: 'Query' } 21 | & RichTextHyperlinkFieldsFragment 22 | ); 23 | 24 | export const RichTextHyperlinkFieldsFragmentDoc = ` 25 | fragment RichTextHyperlinkFields on Query { 26 | page(id: $id, preview: $preview, locale: $locale) { 27 | ...PageLinkFields 28 | } 29 | } 30 | `; 31 | export const CtfRichTextHyperlinkDocument = ` 32 | query CtfRichTextHyperlink($id: String!, $locale: String, $preview: Boolean) { 33 | ...RichTextHyperlinkFields 34 | } 35 | ${RichTextHyperlinkFieldsFragmentDoc} 36 | ${PageLinkFieldsFragmentDoc}`; 37 | export const useCtfRichTextHyperlinkQuery = < 38 | TData = CtfRichTextHyperlinkQuery, 39 | TError = unknown 40 | >( 41 | variables: CtfRichTextHyperlinkQueryVariables, 42 | options?: UseQueryOptions 43 | ) => 44 | useQuery( 45 | ['CtfRichTextHyperlink', variables], 46 | customFetcher(CtfRichTextHyperlinkDocument, variables), 47 | options 48 | ); 49 | 50 | useCtfRichTextHyperlinkQuery.getKey = (variables: CtfRichTextHyperlinkQueryVariables) => ['CtfRichTextHyperlink', variables]; 51 | ; 52 | 53 | useCtfRichTextHyperlinkQuery.fetcher = (variables: CtfRichTextHyperlinkQueryVariables, options?: RequestInit['headers']) => customFetcher(CtfRichTextHyperlinkDocument, variables, options); -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-richtext/ctf-richtext.graphql: -------------------------------------------------------------------------------- 1 | fragment RichTextHyperlinkFields on Query { 2 | page(id: $id, preview: $preview, locale: $locale) { 3 | ...PageLinkFields 4 | } 5 | } 6 | 7 | query CtfRichTextHyperlink($id: String!, $locale: String, $preview: Boolean) { 8 | ...RichTextHyperlinkFields 9 | } 10 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-text-block/ctf-text-block-gql.tsx: -------------------------------------------------------------------------------- 1 | import { useContentfulLiveUpdates } from '@contentful/live-preview/react'; 2 | 3 | import { useCtfTextBlockQuery } from './__generated/ctf-text-block.generated'; 4 | import { CtfTextBlock } from './ctf-text-block'; 5 | 6 | interface CtfTextBlockGqlPropsInterface { 7 | id: string; 8 | locale: string; 9 | preview: boolean; 10 | } 11 | 12 | export const CtfTextBlockGql = ({ id, locale, preview }: CtfTextBlockGqlPropsInterface) => { 13 | const { isLoading, data } = useCtfTextBlockQuery({ 14 | id, 15 | locale, 16 | preview, 17 | }); 18 | 19 | const componentTextBlock = useContentfulLiveUpdates(data?.componentTextBlock); 20 | 21 | if (isLoading || !componentTextBlock) { 22 | return null; 23 | } 24 | 25 | return ; 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-text-block/ctf-text-block.graphql: -------------------------------------------------------------------------------- 1 | fragment TextBlockFields on ComponentTextBlock { 2 | __typename 3 | sys { 4 | id 5 | } 6 | headline 7 | subline 8 | body { 9 | json 10 | links { 11 | entries { 12 | block { 13 | ...ComponentReferenceFields 14 | } 15 | } 16 | assets { 17 | block { 18 | ...AssetFields 19 | } 20 | } 21 | } 22 | } 23 | colorPalette 24 | } 25 | 26 | query CtfTextBlock($id: String!, $locale: String, $preview: Boolean) { 27 | componentTextBlock(id: $id, locale: $locale, preview: $preview) { 28 | ...TextBlockFields 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-text-block/ctf-text-block.tsx: -------------------------------------------------------------------------------- 1 | import { Theme, Container } from '@mui/material'; 2 | import { makeStyles } from '@mui/styles'; 3 | 4 | import { TextBlockFieldsFragment } from './__generated/ctf-text-block.generated'; 5 | 6 | import { CtfRichtext } from '@src/components/features/ctf-components/ctf-richtext/ctf-richtext'; 7 | import { SectionHeadlines } from '@src/components/features/section-headlines'; 8 | import { getColorConfigFromPalette } from '@src/theme'; 9 | 10 | const useStyles = makeStyles((theme: Theme) => ({ 11 | innerContainer: { 12 | marginLeft: 'auto', 13 | marginRight: 'auto', 14 | maxWidth: '126rem', 15 | padding: theme.spacing(19, 0, 19), 16 | }, 17 | sectionHeadlines: { 18 | marginBottom: theme.spacing(12), 19 | }, 20 | })); 21 | 22 | export const CtfTextBlock = ({ 23 | headline, 24 | subline, 25 | body, 26 | colorPalette, 27 | }: TextBlockFieldsFragment) => { 28 | const colorConfig = getColorConfigFromPalette(colorPalette || ''); 29 | const classes = useStyles(); 30 | 31 | return ( 32 | 37 |
    38 | 49 | {body && ( 50 |
    54 | 55 |
    56 | )} 57 |
    58 |
    59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /src/components/features/ctf-components/ctf-video/ctf-video.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@mui/styles'; 2 | import React from 'react'; 3 | 4 | import { AssetFieldsFragment } from '@src/components/features/ctf-components/ctf-asset/__generated/ctf-asset.generated'; 5 | 6 | const useStyles = makeStyles(() => ({ 7 | image: { 8 | width: '100%', 9 | }, 10 | 11 | video: { 12 | width: '100%', 13 | }, 14 | 15 | caption: { 16 | color: '#797979', 17 | fontSize: '1.8rem', 18 | fontStyle: 'italic', 19 | lineHeight: 1.389, 20 | marginLeft: 'auto', 21 | marginRight: 'auto', 22 | marginTop: '4.7rem', 23 | maxWidth: '77rem', 24 | textAlign: 'center', 25 | }, 26 | })); 27 | 28 | interface CtfVideoPropsInterface extends AssetFieldsFragment { 29 | showDescription?: boolean; 30 | autoplay?: boolean; 31 | className?: string; 32 | } 33 | 34 | export const CtfVideo = (props: CtfVideoPropsInterface) => { 35 | const { description, url, showDescription, autoplay, className } = props; 36 | 37 | const classes = useStyles(); 38 | return ( 39 |
    40 | {/* eslint-disable-next-line jsx-a11y/media-has-caption */} 41 |
    44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/features/errors/entry-not-found.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'next-i18next'; 2 | import React from 'react'; 3 | 4 | import { ErrorBox } from '@src/components/shared/error-box'; 5 | 6 | export const EntryNotFound = (props: { className?: string }) => { 7 | const { t } = useTranslation(); 8 | 9 | return {t('error.componentNotFound')}; 10 | }; 11 | -------------------------------------------------------------------------------- /src/components/features/errors/page-error.tsx: -------------------------------------------------------------------------------- 1 | import { Theme, Container, Grid, Typography } from '@mui/material'; 2 | import { makeStyles } from '@mui/styles'; 3 | import { useTranslation } from 'next-i18next'; 4 | import React from 'react'; 5 | import { File } from 'react-kawaii'; 6 | 7 | import { PageContainer } from '@src/components/templates/page-container'; 8 | import colorfulTheme from '@src/theme'; 9 | 10 | interface PropsInterface { 11 | error?: { 12 | code: number; 13 | message?: string; 14 | }; 15 | } 16 | 17 | const useStyles = makeStyles((theme: Theme) => ({ 18 | root: { 19 | width: '100%', 20 | minhHeight: '100%', 21 | color: 'black', 22 | }, 23 | container: { 24 | paddingTop: theme.spacing(16), 25 | }, 26 | content: { 27 | '& > *': { 28 | marginBottom: theme.spacing(6), 29 | }, 30 | }, 31 | icon: { 32 | marginRight: theme.spacing(4), 33 | marginBottom: theme.spacing(3), 34 | }, 35 | headlineWrap: { 36 | alignItems: 'center', 37 | display: 'flex', 38 | }, 39 | })); 40 | 41 | export const PageError = (props: PropsInterface) => { 42 | const { t } = useTranslation(); 43 | const classes = useStyles(); 44 | 45 | const error = 46 | props.error === undefined 47 | ? { 48 | code: 400, 49 | message: t('error.somethingWentWrong'), 50 | } 51 | : props.error; 52 | 53 | return ( 54 |
    55 | 56 | 57 | 58 | 59 |
    60 | 66 | 67 | {t('error.code', { code: error.code })} 68 | 69 |
    70 | {error.message && ( 71 |
    72 | {error.message} 73 |
    74 | )} 75 |
    76 |
    77 |
    78 |
    79 |
    80 | ); 81 | }; 82 | -------------------------------------------------------------------------------- /src/components/features/errors/page-graphql-error.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Box } from '@mui/material'; 2 | 3 | import { GraphqlError } from '@src/components/shared/graphql-error'; 4 | import { PageContainer } from '@src/components/templates/page-container'; 5 | 6 | export const PageGraphqlError = (props: { error: any }) => ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /src/components/features/format-currency/format-currency.tsx: -------------------------------------------------------------------------------- 1 | import { useContentfulContext } from '@src/contentful-context'; 2 | 3 | interface FormatCurrencyProps { 4 | value: number; 5 | locale?: string; 6 | style?: string; 7 | currency?: string; 8 | } 9 | 10 | export const FormatCurrency = ({ 11 | value, 12 | locale, 13 | style = 'currency', 14 | currency = 'EUR', 15 | }: FormatCurrencyProps) => { 16 | const { locale: localeFromRouter } = useContentfulContext(); 17 | 18 | return ( 19 | <>{new Intl.NumberFormat(locale || localeFromRouter, { style, currency }).format(value)} 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/features/format-currency/index.ts: -------------------------------------------------------------------------------- 1 | export * from './format-currency'; 2 | -------------------------------------------------------------------------------- /src/components/features/language-selector/LanguageSelector.tsx: -------------------------------------------------------------------------------- 1 | import { MenuItem, Select, SvgIcon, Theme } from '@mui/material'; 2 | import { makeStyles } from '@mui/styles'; 3 | import { useRouter } from 'next/router'; 4 | 5 | const useStyles = makeStyles((theme: Theme) => ({ 6 | localeMenu: { 7 | alignItems: 'center', 8 | display: 'flex', 9 | '& > svg': { 10 | marginRight: theme.spacing(2), 11 | }, 12 | }, 13 | })); 14 | 15 | export const LanguageSelector = () => { 16 | const { locale, locales } = useRouter(); 17 | const classes = useStyles(); 18 | const router = useRouter(); 19 | 20 | const languageNames = new Intl.DisplayNames([], { 21 | type: 'language', 22 | }); 23 | 24 | return locales && locales.length > 1 ? ( 25 |
    26 | 27 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 53 |
    54 | ) : null; 55 | }; 56 | -------------------------------------------------------------------------------- /src/components/features/language-selector/index.ts: -------------------------------------------------------------------------------- 1 | export * from './LanguageSelector'; 2 | -------------------------------------------------------------------------------- /src/components/features/markdown.tsx: -------------------------------------------------------------------------------- 1 | import { Theme } from '@mui/material'; 2 | import { makeStyles } from '@mui/styles'; 3 | import clsx from 'clsx'; 4 | import React from 'react'; 5 | import parse from 'rehype-parse'; 6 | import rehypeReact from 'rehype-react'; 7 | import breaks from 'remark-breaks'; 8 | import { unified } from 'unified'; 9 | 10 | const useStyles = makeStyles((theme: Theme) => ({ 11 | root: { 12 | ...theme.typography.body1, 13 | ...theme.typography.body2, 14 | ...theme.typography.h2, 15 | ...theme.typography.h3, 16 | ...theme.typography.h4, 17 | ...theme.typography.h5, 18 | ...theme.typography.h6, 19 | '& p': { 20 | ...theme.typography.body1, 21 | }, 22 | '& a': { 23 | color: theme.palette.primary.main, 24 | textDecoration: 'none', 25 | }, 26 | '& li': { 27 | ...theme.typography.body1, 28 | marginBottom: theme.spacing(3), 29 | }, 30 | '& strong, b': { 31 | fontWeight: 600, 32 | }, 33 | }, 34 | })); 35 | 36 | const renderer = unified() 37 | .use(parse) 38 | .use(breaks) 39 | .use(rehypeReact, { createElement: React.createElement }); 40 | type Props = { 41 | text: string; 42 | className?: string; 43 | }; 44 | 45 | export const Markdown = (props: Props) => { 46 | const classes = useStyles(); 47 | return ( 48 |
    49 | {(renderer.processSync(props.text) as any).result} 50 |
    51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /src/components/features/page-link/__generated/category-link.generated.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../../../lib/__generated/graphql.types'; 2 | 3 | export type CategoryLinkFieldsFragment = { __typename: 'Category', slug?: string | null, categoryName?: string | null, sys: { __typename?: 'Sys', id: string } }; 4 | 5 | export const CategoryLinkFieldsFragmentDoc = ` 6 | fragment CategoryLinkFields on Category { 7 | __typename 8 | slug 9 | categoryName 10 | sys { 11 | id 12 | } 13 | categoryName 14 | } 15 | `; 16 | -------------------------------------------------------------------------------- /src/components/features/page-link/__generated/page-link.generated.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../../../lib/__generated/graphql.types'; 2 | 3 | export type PageLinkFieldsFragment = { __typename: 'Page', slug?: string | null, pageName?: string | null, sys: { __typename?: 'Sys', id: string }, pageContent?: { __typename: 'ComponentProductTable', sys: { __typename?: 'Sys', id: string } } | { __typename: 'TopicBusinessInfo', sys: { __typename?: 'Sys', id: string } } | { __typename: 'TopicProduct', sys: { __typename?: 'Sys', id: string } } | null }; 4 | 5 | export const PageLinkFieldsFragmentDoc = ` 6 | fragment PageLinkFields on Page { 7 | __typename 8 | slug 9 | sys { 10 | id 11 | } 12 | pageName 13 | pageContent(locale: $locale, preview: $preview) { 14 | ... on Entry { 15 | __typename 16 | sys { 17 | id 18 | } 19 | } 20 | } 21 | } 22 | `; -------------------------------------------------------------------------------- /src/components/features/page-link/__generated/post-link.generated.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../../../lib/__generated/graphql.types'; 2 | 3 | export type PostLinkFieldsFragment = { __typename: 'Post', slug?: string | null, postName?: string | null, sys: { __typename?: 'Sys', id: string } }; 4 | 5 | export const PostLinkFieldsFragmentDoc = ` 6 | fragment PostLinkFields on Post { 7 | __typename 8 | slug 9 | postName 10 | sys { 11 | id 12 | } 13 | } 14 | `; 15 | -------------------------------------------------------------------------------- /src/components/features/page-link/index.ts: -------------------------------------------------------------------------------- 1 | export * from './page-link' 2 | -------------------------------------------------------------------------------- /src/components/features/page-link/page-link.graphql: -------------------------------------------------------------------------------- 1 | fragment PageLinkFields on Page { 2 | __typename 3 | slug 4 | sys { 5 | id 6 | } 7 | pageName 8 | pageContent(locale: $locale, preview: $preview) { 9 | ... on Entry { 10 | __typename 11 | sys { 12 | id 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/features/page-link/page-link.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | import { PageLinkFieldsFragment } from '@src/components/features/page-link/__generated/page-link.generated'; 4 | import { Link, LinkProps } from '@src/components/shared/link'; 5 | 6 | export type PageLinkProps = Omit & { 7 | page: PageLinkFieldsFragment; 8 | render?: (pathname?: string) => ReactNode; 9 | children?: ReactNode; 10 | }; 11 | 12 | export const PageLink = (props: PageLinkProps) => { 13 | const pathname = props.page.slug ? `/${props.page.slug}` : ``; 14 | 15 | const linkProps = { 16 | href: pathname, 17 | className: props.className, 18 | onClick: props.onClick, 19 | withoutMaterial: props.withoutMaterial, 20 | underline: props.underline, 21 | isButton: props.isButton || false, 22 | variant: props.variant, 23 | size: props.size, 24 | color: props.color, 25 | endIcon: props.endIcon, 26 | urlParams: props.urlParams, 27 | }; 28 | 29 | return {props.render ? props.render(pathname) : props.children}; 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/features/section-headlines/index.ts: -------------------------------------------------------------------------------- 1 | export * from './section-headlines' 2 | -------------------------------------------------------------------------------- /src/components/features/section-headlines/section-headlines.tsx: -------------------------------------------------------------------------------- 1 | import type { InspectorModeTags } from '@contentful/live-preview/dist/inspectorMode/types'; 2 | import { Theme, Typography, TypographyProps } from '@mui/material'; 3 | import { makeStyles } from '@mui/styles'; 4 | import clsx from 'clsx'; 5 | import React from 'react'; 6 | 7 | import { Markdown } from '@src/components/features/markdown'; 8 | 9 | const useStyles = makeStyles((theme: Theme) => ({ 10 | containerCentered: { 11 | textAlign: 'center', 12 | }, 13 | headline: { 14 | fontSize: '2.25rem', 15 | fontWeight: 600, 16 | lineHeight: 1.083, 17 | }, 18 | subline: { 19 | fontWeight: 400, 20 | lineHeight: 1.56, 21 | marginTop: theme.spacing(6), 22 | fontSize: '1.8rem', 23 | color: '#414D63', 24 | }, 25 | 26 | text: { 27 | '& p': { 28 | fontSize: '2.5rem', 29 | lineHeight: 1.52, 30 | }, 31 | }, 32 | })); 33 | 34 | interface SectionHeadlinesPropsInterface { 35 | headline?: string | null; 36 | headlineProps?: TypographyProps; 37 | headlineLivePreviewProps?: InspectorModeTags; 38 | subline?: string | null; 39 | sublineProps?: TypographyProps; 40 | sublineLivePreviewProps?: InspectorModeTags; 41 | body?: string | null; 42 | align?: 'center' | 'left'; 43 | className?: string; 44 | } 45 | 46 | export const SectionHeadlines = (props: SectionHeadlinesPropsInterface) => { 47 | const { 48 | headline, 49 | headlineProps = {}, 50 | headlineLivePreviewProps = {}, 51 | subline, 52 | sublineProps = {}, 53 | sublineLivePreviewProps = {}, 54 | body, 55 | align = 'center', 56 | className = '', 57 | } = props; 58 | 59 | const classes = useStyles(); 60 | const computedHeadlineProps: TypographyProps & { component?: string } = { 61 | variant: 'h1', 62 | component: 'h2', 63 | ...headlineProps, 64 | ...headlineLivePreviewProps, 65 | className: clsx(headlineProps.className, classes.headline), 66 | }; 67 | const computedSublineProps: TypographyProps = { 68 | variant: 'h3', 69 | ...sublineProps, 70 | ...sublineLivePreviewProps, 71 | className: clsx(sublineProps.className, classes.subline), 72 | }; 73 | 74 | if (!headline && !subline && !body) { 75 | return null; 76 | } 77 | 78 | return ( 79 |
    80 | {headline && {headline}} 81 | {subline && {subline}} 82 | {body && } 83 |
    84 | ); 85 | }; 86 | -------------------------------------------------------------------------------- /src/components/features/settings/index.ts: -------------------------------------------------------------------------------- 1 | export * from './settings' 2 | -------------------------------------------------------------------------------- /src/components/features/settings/settings.tsx: -------------------------------------------------------------------------------- 1 | import { Theme, useTheme } from '@mui/material'; 2 | import { makeStyles } from '@mui/styles'; 3 | import React, { useState, useEffect } from 'react'; 4 | import { CSSTransition } from 'react-transition-group'; 5 | 6 | import { SettingsForm } from '@src/components/features/settings/settings-form'; 7 | import SettingsIcon from '@src/icons/settings-icon.svg'; 8 | 9 | const useStyles = makeStyles((theme: Theme) => ({ 10 | toggle: { 11 | alignItems: 'center', 12 | appearance: 'none', 13 | backgroundColor: '#192737', 14 | border: 0, 15 | borderRadius: '50%', 16 | bottom: theme.spacing(3), 17 | boxShadow: '0 2px 6px rgba(0,0,0,0.29)', 18 | cursor: 'pointer', 19 | display: 'flex', 20 | height: '6rem', 21 | justifyContent: 'center', 22 | position: 'fixed', 23 | right: theme.spacing(3), 24 | width: '6rem', 25 | zIndex: 1130, 26 | [theme.breakpoints.up('md')]: { 27 | bottom: theme.spacing(9), 28 | right: theme.spacing(9), 29 | }, 30 | }, 31 | toggleImage: { 32 | display: 'block', 33 | transform: 'translateX(-1px)', 34 | width: '3rem', 35 | }, 36 | animationEnter: { 37 | opacity: 0, 38 | transform: 'scale(0.1)', 39 | transformOrigin: 'bottom right', 40 | }, 41 | animationEnterActive: { 42 | opacity: 1, 43 | transform: 'translateX(0)', 44 | transition: 'opacity 300ms, transform 300ms', 45 | }, 46 | animationExit: { 47 | opacity: 1, 48 | }, 49 | animationExitActive: { 50 | opacity: 0, 51 | transform: 'scale(0.1)', 52 | transformOrigin: 'bottom right', 53 | transition: 'opacity 300ms, transform 300ms', 54 | }, 55 | })); 56 | 57 | export const Settings = () => { 58 | const classes = useStyles(); 59 | const theme = useTheme(); 60 | const [enabled, setEnabled] = useState(false); 61 | const [settingsOpen, setSettingsOpen] = useState(false); 62 | 63 | useEffect(() => { 64 | if (settingsOpen === false) { 65 | document.body.classList.remove('is-scroll-locked'); 66 | return; 67 | } 68 | 69 | if (window.matchMedia(theme.breakpoints.up('md').replace('@media ', '')).matches === true) { 70 | return; 71 | } 72 | 73 | document.body.classList.add('is-scroll-locked'); 74 | }, [settingsOpen, theme.breakpoints]); 75 | 76 | useEffect(() => { 77 | try { 78 | if (window.top?.location.href === window.location.href) { 79 | // Dont show the settings panel when embedded into an iframe (e.g. live preview) 80 | setEnabled(true); 81 | } 82 | } catch (err) { 83 | // window.top.location.href is not accessable for non same origin iframes 84 | } 85 | }, []); 86 | 87 | if (!enabled) { 88 | return null; 89 | } 90 | 91 | return ( 92 | <> 93 | 104 | { 106 | setSettingsOpen(false); 107 | }} 108 | /> 109 | 110 | 120 | 121 | ); 122 | }; 123 | -------------------------------------------------------------------------------- /src/components/shared/component-resolver.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@mui/material'; 2 | import React, { useMemo } from 'react'; 3 | 4 | import { useContentfulContext } from '@src/contentful-context'; 5 | import { componentGqlMap, componentMap } from '@src/mappings'; 6 | 7 | let previousComponent: string | null = null; 8 | interface Props { 9 | componentProps: { 10 | sys: { id: string }; 11 | __typename: string; 12 | [k: string]: any; 13 | }; 14 | 15 | /** 16 | * forces to do a graqhql request to get its content, instead 17 | * of expecting content is provided trough `props.componentProps`: 18 | */ 19 | forceGql?: boolean; 20 | 21 | className?: string; 22 | inline?: boolean; 23 | } 24 | 25 | export const ComponentResolver = (props: Props) => { 26 | const { componentProps, inline = false } = props; 27 | const { previewActive } = useContentfulContext(); 28 | 29 | const { locale } = useContentfulContext(); 30 | 31 | const ComponentGql = componentGqlMap[componentProps.__typename]; 32 | 33 | const shouldForceGql = useMemo(() => { 34 | if (props.forceGql === true) { 35 | return true; 36 | } 37 | 38 | if (!ComponentGql) { 39 | return false; 40 | } 41 | 42 | if (Object.keys(componentProps).length > 3) { 43 | // We expect components with no fragments set up to only contain 2 object 44 | // props. If there are more, it means we are providing fragments manually 45 | return false; 46 | } 47 | 48 | if (componentProps.__typename === undefined || componentProps.sys === undefined) { 49 | // We expect exactly these keys to be present in the returned props if the 50 | // fragment was not specified for this component 51 | return false; 52 | } 53 | 54 | return true; 55 | }, [ComponentGql, componentProps, props.forceGql]); 56 | 57 | const Component = !shouldForceGql && componentMap[componentProps.__typename]; 58 | 59 | const previousComponentProp = previousComponent; 60 | 61 | previousComponent = componentProps.__typename; 62 | 63 | if (!Component && !ComponentGql) { 64 | return null; 65 | } 66 | 67 | return ( 68 | 73 | {Component ? ( 74 | 80 | ) : ( 81 | 89 | )} 90 | 91 | ); 92 | }; 93 | -------------------------------------------------------------------------------- /src/components/shared/error-box.tsx: -------------------------------------------------------------------------------- 1 | import { Theme, Typography } from '@mui/material'; 2 | import { makeStyles } from '@mui/styles'; 3 | import clsx from 'clsx'; 4 | import React from 'react'; 5 | 6 | interface Props { 7 | className?: string; 8 | children?: any; 9 | } 10 | 11 | const useStyles = makeStyles((theme: Theme) => ({ 12 | errorBoxRoot: { 13 | color: theme.palette.error.dark, 14 | border: `1px solid ${theme.palette.error.dark}`, 15 | padding: theme.spacing(1), 16 | margin: theme.spacing(12, 0), 17 | }, 18 | })); 19 | 20 | export const ErrorBox = (props: Props) => { 21 | const classes = useStyles(); 22 | return ( 23 |
    24 | {props.children} 25 |
    26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/shared/graphql-error.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Theme, Typography } from '@mui/material'; 2 | import { useTheme } from '@mui/styles'; 3 | import React, { useMemo } from 'react'; 4 | 5 | import { tryget } from '@src/utils'; 6 | 7 | // TODO: add other errors than only `NetworkError` 8 | 9 | export const GraphqlError = (props: { error: any }) => { 10 | const { error } = props; 11 | console.error({ error }); 12 | const theme = useTheme(); 13 | 14 | const networkErrors = useMemo(() => tryget(() => error.networkError.result.errors), [error]); 15 | 16 | return ( 17 | 18 | {error.message} 19 | 20 | {networkErrors && ( 21 | 22 | Network Errors 23 | {networkErrors.map((err, i) => ( 24 | 25 | {err.message} 26 | 27 | ))} 28 | 29 | )} 30 | 31 | {error.graphQLErrors && error.graphQLErrors.length > 0 && ( 32 | 33 | GraphQl Errors 34 | {error.graphQLErrors.map((err, i) => ( 35 | 36 | {err.message} 37 | {`path: ${err.path.join('/')}`} 38 | 39 | ))} 40 | 41 | )} 42 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /src/components/shared/link.tsx: -------------------------------------------------------------------------------- 1 | import MuiButton from '@mui/material/Button'; 2 | import MuiLink from '@mui/material/Link'; 3 | import { makeStyles } from '@mui/styles'; 4 | import clsx from 'clsx'; 5 | import NextLink from 'next/link'; 6 | import { useRouter } from 'next/router'; 7 | import queryString from 'query-string'; 8 | import { ReactNode } from 'react'; 9 | 10 | const useStyles = makeStyles(() => ({ 11 | baseAnchor: { 12 | display: 'block', 13 | color: 'inherit', 14 | textDecoration: 'none', 15 | }, 16 | })); 17 | 18 | interface Props { 19 | children: ReactNode; 20 | href?: string; 21 | as?: string; 22 | target?: string; 23 | dropUrlParams?: boolean; 24 | className?: string; 25 | withoutMaterial?: boolean; 26 | underline?: boolean; 27 | onClick?: () => any; 28 | isButton?: boolean; 29 | variant?: 'text' | 'outlined' | 'contained' | undefined; 30 | size?: 'small' | 'medium' | 'large' | undefined; 31 | color?: any; 32 | startIcon?: any; 33 | endIcon?: any; 34 | urlParams?: string; 35 | title?: string; 36 | } 37 | export type LinkProps = Props; 38 | 39 | export const Link = (props: Props) => { 40 | const { 41 | dropUrlParams, 42 | className, 43 | children, 44 | withoutMaterial, 45 | underline, 46 | onClick, 47 | isButton = false, 48 | variant, 49 | size, 50 | color, 51 | startIcon, 52 | endIcon, 53 | urlParams = '', 54 | title, 55 | } = props; 56 | const router = useRouter(); 57 | let href = props.href || ''; 58 | let { as } = props; 59 | 60 | if (!dropUrlParams && router) { 61 | const urlQuerystring = router.asPath.split('?')[1]; 62 | if (urlQuerystring) { 63 | href += href.indexOf('?') < 0 ? `?${urlQuerystring}` : `&${urlQuerystring}`; 64 | } 65 | } 66 | 67 | if (urlParams !== '') { 68 | const parsedUrlParams = queryString.parse(urlParams); 69 | const parsedHref = queryString.parseUrl(href); 70 | 71 | const mergedParsedHref = { 72 | ...parsedHref, 73 | query: { 74 | ...parsedHref.query, 75 | ...parsedUrlParams, 76 | }, 77 | }; 78 | 79 | href = queryString.stringifyUrl(mergedParsedHref); 80 | 81 | if (as !== undefined) { 82 | const parsedAs = queryString.parseUrl(as); 83 | 84 | const mergedParsedAs = { 85 | ...parsedAs, 86 | query: { 87 | ...parsedAs.query, 88 | ...parsedUrlParams, 89 | }, 90 | }; 91 | 92 | as = queryString.stringifyUrl(mergedParsedAs); 93 | } 94 | } 95 | 96 | const classes = useStyles(); 97 | 98 | if (props.href === undefined || props.href === null) return <>{props.children}; 99 | 100 | const external = href.startsWith('http://') || href.startsWith('https://'); 101 | const underlineStyle = underline ? 'always' : 'none'; 102 | 103 | if (external === true || !href) { 104 | return isButton ? ( 105 | onClick && onClick()} 110 | variant={variant} 111 | size={size} 112 | startIcon={startIcon} 113 | endIcon={endIcon} 114 | title={title}> 115 | {children} 116 | 117 | ) : ( 118 | onClick && onClick()} 126 | title={title}> 127 | {children} 128 | 129 | ); 130 | } 131 | 132 | if (withoutMaterial === true) { 133 | return ( 134 | 135 | 136 | {children} 137 | 138 | 139 | ); 140 | } 141 | 142 | if (isButton === true) { 143 | return ( 144 | 145 | onClick && onClick()} 150 | variant={variant} 151 | size={size} 152 | startIcon={startIcon} 153 | endIcon={endIcon} 154 | title={title}> 155 | {children} 156 | 157 | 158 | ); 159 | } 160 | 161 | return ( 162 | 163 | onClick && onClick()} 169 | title={title}> 170 | {children} 171 | 172 | 173 | ); 174 | }; 175 | -------------------------------------------------------------------------------- /src/components/templates/header/header.tsx: -------------------------------------------------------------------------------- 1 | import Menu from '@mui/icons-material/Menu'; 2 | import { AppBar, Container, IconButton, Theme, Toolbar, Box } from '@mui/material'; 3 | import { makeStyles } from '@mui/styles'; 4 | import { useTranslation } from 'next-i18next'; 5 | 6 | import { CtfNavigationGql } from '@src/components/features/ctf-components/ctf-navigation/ctf-navigation-gql'; 7 | import { Link } from '@src/components/shared/link'; 8 | import Logo from '@src/icons/colorful-coin-logo.svg'; 9 | import { HEADER_HEIGHT, HEADER_HEIGHT_MD, CONTAINER_WIDTH } from '@src/theme'; 10 | 11 | const useStyles = makeStyles((theme: Theme) => ({ 12 | appbar: { 13 | boxShadow: '0 2px 6px #00000021', 14 | }, 15 | toolbar: { 16 | height: HEADER_HEIGHT_MD, 17 | [theme.breakpoints.up('md')]: { 18 | height: HEADER_HEIGHT, 19 | }, 20 | }, 21 | toolbarContent: { 22 | alignItems: 'center', 23 | display: 'flex', 24 | flexDirection: 'row', 25 | height: '100%', 26 | justifyContent: 'space-between', 27 | }, 28 | logo: { 29 | display: 'block', 30 | maxWidth: '120px', 31 | height: 'auto', 32 | }, 33 | menuWrapper: { 34 | alignItems: 'center', 35 | display: 'flex', 36 | }, 37 | accountMenu: { 38 | alignItems: 'center', 39 | display: 'flex', 40 | listStyle: 'none', 41 | margin: 0, 42 | padding: 0, 43 | }, 44 | 45 | accountMenuItem: { 46 | '& + &': { 47 | marginLeft: theme.spacing(8), 48 | 49 | [theme.breakpoints.up('lg')]: { 50 | marginLeft: theme.spacing(10), 51 | }, 52 | }, 53 | '& .MuiButton-startIcon': { 54 | marginRight: '0.4rem', 55 | }, 56 | '& .MuiButton-iconSizeSmall > :first-child': { 57 | fontSize: '1.5rem', 58 | }, 59 | }, 60 | corporateLogo: { 61 | display: 'block', 62 | height: 'auto', 63 | width: '113px', 64 | }, 65 | })); 66 | 67 | interface HeaderPropsInterface { 68 | isMenuOpen?: boolean; 69 | onMenuClick?: () => any; 70 | } 71 | 72 | export const Header = (props: HeaderPropsInterface) => { 73 | const { t } = useTranslation(); 74 | 75 | const { onMenuClick, isMenuOpen } = props; 76 | const classes = useStyles(); 77 | 78 | return ( 79 | 80 | 81 | 88 | 89 | 90 | 91 | 92 |
    93 | 94 |
    95 |
    96 |
    97 | 98 | {/* menu button */} 99 | 100 | onMenuClick?.()} 103 | aria-controls="mobile-menu" 104 | aria-expanded={isMenuOpen} 105 | aria-haspopup="dialog"> 106 | 107 | 108 | 109 | 110 | 111 | ); 112 | }; 113 | -------------------------------------------------------------------------------- /src/components/templates/header/index.ts: -------------------------------------------------------------------------------- 1 | export * from './header' 2 | -------------------------------------------------------------------------------- /src/components/templates/layout/index.ts: -------------------------------------------------------------------------------- 1 | export * from './layout' 2 | -------------------------------------------------------------------------------- /src/components/templates/layout/layout.tsx: -------------------------------------------------------------------------------- 1 | import { CssBaseline, Theme } from '@mui/material'; 2 | import { makeStyles } from '@mui/styles'; 3 | import { useRouter } from 'next/router'; 4 | import React, { useState, useEffect, ReactElement } from 'react'; 5 | 6 | import { Header } from '../header'; 7 | 8 | import { CtfFooterGql } from '@src/components/features/ctf-components/ctf-footer/ctf-footer-gql'; 9 | import { CtfMobileMenuGql } from '@src/components/features/ctf-components/ctf-mobile-menu/ctf-mobile-menu-gql'; 10 | 11 | const useStyles = makeStyles((theme: Theme) => ({ 12 | content: { 13 | ...theme.typography.body1, 14 | flex: '1 0 auto', 15 | display: 'flex', 16 | flexDirection: 'column', 17 | alignItems: 'center', 18 | }, 19 | })); 20 | 21 | interface LayoutPropsInterface { 22 | preview: boolean; 23 | children: ReactElement[]; 24 | } 25 | 26 | export const Layout: React.FC = ({ children }) => { 27 | const [isMenuOpen, setMenuOpen] = useState(false); 28 | const classes = useStyles(); 29 | const router = useRouter(); 30 | 31 | useEffect(() => { 32 | router.events.on('routeChangeStart', () => { 33 | setMenuOpen(false); 34 | }); 35 | 36 | router.events.on('routeChangeComplete', () => { 37 | if (document.activeElement === null) { 38 | return; 39 | } 40 | 41 | if (document.activeElement instanceof HTMLElement) { 42 | document.activeElement.blur(); 43 | } 44 | }); 45 | }, [router.events]); 46 | 47 | return ( 48 | <> 49 | 50 | {/* header */} 51 |
    setMenuOpen(true)} /> 52 | 53 | {/* content */} 54 |
    {children}
    55 | 56 | {/* footer */} 57 | 58 | 59 | {/* mobile menu */} 60 | { 63 | setMenuOpen(newOpen); 64 | }} 65 | /> 66 | 67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /src/components/templates/page-container/index.ts: -------------------------------------------------------------------------------- 1 | export * from './page-container' 2 | -------------------------------------------------------------------------------- /src/components/templates/page-container/page-container.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from '@mui/material/styles/createTypography'; 2 | import { makeStyles } from '@mui/styles'; 3 | import clsx from 'clsx'; 4 | import React from 'react'; 5 | 6 | const useStyles = makeStyles(() => ({ 7 | pageContainerRoot: { 8 | width: '100%', 9 | }, 10 | })); 11 | 12 | type Props = { 13 | className?: string; 14 | style?: CSSProperties; 15 | children?: React.ReactNode | React.ReactNode[]; 16 | }; 17 | 18 | export const PageContainer = (props: Props) => { 19 | const classes = useStyles(); 20 | return ( 21 |
    22 | {props.children} 23 |
    24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/contentful-context.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, createContext } from 'react'; 2 | 3 | import contentfulConfig from 'contentful.config'; 4 | import i18nConfig from 'next-i18next.config.js'; 5 | const { i18n } = i18nConfig; 6 | 7 | export interface ContentfulContextInterface { 8 | locale: string; 9 | spaceIds: { 10 | main: string; 11 | }; 12 | previewActive: boolean; 13 | } 14 | 15 | export const contentfulContextValue: ContentfulContextInterface = { 16 | locale: i18n.defaultLocale, 17 | spaceIds: { 18 | main: contentfulConfig.contentful.space_id, 19 | }, 20 | previewActive: false, 21 | }; 22 | 23 | export const ContentfulContext = createContext(contentfulContextValue); 24 | 25 | export const useContentfulContext = () => useContext(ContentfulContext); 26 | 27 | const ContentfulContentProvider = ({ children, router }) => { 28 | const previewActive = !!router.query.preview; 29 | 30 | return ( 31 | 40 | {children} 41 | 42 | ); 43 | }; 44 | 45 | export { ContentfulContentProvider }; 46 | -------------------------------------------------------------------------------- /src/icons/settings-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/layout-context.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const defaultLayout = { 4 | containerWidth: 770, 5 | parent: '', 6 | }; 7 | 8 | const LayoutContext = React.createContext(defaultLayout); 9 | 10 | export const useLayoutContext = () => React.useContext(LayoutContext); 11 | 12 | export default LayoutContext; 13 | -------------------------------------------------------------------------------- /src/lib/fetchConfig.ts: -------------------------------------------------------------------------------- 1 | export const fetchConfig = { 2 | endpoint: `https://graphql.contentful.com/content/v1/spaces/${String( 3 | process.env.CONTENTFUL_SPACE_ID, 4 | )}`, 5 | params: { 6 | headers: { 7 | 'Content-Type': 'application/json', 8 | Authorization: `Bearer ${process.env.CONTENTFUL_ACCESS_TOKEN}`, 9 | }, 10 | }, 11 | previewParams: { 12 | headers: { 13 | 'Content-Type': 'application/json', 14 | Authorization: `Bearer ${process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN}`, 15 | }, 16 | }, 17 | }; 18 | 19 | export function customFetcher( 20 | query: string, 21 | variables?: TVariables, 22 | options?: RequestInit['headers'], 23 | ) { 24 | return async (): Promise => { 25 | const res = await fetch(fetchConfig.endpoint as string, { 26 | method: 'POST', 27 | ...options, 28 | ...(variables?.preview ? fetchConfig.previewParams : fetchConfig.params), 29 | body: JSON.stringify({ query, variables }), 30 | }); 31 | 32 | const json = await res.json(); 33 | 34 | if (json.errors) { 35 | const { message } = json.errors[0]; 36 | 37 | throw new Error(message); 38 | } 39 | 40 | return json.data; 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/get-serverside-translations.ts: -------------------------------------------------------------------------------- 1 | import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; 2 | 3 | import i18nConfig from 'next-i18next.config'; 4 | 5 | const { i18n } = i18nConfig; 6 | 7 | export const getServerSideTranslations = (locale?: string) => { 8 | return serverSideTranslations(locale || i18n.defaultLocale, ['common']); 9 | }; 10 | -------------------------------------------------------------------------------- /src/lib/gql-client.ts: -------------------------------------------------------------------------------- 1 | import { QueryCache } from '@tanstack/query-core'; 2 | import Router from 'next/router'; 3 | 4 | export const queryConfig = { 5 | queryCache: new QueryCache({ 6 | onError: () => { 7 | Router.push({ pathname: '/404' }); 8 | }, 9 | }), 10 | defaultOptions: { 11 | queries: { 12 | retry: false, 13 | refetchOnMount: false, 14 | refetchIntervalInBackground: false, 15 | refetchOnWindowFocus: false, 16 | }, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /src/lib/prefetch-mappings.ts: -------------------------------------------------------------------------------- 1 | import { useCtfBusinessInfoQuery } from '@src/components/features/ctf-components/ctf-business-info/__generated/business-info.generated'; 2 | import { useCtfCtaQuery } from '@src/components/features/ctf-components/ctf-cta/__generated/ctf-cta.generated'; 3 | import { useCtfDuplexQuery } from '@src/components/features/ctf-components/ctf-duplex/__generated/ctf-duplex.generated'; 4 | import { useCtfHeroBannerQuery } from '@src/components/features/ctf-components/ctf-hero-banner/__generated/ctf-hero-banner.generated'; 5 | import { useCtfInfoBlockQuery } from '@src/components/features/ctf-components/ctf-info-block/__generated/ctf-info-block.generated'; 6 | import { useCtfPersonQuery } from '@src/components/features/ctf-components/ctf-person/__generated/ctf-person.generated'; 7 | import { useCtfProductQuery } from '@src/components/features/ctf-components/ctf-product/__generated/ctf-product.generated'; 8 | import { useCtfProductTableQuery } from '@src/components/features/ctf-components/ctf-product-table/__generated/ctf-product-table.generated'; 9 | import { useCtfQuoteQuery } from '@src/components/features/ctf-components/ctf-quote/__generated/ctf-quote.generated'; 10 | import { useCtfTextBlockQuery } from '@src/components/features/ctf-components/ctf-text-block/__generated/ctf-text-block.generated'; 11 | import { 12 | CtfBusinessInfoQuery, 13 | CtfPersonQuery, 14 | CtfProductQuery, 15 | } from '@src/lib/__generated/graphql.types'; 16 | 17 | export type PrefetchMappingTypeFetcher = CtfBusinessInfoQuery | CtfPersonQuery | CtfProductQuery; 18 | 19 | /** 20 | * This map is used to match a generated GQL query to a Contentful model's __typename. The query is used to prefetch the data through React Query's prefetchQuery method 21 | */ 22 | export const prefetchMap = { 23 | ComponentCta: useCtfCtaQuery, 24 | ComponentHeroBanner: useCtfHeroBannerQuery, 25 | ComponentDuplex: useCtfDuplexQuery, 26 | ComponentInfoBlock: useCtfInfoBlockQuery, 27 | ComponentTextBlock: useCtfTextBlockQuery, 28 | ComponentQuote: useCtfQuoteQuery, 29 | ComponentProductTable: useCtfProductTableQuery, 30 | TopicBusinessInfo: useCtfBusinessInfoQuery, 31 | TopicProduct: useCtfProductQuery, 32 | TopicPerson: useCtfPersonQuery, 33 | }; 34 | -------------------------------------------------------------------------------- /src/lib/prefetch-promise-array.ts: -------------------------------------------------------------------------------- 1 | import { prefetchMap } from '@src/lib/prefetch-mappings'; 2 | 3 | /** 4 | * Create an array of prefetchQuery functions that can be awaited in our pages to prefetch React Query calls 5 | * @param inputArr 6 | * @param queryClient 7 | * @param locale 8 | */ 9 | export const prefetchPromiseArr = ({ inputArr, queryClient, locale }) => 10 | inputArr?.map(item => { 11 | if (!item) return; 12 | 13 | const { __typename, sys } = item; 14 | 15 | if (!__typename) return; 16 | 17 | const query = prefetchMap?.[__typename]; 18 | 19 | if (!query) return; 20 | 21 | return queryClient.prefetchQuery( 22 | query.getKey({ id: sys.id, locale, preview: false }), 23 | query.fetcher({ id: sys.id, locale, preview: false }), 24 | ); 25 | }) || []; 26 | -------------------------------------------------------------------------------- /src/lib/shared-fragments/__generated/ctf-componentMap.generated.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../__generated/graphql.types'; 2 | 3 | export type ComponentReferenceFields_ComponentCta_Fragment = { __typename: 'ComponentCta', sys: { __typename?: 'Sys', id: string } }; 4 | 5 | export type ComponentReferenceFields_ComponentDuplex_Fragment = { __typename: 'ComponentDuplex', sys: { __typename?: 'Sys', id: string } }; 6 | 7 | export type ComponentReferenceFields_ComponentHeroBanner_Fragment = { __typename: 'ComponentHeroBanner', sys: { __typename?: 'Sys', id: string } }; 8 | 9 | export type ComponentReferenceFields_ComponentInfoBlock_Fragment = { __typename: 'ComponentInfoBlock', sys: { __typename?: 'Sys', id: string } }; 10 | 11 | export type ComponentReferenceFields_ComponentProductTable_Fragment = { __typename: 'ComponentProductTable', sys: { __typename?: 'Sys', id: string } }; 12 | 13 | export type ComponentReferenceFields_ComponentQuote_Fragment = { __typename: 'ComponentQuote', sys: { __typename?: 'Sys', id: string } }; 14 | 15 | export type ComponentReferenceFields_ComponentTextBlock_Fragment = { __typename: 'ComponentTextBlock', sys: { __typename?: 'Sys', id: string } }; 16 | 17 | export type ComponentReferenceFields_FooterMenu_Fragment = { __typename: 'FooterMenu', sys: { __typename?: 'Sys', id: string } }; 18 | 19 | export type ComponentReferenceFields_MenuGroup_Fragment = { __typename: 'MenuGroup', sys: { __typename?: 'Sys', id: string } }; 20 | 21 | export type ComponentReferenceFields_NavigationMenu_Fragment = { __typename: 'NavigationMenu', sys: { __typename?: 'Sys', id: string } }; 22 | 23 | export type ComponentReferenceFields_Page_Fragment = { __typename: 'Page', sys: { __typename?: 'Sys', id: string } }; 24 | 25 | export type ComponentReferenceFields_Seo_Fragment = { __typename: 'Seo', sys: { __typename?: 'Sys', id: string } }; 26 | 27 | export type ComponentReferenceFields_TopicBusinessInfo_Fragment = { __typename: 'TopicBusinessInfo', sys: { __typename?: 'Sys', id: string } }; 28 | 29 | export type ComponentReferenceFields_TopicPerson_Fragment = { __typename: 'TopicPerson', sys: { __typename?: 'Sys', id: string } }; 30 | 31 | export type ComponentReferenceFields_TopicProduct_Fragment = { __typename: 'TopicProduct', sys: { __typename?: 'Sys', id: string } }; 32 | 33 | export type ComponentReferenceFields_TopicProductFeature_Fragment = { __typename: 'TopicProductFeature', sys: { __typename?: 'Sys', id: string } }; 34 | 35 | export type ComponentReferenceFieldsFragment = ComponentReferenceFields_ComponentCta_Fragment | ComponentReferenceFields_ComponentDuplex_Fragment | ComponentReferenceFields_ComponentHeroBanner_Fragment | ComponentReferenceFields_ComponentInfoBlock_Fragment | ComponentReferenceFields_ComponentProductTable_Fragment | ComponentReferenceFields_ComponentQuote_Fragment | ComponentReferenceFields_ComponentTextBlock_Fragment | ComponentReferenceFields_FooterMenu_Fragment | ComponentReferenceFields_MenuGroup_Fragment | ComponentReferenceFields_NavigationMenu_Fragment | ComponentReferenceFields_Page_Fragment | ComponentReferenceFields_Seo_Fragment | ComponentReferenceFields_TopicBusinessInfo_Fragment | ComponentReferenceFields_TopicPerson_Fragment | ComponentReferenceFields_TopicProduct_Fragment | ComponentReferenceFields_TopicProductFeature_Fragment; 36 | 37 | export const ComponentReferenceFieldsFragmentDoc = ` 38 | fragment ComponentReferenceFields on Entry { 39 | __typename 40 | sys { 41 | id 42 | } 43 | } 44 | `; -------------------------------------------------------------------------------- /src/lib/shared-fragments/__generated/ctf-menuGroup.generated.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../../__generated/graphql.types'; 2 | 3 | import { PageLinkFieldsFragment } from '../../../components/features/page-link/__generated/page-link.generated'; 4 | import { PageLinkFieldsFragmentDoc } from '../../../components/features/page-link/__generated/page-link.generated'; 5 | export type MenuGroupFieldsFragment = { __typename?: 'MenuGroupFeaturedPagesCollection', items: Array<( 6 | { __typename?: 'Page' } 7 | & PageLinkFieldsFragment 8 | ) | null> }; 9 | 10 | export const MenuGroupFieldsFragmentDoc = ` 11 | fragment MenuGroupFields on MenuGroupFeaturedPagesCollection { 12 | items { 13 | ...PageLinkFields 14 | } 15 | } 16 | `; -------------------------------------------------------------------------------- /src/lib/shared-fragments/ctf-componentMap.graphql: -------------------------------------------------------------------------------- 1 | fragment ComponentReferenceFields on Entry { 2 | __typename 3 | sys { 4 | id 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/shared-fragments/ctf-menuGroup.graphql: -------------------------------------------------------------------------------- 1 | fragment MenuGroupFields on MenuGroupFeaturedPagesCollection { 2 | items { 3 | ...PageLinkFields 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/mappings.ts: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic'; 2 | 3 | const pageTopicMap = { 4 | TopicProduct: dynamic(() => 5 | import('@src/components/features/ctf-components/ctf-product/ctf-product-gql').then(module => module.CtfProductGql), 6 | ), 7 | TopicBusinessInfo: dynamic(() => 8 | import('@src/components/features/ctf-components/ctf-business-info/ctf-business-info-gql').then( 9 | module => module.CtfBusinessInfoGql, 10 | ), 11 | ), 12 | ComponentProductTable: dynamic(() => 13 | import('@src/components/features/ctf-components/ctf-product-table/ctf-product-table-gql').then( 14 | module => module.CtfProductTableGql, 15 | ), 16 | ), 17 | }; 18 | 19 | export const componentMap = { 20 | ComponentCta: dynamic(() => 21 | import('@src/components/features/ctf-components/ctf-cta/ctf-cta').then(module => module.CtfCta), 22 | ), 23 | ComponentDuplex: dynamic(() => 24 | import('@src/components/features/ctf-components/ctf-duplex/ctf-duplex').then(module => module.CtfDuplex), 25 | ), 26 | ComponentHeroBanner: dynamic(() => 27 | import('@src/components/features/ctf-components/ctf-hero-banner/ctf-hero-banner').then(module => module.CtfHeroBanner), 28 | ), 29 | ComponentInfoBlock: dynamic(() => 30 | import('@src/components/features/ctf-components/ctf-info-block/ctf-info-block').then(module => module.CtfInfoBlock), 31 | ), 32 | ComponentQuote: dynamic(() => 33 | import('@src/components/features/ctf-components/ctf-quote/ctf-quote').then(module => module.CtfQuote), 34 | ), 35 | ComponentTextBlock: dynamic(() => 36 | import('@src/components/features/ctf-components/ctf-text-block/ctf-text-block').then(module => module.CtfTextBlock), 37 | ), 38 | TopicPerson: dynamic(() => 39 | import('@src/components/features/ctf-components/ctf-person/ctf-person').then(module => module.CtfPerson), 40 | ), 41 | }; 42 | 43 | export const componentGqlMap = { 44 | ...pageTopicMap, 45 | ComponentCta: dynamic(() => 46 | import('@src/components/features/ctf-components/ctf-cta/ctf-cta-gql').then(module => module.CtfCtaGql), 47 | ), 48 | ComponentDuplex: dynamic(() => 49 | import('@src/components/features/ctf-components/ctf-duplex/ctf-duplex-gql').then(module => module.CtfDuplexGql), 50 | ), 51 | ComponentHeroBanner: dynamic(() => 52 | import('@src/components/features/ctf-components/ctf-hero-banner/ctf-hero-banner-gql').then( 53 | module => module.CtfHeroGql, 54 | ), 55 | ), 56 | ComponentInfoBlock: dynamic(() => 57 | import('@src/components/features/ctf-components/ctf-info-block/ctf-info-block-gql').then( 58 | module => module.CtfInfoBlockGql, 59 | ), 60 | ), 61 | ComponentQuote: dynamic(() => 62 | import('@src/components/features/ctf-components/ctf-quote/ctf-quote-gql').then(module => module.CtfQuoteGql), 63 | ), 64 | ComponentTextBlock: dynamic(() => 65 | import('@src/components/features/ctf-components/ctf-text-block/ctf-text-block-gql').then( 66 | module => module.CtfTextBlockGql, 67 | ), 68 | ), 69 | TopicPerson: dynamic(() => 70 | import('@src/components/features/ctf-components/ctf-person/ctf-person-gql').then(module => module.CtfPersonGql), 71 | ), 72 | ComponentFooter: dynamic(() => 73 | import('@src/components/features/ctf-components/ctf-footer/ctf-footer-gql').then(module => module.CtfFooterGql), 74 | ), 75 | }; 76 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { dehydrate, QueryClient } from '@tanstack/react-query'; 2 | import { GetStaticProps } from 'next'; 3 | 4 | import { useCtfFooterQuery } from '@src/components/features/ctf-components/ctf-footer/__generated/ctf-footer.generated'; 5 | import { useCtfNavigationQuery } from '@src/components/features/ctf-components/ctf-navigation/__generated/ctf-navigation.generated'; 6 | import { PageError } from '@src/components/features/errors/page-error'; 7 | import { getServerSideTranslations } from '@src/lib/get-serverside-translations'; 8 | 9 | const ErrorPage404 = () => { 10 | return ; 11 | }; 12 | 13 | export const getStaticProps: GetStaticProps = async ({ locale }) => { 14 | const queryClient = new QueryClient(); 15 | 16 | await queryClient.prefetchQuery( 17 | useCtfNavigationQuery.getKey({ locale, preview: false }), 18 | useCtfNavigationQuery.fetcher({ locale, preview: false }), 19 | ); 20 | await queryClient.prefetchQuery( 21 | useCtfFooterQuery.getKey({ locale, preview: false }), 22 | useCtfFooterQuery.fetcher({ locale, preview: false }), 23 | ); 24 | 25 | return { 26 | props: { 27 | ...(await getServerSideTranslations(locale)), 28 | dehydratedState: dehydrate(queryClient), 29 | }, 30 | }; 31 | }; 32 | 33 | export default ErrorPage404; 34 | -------------------------------------------------------------------------------- /src/pages/[slug].tsx: -------------------------------------------------------------------------------- 1 | import { dehydrate, QueryClient } from '@tanstack/react-query'; 2 | import { NextPage, NextPageContext } from 'next'; 3 | import { useRouter } from 'next/router'; 4 | 5 | import { useCtfFooterQuery } from '@src/components/features/ctf-components/ctf-footer/__generated/ctf-footer.generated'; 6 | import { useCtfNavigationQuery } from '@src/components/features/ctf-components/ctf-navigation/__generated/ctf-navigation.generated'; 7 | import { useCtfPageQuery } from '@src/components/features/ctf-components/ctf-page/__generated/ctf-page.generated'; 8 | import CtfPageGgl from '@src/components/features/ctf-components/ctf-page/ctf-page-gql'; 9 | import { ComponentReferenceFieldsFragment } from '@src/lib/__generated/graphql.types'; 10 | import { getServerSideTranslations } from '@src/lib/get-serverside-translations'; 11 | import { prefetchMap, PrefetchMappingTypeFetcher } from '@src/lib/prefetch-mappings'; 12 | import { prefetchPromiseArr } from '@src/lib/prefetch-promise-array'; 13 | 14 | const SlugPage: NextPage = () => { 15 | const router = useRouter(); 16 | const slug = (router?.query.slug as string) || ''; 17 | 18 | return ; 19 | }; 20 | 21 | export interface CustomNextPageContext extends NextPageContext { 22 | params: { 23 | slug: string; 24 | }; 25 | id: string; 26 | } 27 | 28 | export const getServerSideProps = async ({ locale, params, query }: CustomNextPageContext) => { 29 | const slug = params.slug; 30 | const preview = Boolean(query.preview); 31 | 32 | try { 33 | const queryClient = new QueryClient(); 34 | 35 | // Default queries 36 | const prefetchPromises = [ 37 | queryClient.prefetchQuery( 38 | useCtfPageQuery.getKey({ slug, locale, preview }), 39 | useCtfPageQuery.fetcher({ slug, locale, preview }), 40 | ), 41 | queryClient.prefetchQuery( 42 | useCtfNavigationQuery.getKey({ locale, preview }), 43 | useCtfNavigationQuery.fetcher({ locale, preview }), 44 | ), 45 | queryClient.prefetchQuery( 46 | useCtfFooterQuery.getKey({ locale, preview }), 47 | useCtfFooterQuery.fetcher({ locale, preview }), 48 | ), 49 | ]; 50 | // Dynamic queries 51 | const pageData = await useCtfPageQuery.fetcher({ slug, locale, preview })(); 52 | const page = pageData.pageCollection?.items[0]; 53 | 54 | const topSection = page?.topSectionCollection?.items; 55 | const extraSection = page?.extraSectionCollection?.items; 56 | const content: ComponentReferenceFieldsFragment | undefined | null = page?.pageContent; 57 | 58 | await Promise.all([ 59 | ...prefetchPromises, 60 | ...prefetchPromiseArr({ inputArr: topSection, locale, queryClient }), 61 | ...prefetchPromiseArr({ inputArr: extraSection, locale, queryClient }), 62 | ...prefetchPromiseArr({ inputArr: [content], locale, queryClient }), 63 | ]); 64 | 65 | if (content) { 66 | const { __typename, sys } = content; 67 | 68 | if (!__typename) 69 | return { 70 | notFound: true, 71 | }; 72 | 73 | const query = prefetchMap?.[__typename]; 74 | 75 | if (!query) 76 | return { 77 | notFound: true, 78 | }; 79 | 80 | const data: PrefetchMappingTypeFetcher = await query.fetcher({ 81 | id: sys.id, 82 | locale, 83 | preview, 84 | })(); 85 | 86 | // Different data structured can be returned, this function makes sure the correct data is returned 87 | const inputArr = (__typename => { 88 | if ('topicBusinessInfo' in data) { 89 | return data?.topicBusinessInfo?.body?.links.entries.block; 90 | } 91 | 92 | if ('topicPerson' in data) { 93 | return [data?.topicPerson]; 94 | } 95 | 96 | if ('topicProduct' in data) { 97 | return [data?.topicProduct]; 98 | } 99 | 100 | return []; 101 | })(); 102 | 103 | await Promise.all([ 104 | ...prefetchPromiseArr({ 105 | inputArr, 106 | locale, 107 | queryClient, 108 | }), 109 | ]); 110 | } 111 | 112 | return { 113 | props: { 114 | ...(await getServerSideTranslations(locale)), 115 | dehydratedState: dehydrate(queryClient), 116 | }, 117 | }; 118 | } catch { 119 | return { 120 | notFound: true, 121 | }; 122 | } 123 | }; 124 | 125 | export default SlugPage; 126 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { ContentfulLivePreviewProvider } from '@contentful/live-preview/react'; 2 | import { ThemeProvider, StyledEngineProvider } from '@mui/material/styles'; 3 | import { DehydratedState, Hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query'; 4 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; 5 | import { AppProps } from 'next/app'; 6 | import Head from 'next/head'; 7 | import { appWithTranslation, SSRConfig } from 'next-i18next'; 8 | import { useEffect, useState } from 'react'; 9 | 10 | import { Settings } from '@src/components/features/settings'; 11 | import { Layout } from '@src/components/templates/layout/layout'; 12 | import { useContentfulContext, ContentfulContentProvider } from '@src/contentful-context'; 13 | import { queryConfig } from '@src/lib/gql-client'; 14 | import colorfulTheme from '@src/theme'; 15 | import contentfulConfig from 'contentful.config'; 16 | import nextI18nConfig from 'next-i18next.config'; 17 | 18 | const LivePreviewProvider = ({ children }) => { 19 | const { previewActive, locale } = useContentfulContext(); 20 | 21 | return ( 22 | 27 | {children} 28 | 29 | ); 30 | }; 31 | 32 | type CustomPageProps = SSRConfig & { dehydratedState: DehydratedState; err: Error }; 33 | 34 | const CustomApp = ({ 35 | Component, 36 | router, 37 | pageProps: originalPageProps, 38 | }: AppProps) => { 39 | const [queryClient] = useState(() => new QueryClient(queryConfig)); 40 | const { dehydratedState, err, ...pageProps } = originalPageProps; 41 | const { previewActive } = useContentfulContext(); 42 | 43 | useEffect(() => { 44 | // when component is mounting we remove server side rendered css: 45 | const jssStyles = document.querySelector('#jss-server-side'); 46 | 47 | if (jssStyles && jssStyles.parentNode) { 48 | jssStyles.parentNode.removeChild(jssStyles); 49 | } 50 | 51 | router.events.on('routeChangeComplete', () => null); 52 | 53 | return () => { 54 | router.events.off('routeChangeComplete', () => null); 55 | }; 56 | }, [router.events]); 57 | 58 | return ( 59 | <> 60 | 61 | 65 | {contentfulConfig.meta.title} 66 | 67 | 68 | 69 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | ); 99 | }; 100 | 101 | export default appWithTranslation(CustomApp, nextI18nConfig); 102 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-danger */ 2 | // eslint-disable-next-line import/no-extraneous-dependencies 3 | import { ServerStyleSheets } from '@mui/styles'; 4 | import Document, { DocumentContext, Head, Main, NextScript, Html } from 'next/document'; 5 | import React from 'react'; 6 | // eslint-disable-next-line import/no-extraneous-dependencies 7 | import flush from 'styled-jsx'; 8 | 9 | import colorfulTheme from '@src/theme'; 10 | 11 | export default class CustomDocument extends Document { 12 | render() { 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | {/* PWA primary color */} 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
    34 | 35 | 36 | 37 | ); 38 | } 39 | } 40 | 41 | // eslint-disable-next-line func-names 42 | CustomDocument.getInitialProps = async function (ctx: DocumentContext) { 43 | // Resolution order 44 | // 45 | // On the server: 46 | // 1. app.getInitialProps 47 | // 2. page.getInitialProps 48 | // 3. document.getInitialProps 49 | // 4. app.render 50 | // 5. page.render 51 | // 6. document.render 52 | // 53 | // On the server with error: 54 | // 1. document.getInitialProps 55 | // 2. app.render 56 | // 3. page.render 57 | // 4. document.render 58 | // 59 | // On the client 60 | // 1. app.getInitialProps 61 | // 2. page.getInitialProps 62 | // 3. app.render 63 | // 4. page.render 64 | 65 | // Render app and page and get the context of the page with collected side effects. 66 | const sheets = new ServerStyleSheets(); 67 | const originalRenderPage = ctx.renderPage; 68 | 69 | ctx.renderPage = () => 70 | originalRenderPage({ 71 | enhanceApp: App => props => sheets.collect(), 72 | }); 73 | 74 | const initialProps = await Document.getInitialProps(ctx); 75 | 76 | return { 77 | ...initialProps, 78 | locale: ctx.locale, 79 | styles: ( 80 | <> 81 | {sheets.getStyleElement()} 82 | {flush.createStyleRegistry().styles() || null} 83 | 84 | ), 85 | }; 86 | }; 87 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { dehydrate, QueryClient } from '@tanstack/react-query'; 2 | import { NextPage, NextPageContext } from 'next'; 3 | 4 | import { useCtfFooterQuery } from '@src/components/features/ctf-components/ctf-footer/__generated/ctf-footer.generated'; 5 | import { useCtfNavigationQuery } from '@src/components/features/ctf-components/ctf-navigation/__generated/ctf-navigation.generated'; 6 | import { useCtfPageQuery } from '@src/components/features/ctf-components/ctf-page/__generated/ctf-page.generated'; 7 | import CtfPageGgl from '@src/components/features/ctf-components/ctf-page/ctf-page-gql'; 8 | import { getServerSideTranslations } from '@src/lib/get-serverside-translations'; 9 | import { prefetchPromiseArr } from '@src/lib/prefetch-promise-array'; 10 | 11 | const LangPage: NextPage = () => { 12 | return ; 13 | }; 14 | 15 | export const getServerSideProps = async ({ locale, query }: NextPageContext) => { 16 | const preview = Boolean(query.preview); 17 | 18 | try { 19 | const queryClient = new QueryClient(); 20 | 21 | // Default queries 22 | const prefetchPromises = [ 23 | queryClient.prefetchQuery( 24 | useCtfPageQuery.getKey({ slug: 'home', locale, preview }), 25 | useCtfPageQuery.fetcher({ slug: 'home', locale, preview }), 26 | ), 27 | queryClient.prefetchQuery( 28 | useCtfNavigationQuery.getKey({ locale, preview }), 29 | useCtfNavigationQuery.fetcher({ locale, preview }), 30 | ), 31 | queryClient.prefetchQuery( 32 | useCtfFooterQuery.getKey({ locale, preview }), 33 | useCtfFooterQuery.fetcher({ locale, preview }), 34 | ), 35 | ]; 36 | 37 | // Dynamic queries 38 | const pageData = await useCtfPageQuery.fetcher({ slug: 'home', locale, preview })(); 39 | const page = pageData.pageCollection?.items[0]; 40 | 41 | const topSection = page?.topSectionCollection?.items; 42 | const content = page?.pageContent; 43 | const extraSection = page?.extraSectionCollection?.items; 44 | 45 | await Promise.all([ 46 | ...prefetchPromises, 47 | ...prefetchPromiseArr({ inputArr: topSection, locale, queryClient }), 48 | ...prefetchPromiseArr({ inputArr: extraSection, locale, queryClient }), 49 | ...prefetchPromiseArr({ inputArr: [content], locale, queryClient }), 50 | ]); 51 | 52 | return { 53 | props: { 54 | ...(await getServerSideTranslations(locale)), 55 | dehydratedState: dehydrate(queryClient), 56 | }, 57 | }; 58 | } catch { 59 | return { 60 | notFound: true, 61 | }; 62 | } 63 | }; 64 | 65 | export default LangPage; 66 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Add global polyfills for application 3 | * Next.js adds corejs (babel-polyfill), the URL api and fetch by default so you dont have to include those 4 | * intersection-observer: https://github.com/GoogleChromeLabs/intersection-observer 5 | */ 6 | 7 | import 'intersection-observer'; 8 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | type OmitCustom = Pick>; 2 | export type OmitDistributive = T extends any 3 | ? T extends object 4 | ? Id> 5 | : T 6 | : never; 7 | export type Id = Record & { [P in keyof T]: T[P] }; // Cosmetic use only makes the tooltips expand the type can be removed 8 | export type OmitRecursive = OmitCustom<{ [P in keyof T]: OmitDistributive }, K>; 9 | 10 | // eslint-disable-next-line arrow-parens 11 | export const tryget = (exp: () => T, d: T | undefined | null = undefined) => { 12 | try { 13 | const val = exp(); 14 | if (val != null) { 15 | return val; 16 | } 17 | } catch { 18 | /* noop */ 19 | } 20 | return d; 21 | }; 22 | 23 | export const optimizeLineBreak = (str: string): string => { 24 | const tokens = str.split(' '); 25 | 26 | if (tokens.length < 3) { 27 | return str; 28 | } 29 | 30 | const lastToken = tokens.pop(); 31 | 32 | return `${tokens.join(' ')}\u00A0${lastToken}`; 33 | }; 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "resolveJsonModule": true, 6 | "jsx": "preserve", 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "noEmit": true, 10 | "preserveConstEnums": true, 11 | "removeComments": false, 12 | "skipLibCheck": true, 13 | "sourceMap": true, 14 | "strict": true, 15 | "noImplicitAny": false, 16 | "target": "esnext", 17 | "baseUrl": "./", 18 | "paths": { 19 | "@public/*": ["./public/*"], 20 | "@pages/*": ["./pages/*"], 21 | "@src/*": ["./src/*"], 22 | "@ctf-components/*": ["./src/ctf-components/*"] 23 | }, 24 | "typeRoots": ["./@types", "./node_modules/@types"], 25 | "lib": ["dom", "dom.iterable", "esnext"], 26 | "forceConsistentCasingInFileNames": true, 27 | "esModuleInterop": true, 28 | "isolatedModules": true, 29 | "incremental": true 30 | }, 31 | "exclude": ["node_modules"], 32 | "include": ["**/*.ts", "**/*.tsx", "@types/**/*.d.ts"] 33 | } 34 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildCommand": "NEXT_PUBLIC_BASE_URL=\"${NEXT_PUBLIC_BASE_URL:-https://$NEXT_PUBLIC_VERCEL_URL}\" yarn build" 3 | } 4 | --------------------------------------------------------------------------------