├── .codesandbox └── tasks.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── config.yml └── workflows │ ├── deploy-docs.yml │ ├── deploy-test.yml │ └── smoke-test.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── .node-version ├── .nojekyll ├── .vitepress │ └── config.ts ├── index.md ├── package.json ├── release-log.md └── yarn.lock └── examples ├── .gitignore ├── contact-profile ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── contact-profile-project │ ├── .hsignore │ ├── contact-profile-app │ │ ├── .node-version │ │ ├── cms-assets.json │ │ ├── components │ │ │ ├── Column.jsx │ │ │ ├── Company.jsx │ │ │ ├── FormInput.jsx │ │ │ ├── Grid.jsx │ │ │ ├── Layout.jsx │ │ │ ├── Link.jsx │ │ │ ├── ProfileImage.jsx │ │ │ ├── ProfileName.jsx │ │ │ ├── Row.jsx │ │ │ ├── SVGs.jsx │ │ │ └── modules │ │ │ │ ├── ContactProfile │ │ │ │ ├── fields.jsx │ │ │ │ └── index.jsx │ │ │ │ └── ContactWithAssociations │ │ │ │ ├── fields.jsx │ │ │ │ └── index.jsx │ │ ├── package.json │ │ └── styles │ │ │ ├── layout.module.css │ │ │ └── theme.module.css │ └── hsproject.json ├── contact-profile-theme │ ├── contact-profile.hubl.html │ ├── contact-with-associations.hubl.html │ ├── fields.json │ ├── layouts │ │ └── base.hubl.html │ └── theme.json ├── jsconfig.json └── package.json ├── data-fetching ├── .eslintrc.js ├── .gitignore ├── .node-version ├── .prettierrc ├── LICENSE ├── data-fetching-project │ ├── .hsignore │ ├── data-fetching-app │ │ ├── Globals.d.ts │ │ ├── cms-assets.json │ │ ├── components │ │ │ ├── PokeCard.tsx │ │ │ └── modules │ │ │ │ └── Fetcher │ │ │ │ ├── fields.tsx │ │ │ │ └── index.tsx │ │ ├── package.json │ │ ├── styles │ │ │ ├── component.module.css │ │ │ └── pokecard.module.css │ │ ├── tsconfig.json │ │ ├── utils │ │ │ └── index.ts │ │ └── yarn.lock │ └── hsproject.json ├── data-fetching-theme │ ├── data-fetching.hubl.html │ ├── fields.json │ ├── layouts │ │ └── base.hubl.html │ └── theme.json ├── package.json └── yarn.lock ├── getting-started-project-theme ├── .eslintrc.js ├── .gitignore ├── .node-version ├── .prettierrc ├── README.md ├── hsproject.json ├── package.json └── src │ └── getting-started-theme │ ├── Globals.d.ts │ ├── assets │ ├── clear.svg │ ├── cloudy.svg │ ├── fog.svg │ ├── rain.svg │ ├── snow.svg │ ├── sprocket.svg │ └── thunderstorm.svg │ ├── components │ ├── WeatherCards.tsx │ ├── islands │ │ └── WeatherForecast.tsx │ └── modules │ │ ├── Footer │ │ └── index.tsx │ │ ├── Header │ │ └── index.tsx │ │ └── Weather │ │ └── index.tsx │ ├── constants.ts │ ├── fields.json │ ├── package.json │ ├── styles │ ├── footer.module.css │ ├── header.module.css │ └── weather.module.css │ ├── templates │ ├── layouts │ │ └── base.hubl.html │ └── weather.hubl.html │ ├── theme.json │ ├── tsconfig.json │ └── utils.ts ├── getting-started ├── .eslintrc.js ├── .gitignore ├── .node-version ├── .prettierrc ├── README.md ├── getting-started-project │ ├── getting-started-app │ │ ├── Globals.d.ts │ │ ├── assets │ │ │ ├── clear.svg │ │ │ ├── cloudy.svg │ │ │ ├── fog.svg │ │ │ ├── rain.svg │ │ │ ├── snow.svg │ │ │ ├── sprocket.svg │ │ │ └── thunderstorm.svg │ │ ├── cms-assets.json │ │ ├── components │ │ │ ├── WeatherCards.tsx │ │ │ ├── islands │ │ │ │ └── WeatherForecast.tsx │ │ │ └── modules │ │ │ │ ├── Footer │ │ │ │ └── index.tsx │ │ │ │ ├── Header │ │ │ │ └── index.tsx │ │ │ │ └── Weather │ │ │ │ └── index.tsx │ │ ├── constants.ts │ │ ├── package.json │ │ ├── styles │ │ │ ├── footer.module.css │ │ │ ├── header.module.css │ │ │ └── weather.module.css │ │ ├── tsconfig.json │ │ └── utils.ts │ └── hsproject.json ├── getting-started-theme │ ├── fields.json │ ├── layouts │ │ └── base.hubl.html │ ├── theme.json │ └── weather.hubl.html └── package.json ├── graphql-storybook ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── gql-storybook-project │ ├── .hsignore │ ├── gql-storybook-app │ │ ├── .node-version │ │ ├── cms-assets.json │ │ ├── components │ │ │ └── modules │ │ │ │ └── ContactList │ │ │ │ ├── ContactList.stories.jsx │ │ │ │ ├── fields.tsx │ │ │ │ └── index.tsx │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── vitest.config.js │ └── hsproject.json ├── gql-storybook-theme │ ├── fields.json │ ├── gql-storybook.hubl.html │ ├── layouts │ │ └── base.hubl.html │ └── theme.json └── package.json ├── hello-world ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── hello-world-project │ ├── .hsignore │ ├── hello-world-app │ │ ├── .node-version │ │ ├── assets │ │ │ ├── HubSpot-Sprocket.svg │ │ │ ├── huayra.jpg │ │ │ ├── orange-line.jpg │ │ │ ├── tacoma.jpg │ │ │ └── vw-id.jpg │ │ ├── cms-assets.json │ │ ├── components │ │ │ ├── HubSpotSprocket.jsx │ │ │ ├── Layout.jsx │ │ │ ├── Link.jsx │ │ │ ├── islands │ │ │ │ ├── Button.jsx │ │ │ │ └── TodoList.jsx │ │ │ ├── modules │ │ │ │ └── TodoList │ │ │ │ │ ├── fields.jsx │ │ │ │ │ └── index.jsx │ │ │ └── partials │ │ │ │ ├── Cars.jsx │ │ │ │ ├── Footer.jsx │ │ │ │ ├── Header.jsx │ │ │ │ └── Home.jsx │ │ ├── package.json │ │ ├── styles │ │ │ ├── cars.module.css │ │ │ ├── footer.module.css │ │ │ ├── header.module.css │ │ │ ├── home.module.css │ │ │ ├── layout.module.css │ │ │ └── todo.module.css │ │ ├── tests │ │ │ └── components │ │ │ │ ├── __snapshots__ │ │ │ │ └── partials.home.test.jsx.snap │ │ │ │ └── partials.home.test.jsx │ │ └── vitest.config.js │ └── hsproject.json ├── hello-world-theme │ ├── cars.hubl.html │ ├── fields.json │ ├── home.hubl.html │ ├── layouts │ │ └── base.hubl.html │ ├── theme.json │ └── todo.hubl.html ├── jsconfig.json └── package.json ├── islands ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── islands-project │ ├── .hsignore │ ├── hsproject.json │ └── islands-app │ │ ├── .node-version │ │ ├── Globals.d.ts │ │ ├── cms-assets.json │ │ ├── components │ │ ├── ButtonCounter │ │ │ ├── ButtonCounter.module.css │ │ │ ├── ButtonCounter.tsx │ │ │ └── index.tsx │ │ ├── Clock.jsx │ │ └── partials │ │ │ ├── BasicIslandClock.jsx │ │ │ ├── IslandsTester.jsx │ │ │ └── IslandsTesterPlus.jsx │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── vitest.config.js ├── islands-theme │ ├── fields.json │ ├── islands.hubl.html │ ├── layouts │ │ └── base.hubl.html │ └── theme.json └── package.json ├── module-components ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── jsconfig.json ├── module-components-project │ ├── .hsignore │ ├── hsproject.json │ └── module-components-app │ │ ├── cms-assets.json │ │ ├── components │ │ └── modules │ │ │ ├── IconExample │ │ │ └── index.jsx │ │ │ └── RichTextExample │ │ │ └── index.jsx │ │ └── package.json └── package.json ├── routing ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── package.json ├── routing-project │ ├── hsproject.json │ └── routing-app │ │ ├── Global.d.ts │ │ ├── assets │ │ ├── dynamic-slug-screenshot.png │ │ └── pokeball.svg │ │ ├── cms-assets.json │ │ ├── components │ │ ├── Header.tsx │ │ ├── Home.tsx │ │ ├── ListingCard.tsx │ │ ├── Pokedex.tsx │ │ ├── Pokemon.tsx │ │ ├── ProfileCard.tsx │ │ ├── islands │ │ │ └── App.tsx │ │ └── modules │ │ │ └── Router │ │ │ └── index.tsx │ │ ├── constants.ts │ │ ├── package.json │ │ ├── styles │ │ ├── card.module.css │ │ ├── header.module.css │ │ └── page.module.css │ │ └── tsconfig.json └── routing-theme │ ├── fields.json │ ├── layouts │ └── base.hubl.html │ ├── router-page.hubl.html │ └── theme.json ├── serverless ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── cms-with-serverless-project │ ├── .hsignore │ ├── app │ │ ├── app.functions │ │ │ ├── package.json │ │ │ ├── parrot-function.js │ │ │ └── serverless.json │ │ └── app.json │ ├── cms-react │ │ ├── cms-assets.json │ │ ├── components │ │ │ └── modules │ │ │ │ └── MakeServerlessRequest │ │ │ │ ├── MakeServerlessRequestIsland.jsx │ │ │ │ └── index.jsx │ │ └── package.json │ └── hsproject.json ├── jsconfig.json └── package.json ├── styling ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── package.json ├── styling-project │ ├── .hsignore │ ├── hsproject.json │ └── styling-app │ │ ├── .node-version │ │ ├── assets │ │ ├── biker-tailwind-ai.png │ │ ├── dog-named-tailwind-ai.png │ │ └── windy-trail-ai.png │ │ ├── cms-assets.json │ │ ├── components │ │ ├── InteractiveStyledComponent.jsx │ │ ├── InteractiveStyledJSXComponent.jsx │ │ ├── StyledComponentsIsland.jsx │ │ ├── StyledComponentsRegistry.jsx │ │ ├── StyledJSXIsland.jsx │ │ ├── StyledJSXRegistry.jsx │ │ ├── islands │ │ │ └── ToggleSwitch.jsx │ │ └── partials │ │ │ ├── CSSModulesPartial.jsx │ │ │ ├── CSSPropertiesPartial.jsx │ │ │ ├── InlineStylesPartial.jsx │ │ │ ├── StyledComponentsPartial.jsx │ │ │ ├── StyledJSXPartial.jsx │ │ │ └── TailwindPartial.jsx │ │ ├── jsconfig.json │ │ ├── package.json │ │ ├── postcss.config.mjs │ │ ├── styles │ │ ├── CSSModulesPartial.module.css │ │ ├── CSSPropertiesPartial.css │ │ └── tailwind.css │ │ ├── tailwind.config.js │ │ └── vitest.config.js └── styling-theme │ ├── fields.json │ ├── layouts │ └── base.hubl.html │ ├── styling.hubl.html │ ├── tailwind.hubl.html │ └── theme.json └── todo-mvc ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── jsconfig.json ├── package.json ├── todo-mvc-project ├── .hsignore ├── hsproject.json └── todo-mvc-app │ ├── .node-version │ ├── cms-assets.json │ ├── components │ ├── modules │ │ └── TodoMVCModule │ │ │ ├── fields.jsx │ │ │ └── index.jsx │ └── todomvc-separate-islands │ │ ├── components │ │ ├── app │ │ │ └── app.jsx │ │ ├── copy-right │ │ │ └── copy-right.jsx │ │ ├── footer │ │ │ └── footer.jsx │ │ ├── header │ │ │ ├── TodoInput.jsx │ │ │ └── header.jsx │ │ ├── hooks │ │ │ └── useFilterFromURL.js │ │ ├── item │ │ │ └── item.jsx │ │ └── list │ │ │ └── list.jsx │ │ ├── constants │ │ ├── action-type.js │ │ └── filter.js │ │ ├── index.jsx │ │ └── store │ │ ├── actions │ │ └── todo.js │ │ ├── reducers │ │ └── todo.js │ │ └── selectors │ │ └── todo.js │ ├── package.json │ └── vitest.config.js └── todo-mvc-theme ├── fields.json ├── layouts └── base.hubl.html ├── theme.json └── todo-mvc.hubl.html /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: CMS React bug report 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | 10 | ### Description 11 | 12 | 13 | ### Steps to Reproduce 14 | 20 | 21 | ### Expected Behavior 22 | 23 | 24 | ### Actual Behavior 25 | 26 | 27 | ### Environment 28 | - OS: 29 | - Node version: 30 | - @hubspot/cli version: 31 | - @hubspot/cms-dev-server version: 32 | - @hubspot/cms-components version: 33 | 34 | ### Logs or Error Messages 35 | 36 | 37 | ### Screenshots/GIFs 38 | 39 | 40 | ### Additional Context 41 | 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy docs to GH Pages 2 | 3 | on: 4 | # Runs on pushes targeting the `main` branch. Change this to `master` if you're 5 | # using the `master` branch as the default branch. 6 | push: 7 | branches: [main] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: pages 22 | cancel-in-progress: false 23 | 24 | defaults: 25 | run: 26 | working-directory: docs 27 | 28 | jobs: 29 | # Build job 30 | build: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v4 35 | with: 36 | fetch-depth: 0 # Not needed if lastUpdated is not enabled 37 | - name: Setup Node 38 | uses: actions/setup-node@v4 39 | with: 40 | node-version: 18 41 | cache: yarn # or pnpm / yarn 42 | cache-dependency-path: 'docs/yarn.lock' 43 | - name: Setup Pages 44 | uses: actions/configure-pages@v4 45 | - name: Install dependencies 46 | run: yarn # or pnpm install / yarn install / bun install 47 | - name: Build with VitePress 48 | run: | 49 | yarn build 50 | touch .vitepress/dist/.nojekyll 51 | - name: Upload artifact 52 | uses: actions/upload-pages-artifact@v3 53 | with: 54 | path: docs/.vitepress/dist 55 | 56 | # Deployment job 57 | deploy: 58 | environment: 59 | name: github-pages 60 | url: ${{ steps.deployment.outputs.page_url }} 61 | needs: build 62 | runs-on: ubuntu-latest 63 | name: Deploy 64 | steps: 65 | - name: Deploy to GitHub Pages 66 | id: deployment 67 | uses: actions/deploy-pages@v4 68 | -------------------------------------------------------------------------------- /.github/workflows/deploy-test.yml: -------------------------------------------------------------------------------- 1 | name: Project build and deploy test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy-test: 10 | strategy: 11 | matrix: 12 | os: [windows-latest, ubuntu-latest] 13 | node: [18] 14 | 15 | runs-on: ${{ matrix.os }} 16 | 17 | defaults: 18 | run: 19 | working-directory: examples/hello-world/hello-world-project 20 | shell: bash 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node }} 27 | 28 | - name: Rename project 29 | run: | 30 | sed -i 's/hello-world-project/hello-world-project-os-${{ matrix.os }}-node-${{ matrix.node }}/g' hsproject.json 31 | echo "" 32 | echo "After rename" 33 | cat hsproject.json 34 | 35 | - name: Build and deploy 36 | env: 37 | HUBSPOT_PORTAL_ID: ${{ secrets.hubspot_portal_id }} 38 | HUBSPOT_PERSONAL_ACCESS_KEY: ${{ secrets.hubspot_personal_access_key }} 39 | run: | 40 | npx --yes --package=@hubspot/cli@latest --call='hs project upload --use-env' 41 | 42 | -------------------------------------------------------------------------------- /.github/workflows/smoke-test.yml: -------------------------------------------------------------------------------- 1 | name: CMS dev server test 2 | 3 | on: push 4 | 5 | jobs: 6 | dev-server-smoke-test: 7 | strategy: 8 | matrix: 9 | os: [windows-latest, ubuntu-latest] 10 | node: [16, 18] 11 | 12 | runs-on: ${{ matrix.os }} 13 | 14 | defaults: 15 | run: 16 | working-directory: examples/hello-world 17 | shell: bash 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - uses: actions/setup-node@v3 22 | with: 23 | node-version: ${{ matrix.node }} 24 | 25 | - name: NPM install 26 | run: | 27 | npm install 28 | 29 | - name: Run dev server 30 | run: | 31 | npm run start & 32 | 33 | - name: Basic dev server home test 34 | run: | 35 | curl --no-progress-meter http://localhost:3000/ --output home.txt 36 | ls -lh 37 | wc home.txt 38 | echo "Grepping home.txt" 39 | grep -q '/partial/Cars' home.txt 40 | echo "-----" 41 | cat home.txt 42 | 43 | - name: Basic dev server partial test 44 | run: | 45 | curl --no-progress-meter http://localhost:3000/partial/Cars --output Cars.txt 46 | echo "Grepping Cars.txt" 47 | grep -q 'Volkswagen' Cars.txt 48 | echo "-----" 49 | cat Cars.txt 50 | 51 | 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | hubspot.config.yml 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HubSpot CMS React 2 | 3 | A collection of CMS React examples for building on the HubSpot CMS. 4 | 5 | ## Table of Contents 6 | 7 | - [Overview](#overview) 8 | - [Documentation](#documentation) 9 | - [Examples](#examples) 10 | - [Contributing](#contributing) 11 | - [Support](#support) 12 | 13 | --- 14 | 15 | ## Overview 16 | 17 | This repo demonstrates how to build React modules and "islands" for the HubSpot CMS. You'll find step-by-step examples, best practices, and customizable boilerplates to get you up and running quickly. 18 | 19 | ## Documentation 20 | 21 | - Official HubSpot CMS React docs: 22 | https://developers.hubspot.com/docs/guides/cms/react/overview 23 | 24 | ## Examples 25 | Each folder under `examples/` is a standalone example focused on a specific use case or feature. If you are just getting started with CMS React, the following examples are the best places to start: 26 | 27 | - **[getting-started-project-theme](examples/getting-started-project-theme)** 28 | Building a CMS theme with React and Projects 29 | - **[getting-started](examples/getting-started)** 30 | Building a standalone Project with React modules that can be used across CMS themes 31 | 32 | If you want to bootstrap a new Project with CMS React you can run `npx @hubspot/create-cms-theme@latest` which will create a Project starting point for you to build upon. [Learn More](https://www.npmjs.com/package/@hubspot/create-cms-theme) 33 | 34 | --- 35 | 36 | ## Contributing 37 | 38 | We welcome bug reports and PRs to update or add new examples: 39 | 40 | 1. Fork this repo 41 | 2. Create a feature branch (`git checkout -b my-new-example`) 42 | 3. Commit your changes (`git commit -m "Add amazing example"`) 43 | 4. Open a PR 44 | 45 | --- 46 | 47 | ## Support 48 | 49 | Chat with the community on Slack: 50 | https://developers.hubspot.com/docs/getting-started/slack/developer-slack 51 | 52 | --- 53 | -------------------------------------------------------------------------------- /docs/.node-version: -------------------------------------------------------------------------------- 1 | 16.13.2 2 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/cms-react/6f4a05d12d121080d518363fc5643e90a80c6ab7/docs/.nojekyll -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress'; 2 | 3 | // https://vitepress.dev/reference/site-config 4 | export default defineConfig({ 5 | title: 'HubSpot - CMS React', 6 | description: 'Archived documentation site for HubSpot CMS React', 7 | base: '/cms-react/', 8 | themeConfig: { 9 | // https://vitepress.dev/reference/default-theme-config 10 | nav: [ 11 | { text: 'Official Docs', link: 'https://developers.hubspot.com/docs/guides/cms/react/overview' }, 12 | { text: 'Examples', link: 'https://github.com/HubSpot/cms-react/tree/main/examples' }, 13 | { text: 'Release Log', link: 'release-log' }, 14 | ], 15 | }, 16 | ignoreDeadLinks: true, 17 | }); 18 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # This site has been archived 2 | Please refer to the [official documentation](https://developers.hubspot.com/docs/guides/cms/react/overview) for the latest CMS React information. 3 | 4 | ## Quicklinks 5 | - [Getting started](https://developers.hubspot.com/docs/guides/cms/quickstart/react-plus-hubl-quickstart) 6 | - [Field types](https://developers.hubspot.com/docs/reference/cms/fields/module-theme-fields) 7 | 8 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vitepress dev", 7 | "build": "vitepress build", 8 | "preview": "vitepress preview", 9 | "serve": "vitepress serve" 10 | }, 11 | "dependencies": {}, 12 | "devDependencies": { 13 | "vitepress": "1.0.0-rc.11" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | -------------------------------------------------------------------------------- /examples/contact-profile/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | sourceType: 'module', 4 | ecmaFeatures: { 5 | jsx: true, 6 | }, 7 | }, 8 | env: { 9 | node: true, 10 | es2021: true, 11 | }, 12 | extends: ['eslint:recommended', 'prettier', 'plugin:react/recommended'], 13 | rules: { 14 | 'react/react-in-jsx-scope': 'off', 15 | 'react/prop-types': 'off', 16 | }, 17 | ignorePatterns: ['hello-world-project/hello-world-app/dist/*'], 18 | settings: { 19 | react: { 20 | version: '18.1', 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /examples/contact-profile/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | hubspot.config.yml 3 | dist -------------------------------------------------------------------------------- /examples/contact-profile/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "overrides": [ 7 | { 8 | "files": "*.hubl.html", 9 | "options": { 10 | "parser": "hubl" 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /examples/contact-profile/contact-profile-project/.hsignore: -------------------------------------------------------------------------------- 1 | README.md 2 | tsconfig.json 3 | yarn.lock 4 | dist 5 | -------------------------------------------------------------------------------- /examples/contact-profile/contact-profile-project/contact-profile-app/.node-version: -------------------------------------------------------------------------------- 1 | 16.13.2 2 | -------------------------------------------------------------------------------- /examples/contact-profile/contact-profile-project/contact-profile-app/cms-assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Contact Profile - React", 3 | "outputPath": "" 4 | } 5 | -------------------------------------------------------------------------------- /examples/contact-profile/contact-profile-project/contact-profile-app/components/Column.jsx: -------------------------------------------------------------------------------- 1 | import styles from '../styles/theme.module.css'; 2 | 3 | const Column = ({ className, children, style = {}, width = 12 }) => { 4 | return ( 5 |
9 | {children} 10 |
11 | ); 12 | }; 13 | 14 | export default Column; 15 | -------------------------------------------------------------------------------- /examples/contact-profile/contact-profile-project/contact-profile-app/components/Company.jsx: -------------------------------------------------------------------------------- 1 | import Row from './Row'; 2 | import Column from './Column'; 3 | 4 | const ProfileName = ({ name, description }) => { 5 | return ( 6 |
7 | 8 |

Primary Company:

9 |
10 | 11 | 12 |

{name}

13 |
14 |
15 | 16 | 17 | {description} 18 | 19 | 20 |
21 | ); 22 | }; 23 | 24 | export default ProfileName; 25 | -------------------------------------------------------------------------------- /examples/contact-profile/contact-profile-project/contact-profile-app/components/FormInput.jsx: -------------------------------------------------------------------------------- 1 | const FormInput = ({ 2 | helpText, 3 | id = 'no-id', 4 | label = 'Missing Label', 5 | type = 'text', 6 | defaultValue, 7 | }) => { 8 | return ( 9 |

10 | 13 | 14 |

15 | ); 16 | }; 17 | 18 | export default FormInput; 19 | -------------------------------------------------------------------------------- /examples/contact-profile/contact-profile-project/contact-profile-app/components/Grid.jsx: -------------------------------------------------------------------------------- 1 | import styles from '../styles/theme.module.css'; 2 | 3 | const Grid = ({ className, children, style = {} }) => { 4 | return ( 5 |
11 | {children} 12 |
13 | ); 14 | }; 15 | 16 | export default Grid; 17 | -------------------------------------------------------------------------------- /examples/contact-profile/contact-profile-project/contact-profile-app/components/Layout.jsx: -------------------------------------------------------------------------------- 1 | import layoutStyles from '../styles/layout.module.css'; 2 | 3 | function Layout({ children }) { 4 | return
{children}
; 5 | } 6 | 7 | export default Layout; 8 | -------------------------------------------------------------------------------- /examples/contact-profile/contact-profile-project/contact-profile-app/components/Link.jsx: -------------------------------------------------------------------------------- 1 | function Link(props) { 2 | return {props.title}; 3 | } 4 | 5 | export default Link; 6 | -------------------------------------------------------------------------------- /examples/contact-profile/contact-profile-project/contact-profile-app/components/ProfileImage.jsx: -------------------------------------------------------------------------------- 1 | import { Pencil } from './SVGs'; 2 | 3 | const AVATAR_BG_COLOR = 'rgb(142 124 195)'; 4 | 5 | const ProfileImage = ({ firstName = 'John Doe' }) => { 6 | return ( 7 |
20 | {firstName && firstName.slice(0, 1)} 21 |
37 | 45 |
46 |
47 | ); 48 | }; 49 | 50 | export default ProfileImage; 51 | -------------------------------------------------------------------------------- /examples/contact-profile/contact-profile-project/contact-profile-app/components/ProfileName.jsx: -------------------------------------------------------------------------------- 1 | import Row from './Row'; 2 | import Column from './Column'; 3 | 4 | import styles from '../styles/theme.module.css'; 5 | 6 | const ProfileName = ({ firstName, lastName, city, country, style = {} }) => { 7 | return ( 8 |
9 | 10 | 11 |

12 | {firstName} {lastName} 13 |

14 |
15 |
16 | 17 | 18 | 19 | {city} · {country} 20 | 21 | 22 | 23 |
24 | ); 25 | }; 26 | 27 | export default ProfileName; 28 | -------------------------------------------------------------------------------- /examples/contact-profile/contact-profile-project/contact-profile-app/components/Row.jsx: -------------------------------------------------------------------------------- 1 | import styles from '../styles/theme.module.css'; 2 | 3 | const Row = ({ children, className, style = {} }) => { 4 | return ( 5 |
9 | {children} 10 |
11 | ); 12 | }; 13 | 14 | export default Row; 15 | -------------------------------------------------------------------------------- /examples/contact-profile/contact-profile-project/contact-profile-app/components/SVGs.jsx: -------------------------------------------------------------------------------- 1 | export const Pencil = ({ style = {} }) => ( 2 | 13 | ); 14 | -------------------------------------------------------------------------------- /examples/contact-profile/contact-profile-project/contact-profile-app/components/modules/ContactProfile/fields.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ModuleFields, ColorField } from '@hubspot/cms-components/fields'; 3 | 4 | export const fields = ( 5 | 6 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /examples/contact-profile/contact-profile-project/contact-profile-app/components/modules/ContactWithAssociations/fields.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ModuleFields, ColorField } from '@hubspot/cms-components/fields'; 3 | 4 | export const fields = ( 5 | 6 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /examples/contact-profile/contact-profile-project/contact-profile-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contact-profile-app", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "dependencies": { 6 | "@hubspot/cms-components": "latest", 7 | "prop-types": "^15.8.1", 8 | "react": "^18.1.0" 9 | }, 10 | "devDependencies": { 11 | "@hubspot/cms-dev-server": "latest", 12 | "@testing-library/react": "^13.4.0", 13 | "@vitejs/plugin-react": "^2.1.0", 14 | "jsdom": "^20.0.1", 15 | "vitest": "^0.24.3" 16 | }, 17 | "scripts": { 18 | "start": "hs-cms-dev-server .", 19 | "test": "vitest" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/contact-profile/contact-profile-project/contact-profile-app/styles/layout.module.css: -------------------------------------------------------------------------------- 1 | :global(html) { 2 | height: 100%; 3 | } 4 | 5 | :global(body) { 6 | font-family: Arial, Helvetica, sans-serif; 7 | height: 100%; 8 | margin: 0; 9 | padding: 0; 10 | } 11 | 12 | :global(.body-wrapper) { 13 | height: 100%; 14 | display: flex; 15 | flex-direction: column; 16 | } 17 | 18 | .layout { 19 | flex: 1; 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | flex-direction: column; 24 | } 25 | -------------------------------------------------------------------------------- /examples/contact-profile/contact-profile-project/contact-profile-app/styles/theme.module.css: -------------------------------------------------------------------------------- 1 | .rowFluidWrapper { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .rowFluid { 7 | display: flex; 8 | } 9 | 10 | .col { 11 | flex: 1; 12 | } 13 | -------------------------------------------------------------------------------- /examples/contact-profile/contact-profile-project/hsproject.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contact-profile-project", 3 | "srcDir": ".", 4 | "platformVersion": "2023.2" 5 | } 6 | -------------------------------------------------------------------------------- /examples/contact-profile/contact-profile-theme/contact-profile.hubl.html: -------------------------------------------------------------------------------- 1 | 6 | {% extends "./layouts/base.hubl.html" %} 7 | 8 | {% block body %} 9 | 10 | {% module "contact_profile" 11 | path="@projects/contact-profile-project/contact-profile-app/components/modules/ContactProfile", 12 | firstName="{{contact.firstname}}", 13 | lastName="{{contact.lastname}}", 14 | email="{{contact.email}}", 15 | foo="bar" %} 16 | 17 | {% endblock body %} 18 | -------------------------------------------------------------------------------- /examples/contact-profile/contact-profile-theme/contact-with-associations.hubl.html: -------------------------------------------------------------------------------- 1 | 6 | {% extends "./layouts/base.hubl.html" %} 7 | 8 | {% block body %} 9 | 10 | {% module "contact_profile_with_associations" 11 | path="@projects/contact-profile-project/contact-profile-app/components/modules/ContactWithAssociations" %} 12 | 13 | {% endblock body %} 14 | -------------------------------------------------------------------------------- /examples/contact-profile/contact-profile-theme/fields.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "Color", 4 | "name": "brand_color", 5 | "label": "Brand Color", 6 | "default": { 7 | "color": "#FF7A59", 8 | "opacity": 100 9 | } 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /examples/contact-profile/contact-profile-theme/layouts/base.hubl.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | {% if page_meta.html_title or pageTitle %} 9 | {{ page_meta.html_title or pageTitle }} 10 | {% endif %} 11 | {% if brand_settings.primaryFavicon.src %} 12 | 13 | {% endif %} 14 | 15 | {{ standard_header_includes }} 16 | 17 | 18 |
19 | {% block header %} 20 | {% endblock header %} 21 | 22 | {# The main-content ID is used for the navigation skipper in the header.html file. More information on the navigation skipper can be found here: https://github.com/HubSpot/cms-theme-boilerplate/wiki/Accessibility #} 23 | 24 | {% block body %} 25 | {% endblock body %} 26 | 27 | {% block footer %} 28 | {% endblock footer %} 29 |
30 | 31 | {# To see a full list of what is included via standard_footer_includes please reference this article: https://developers.hubspot.com/docs/cms/hubl/variables#required-page-template-variables #} 32 | {{ standard_footer_includes }} 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/contact-profile/contact-profile-theme/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "JS Building Blocks - Contact Profile", 3 | "preview_path": "./contact-profile.hubl.html" 4 | } 5 | -------------------------------------------------------------------------------- /examples/contact-profile/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "NodeNext" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/contact-profile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-building-blocks-contact-profile", 3 | "description": "JS Building Blocks Contact Profile", 4 | "license": "Apache-2.0", 5 | "devDependencies": { 6 | "@hubspot/cli": "latest", 7 | "@hubspot/prettier-plugin-hubl": "latest", 8 | "eslint": "^8.24.0", 9 | "eslint-config-prettier": "^8.5.0", 10 | "eslint-plugin-react": "^7.31.10", 11 | "prettier": "^2.7.1", 12 | "yarpm": "^1.2.0" 13 | }, 14 | "scripts": { 15 | "start": "cd contact-profile-project/contact-profile-app && yarpm start", 16 | "postinstall": "cd contact-profile-project/contact-profile-app && yarpm install", 17 | "lint:js": "eslint . --ext .js,.jsx", 18 | "prettier": "prettier . --check", 19 | "watch:hubl": "hs watch contact-profile-theme contact-profile-theme", 20 | "upload:hubl": "hs upload contact-profile-theme contact-profile-theme", 21 | "deploy": "hs project upload" 22 | }, 23 | "dependencies": {}, 24 | "engines": { 25 | "node": ">=16.0.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/data-fetching/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | sourceType: 'module', 4 | ecmaFeatures: { 5 | jsx: true, 6 | }, 7 | }, 8 | env: { 9 | node: true, 10 | es2021: true, 11 | }, 12 | extends: ['eslint:recommended', 'prettier', 'plugin:react/recommended'], 13 | rules: { 14 | 'react/react-in-jsx-scope': 'off', 15 | 'react/prop-types': 'off', 16 | }, 17 | settings: { 18 | react: { 19 | version: '18.1', 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /examples/data-fetching/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | hubspot.config.yml 3 | dist 4 | -------------------------------------------------------------------------------- /examples/data-fetching/.node-version: -------------------------------------------------------------------------------- 1 | 20.11.0 2 | -------------------------------------------------------------------------------- /examples/data-fetching/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "overrides": [ 7 | { 8 | "files": "*.hubl.html", 9 | "options": { 10 | "parser": "hubl" 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /examples/data-fetching/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 HubSpot, Inc. 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | -------------------------------------------------------------------------------- /examples/data-fetching/data-fetching-project/.hsignore: -------------------------------------------------------------------------------- 1 | README.md 2 | tsconfig.json 3 | yarn.lock 4 | dist 5 | -------------------------------------------------------------------------------- /examples/data-fetching/data-fetching-project/data-fetching-app/Globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.css'; 2 | -------------------------------------------------------------------------------- /examples/data-fetching/data-fetching-project/data-fetching-app/cms-assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "CMS React - Data fetching", 3 | "outputPath": "" 4 | } 5 | -------------------------------------------------------------------------------- /examples/data-fetching/data-fetching-project/data-fetching-app/components/PokeCard.tsx: -------------------------------------------------------------------------------- 1 | import pokeCardStyles from '../styles/pokecard.module.css'; 2 | import { getTypeColor, PokemonTypes } from '../utils/index.js'; 3 | 4 | type PokemonData = { 5 | pokemonName: string; 6 | height: number; 7 | weight: number; 8 | profileImage: string; 9 | pokemonType: PokemonTypes; 10 | }; 11 | 12 | type PokeCardProps = { 13 | pokemonData: PokemonData; 14 | }; 15 | 16 | export default function PokeCard({ pokemonData }: PokeCardProps) { 17 | const { pokemonName, height, weight, profileImage, pokemonType } = 18 | pokemonData; 19 | const typeColor = getTypeColor(pokemonType); 20 | 21 | return ( 22 |
23 |
27 |
28 | {pokemonName} 29 |
30 |
31 |

{pokemonName}

32 |
33 |
34 |

Height

35 |

{height}m

36 |
37 |
38 |

Weight

39 |

{weight}kg

40 |
41 |
42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /examples/data-fetching/data-fetching-project/data-fetching-app/components/modules/Fetcher/fields.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | BooleanField, 3 | ChoiceField, 4 | ModuleFields, 5 | TextField, 6 | } from '@hubspot/cms-components/fields'; 7 | 8 | export const fields = ( 9 | 10 | 29 | 39 | 44 | 61 | 62 | ); 63 | -------------------------------------------------------------------------------- /examples/data-fetching/data-fetching-project/data-fetching-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "data-fetching-app", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "dependencies": { 6 | "@apollo/client": "^3.9.7", 7 | "@hubspot/cms-components": "latest", 8 | "axios": "^1.6.8", 9 | "graphql": "^16.8.1", 10 | "graphql-request": "^6.1.0", 11 | "node-fetch": "^3.3.2", 12 | "node-fetch-cache": "^4.1.0", 13 | "prop-types": "^15.8.1", 14 | "react": "^18.1.0" 15 | }, 16 | "devDependencies": { 17 | "@hubspot/cms-dev-server": "latest", 18 | "@testing-library/react": "^13.4.0", 19 | "@vitejs/plugin-react": "^2.1.0", 20 | "jsdom": "^20.0.1", 21 | "vitest": "^0.24.3" 22 | }, 23 | "scripts": { 24 | "start": "hs-cms-dev-server .", 25 | "test": "vitest" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/data-fetching/data-fetching-project/data-fetching-app/styles/component.module.css: -------------------------------------------------------------------------------- 1 | .summary h2 code { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /examples/data-fetching/data-fetching-project/data-fetching-app/styles/pokecard.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | gap: 10px; 4 | } 5 | 6 | .card { 7 | display: flex; 8 | border-radius: 5px; 9 | background-color: #303030; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | padding: 20px 20px 25px 20px; 14 | margin: 10px; 15 | min-height: 300px; 16 | min-width: 225px; 17 | gap: 20px; 18 | } 19 | 20 | .profile { 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | } 25 | 26 | .profile .highlight { 27 | width: 175px; 28 | height: 175px; 29 | background: transparent; 30 | border-radius: 50%; 31 | position: absolute; 32 | box-shadow: 0px 0px 0px 0px #fff, 0px 0px 50px inset rgba(255, 255,255, 0.5); 33 | z-index: 0; 34 | } 35 | 36 | .card img { 37 | width: 100%; 38 | max-width: 100px; 39 | align-self: center; 40 | z-index: 1; 41 | height: 175px; 42 | } 43 | 44 | .attributes { 45 | display: flex; 46 | justify-content: space-around; 47 | width: 100%; 48 | margin-top: 10px; 49 | } 50 | 51 | .attributes h4 { 52 | margin-top: 0; 53 | margin-bottom: 5px; 54 | color: white; 55 | } 56 | 57 | .attributes p { 58 | margin: 0; 59 | color: white; 60 | } 61 | 62 | .stack { 63 | display: flex; 64 | justify-content: center; 65 | align-items: center; 66 | flex-direction: column; 67 | } 68 | 69 | .card h2 { 70 | margin: 0px; 71 | color: white; 72 | } 73 | 74 | .card span { 75 | padding: 2.5px 5px; 76 | background-color: #f0f0f0; 77 | } 78 | 79 | ul { 80 | margin-top: 0px; 81 | } 82 | -------------------------------------------------------------------------------- /examples/data-fetching/data-fetching-project/data-fetching-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "module": "Node16", 5 | "moduleResolution": "node16", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "jsx": "react-jsx", 11 | "plugins": [ 12 | { 13 | "name": "typescript-plugin-css-modules" 14 | } 15 | ] 16 | }, 17 | "$schema": "https://json.schemastore.org/tsconfig", 18 | "display": "Recommended" 19 | } 20 | -------------------------------------------------------------------------------- /examples/data-fetching/data-fetching-project/hsproject.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "data-fetching-project", 3 | "srcDir": ".", 4 | "platformVersion": "2023.2" 5 | } 6 | -------------------------------------------------------------------------------- /examples/data-fetching/data-fetching-theme/data-fetching.hubl.html: -------------------------------------------------------------------------------- 1 | 6 | {% extends "./layouts/base.hubl.html" %} 7 | {% block body %} 8 | {% module 'fetcher' 9 | path="@projects/data-fetching-project/data-fetching-app/components/modules/Fetcher" 10 | %} 11 | {% endblock body %} 12 | -------------------------------------------------------------------------------- /examples/data-fetching/data-fetching-theme/fields.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /examples/data-fetching/data-fetching-theme/layouts/base.hubl.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | {% if page_meta.html_title or pageTitle %} 9 | {{ page_meta.html_title or pageTitle }} 10 | {% endif %} 11 | {% if brand_settings.primaryFavicon.src %} 12 | 13 | {% endif %} 14 | 15 | {{ standard_header_includes }} 16 | 17 | 18 |
19 | {% block header %} 20 | {% endblock header %} 21 | 22 | {% block body %} 23 | {% endblock body %} 24 | 25 | {% block footer %} 26 | {% endblock footer %} 27 |
28 | 29 | {# To see a full list of what is included via standard_footer_includes please reference this article: https://developers.hubspot.com/docs/cms/hubl/variables#required-page-template-variables #} 30 | {{ standard_footer_includes }} 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/data-fetching/data-fetching-theme/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Data fetching theme" 3 | } 4 | -------------------------------------------------------------------------------- /examples/data-fetching/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cms-react-data-fetching", 3 | "description": "CMS React Data Fetching", 4 | "license": "Apache-2.0", 5 | "devDependencies": { 6 | "@hubspot/cli": "latest", 7 | "@hubspot/prettier-plugin-hubl": "latest", 8 | "eslint": "^8.24.0", 9 | "eslint-config-prettier": "^8.5.0", 10 | "eslint-plugin-react": "^7.31.10", 11 | "prettier": "^2.7.1", 12 | "yarpm": "^1.2.0" 13 | }, 14 | "scripts": { 15 | "start": "cd data-fetching-project/data-fetching-app && yarpm start --", 16 | "postinstall": "cd data-fetching-project/data-fetching-app && yarpm install", 17 | "lint:js": "eslint . --ext .js,.jsx", 18 | "prettier": "prettier . --write", 19 | "watch:hubl": "hs watch data-fetching-theme data-fetching-theme", 20 | "upload:hubl": "hs upload data-fetching-theme", 21 | "deploy": "hs project upload" 22 | }, 23 | "engines": { 24 | "node": ">=20.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/getting-started-project-theme/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | sourceType: 'module', 4 | ecmaFeatures: { 5 | jsx: true, 6 | }, 7 | }, 8 | env: { 9 | node: true, 10 | es2021: true, 11 | }, 12 | extends: ['eslint:recommended', 'prettier', 'plugin:react/recommended'], 13 | rules: { 14 | 'react/react-in-jsx-scope': 'off', 15 | 'react/prop-types': 'off', 16 | }, 17 | settings: { 18 | react: { 19 | version: '18.1', 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /examples/getting-started-project-theme/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | hubspot.config.yml 3 | dist 4 | -------------------------------------------------------------------------------- /examples/getting-started-project-theme/.node-version: -------------------------------------------------------------------------------- 1 | 20.11.0 2 | -------------------------------------------------------------------------------- /examples/getting-started-project-theme/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /examples/getting-started-project-theme/hsproject.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getting-started-project-theme", 3 | "srcDir": "src", 4 | "platformVersion": "2023.2" 5 | } 6 | -------------------------------------------------------------------------------- /examples/getting-started-project-theme/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getting-started-project-theme-example", 3 | "description": "Getting started CMS Themes on Projects", 4 | "license": "Apache-2.0", 5 | "devDependencies": { 6 | "@hubspot/cli": "latest", 7 | "@hubspot/prettier-plugin-hubl": "latest", 8 | "eslint": "^8.24.0", 9 | "eslint-config-prettier": "^8.5.0", 10 | "eslint-plugin-react": "^7.31.10", 11 | "prettier": "^2.7.1", 12 | "yarpm": "^1.2.0" 13 | }, 14 | "scripts": { 15 | "start": "cd src/getting-started-theme && yarpm start", 16 | "postinstall": "cd src/getting-started-theme && yarpm install", 17 | "lint:js": "eslint . --ext .js,.jsx", 18 | "prettier": "prettier . --check", 19 | "deploy": "hs project upload" 20 | }, 21 | "engines": { 22 | "node": ">=16.0.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/getting-started-project-theme/src/getting-started-theme/Globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.css'; 2 | -------------------------------------------------------------------------------- /examples/getting-started-project-theme/src/getting-started-theme/assets/clear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/getting-started-project-theme/src/getting-started-theme/assets/cloudy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/getting-started-project-theme/src/getting-started-theme/components/WeatherCards.tsx: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import weatherStyles from '../styles/weather.module.css'; 3 | import { getWeatherIcon } from '../utils.ts'; 4 | import { ForecastData } from '../constants.ts'; 5 | 6 | interface WeatherProps { 7 | city: string; 8 | forecast: ForecastData[]; 9 | } 10 | interface CurrentWeatherCardProps { 11 | weatherData: WeatherProps; 12 | } 13 | 14 | export function CurrentWeatherCard({ weatherData }: CurrentWeatherCardProps) { 15 | const { forecast, city } = weatherData; 16 | const currentDay = forecast[0]; 17 | 18 | return ( 19 |
20 |
21 | {`${city}-weather-icon-${currentDay.weather_code}`} 25 |

26 | {currentDay.apparent_temperature_max}° 27 | F 28 |

29 |
30 |

{city}

31 |
32 | ); 33 | } 34 | 35 | interface UpcomingWeatherCardProps { 36 | weatherData: WeatherProps; 37 | } 38 | 39 | export function UpcomingWeatherCard({ weatherData }: UpcomingWeatherCardProps) { 40 | const { city, forecast } = weatherData; 41 | 42 | return ( 43 | <> 44 | {forecast?.map((weather, index: number) => { 45 | if (index === 0) return null; 46 | 47 | return ( 48 |
49 | {dayjs(weather.time).format('dddd')} 50 | {`${city}-weather-icon-${weather.weather_code}`} 54 |

55 | {weather.apparent_temperature_max}° 56 | F 57 |

58 |
59 | ); 60 | })} 61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /examples/getting-started-project-theme/src/getting-started-theme/components/islands/WeatherForecast.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import weatherStyles from '../../styles/weather.module.css'; 3 | import { getWeatherForecast } from '../../utils.ts'; 4 | import { WeatherForecast as WeatherForecastType } from '../../constants.ts'; 5 | import { CurrentWeatherCard, UpcomingWeatherCard } from '../WeatherCards.tsx'; 6 | 7 | interface WeatherForecastProps { 8 | headline: string; 9 | } 10 | 11 | export default function WeatherForecast({ headline }: WeatherForecastProps) { 12 | const [city, setCity] = useState(''); 13 | const [weatherData, setWeatherData] = useState(); 14 | 15 | const handleFetchWeather = () => { 16 | getWeatherForecast(city).then((data) => { 17 | setWeatherData(data); 18 | }); 19 | }; 20 | 21 | const isFetching: boolean = !weatherData; 22 | const hasError: boolean = !isFetching && !!weatherData.error; 23 | const hasWeatherData: boolean = 24 | !isFetching && !hasError && !!weatherData.forecast; 25 | const missingData = !isFetching && !hasWeatherData && !hasError; 26 | 27 | function WeatherForecast({ weatherData }) { 28 | return ( 29 | <> 30 |
31 | 32 |
33 |
34 | 35 |
36 | 37 | ); 38 | } 39 | 40 | return ( 41 |
42 |

{headline}

43 |
44 | setCity(event.target.value)} 48 | /> 49 | 50 |
51 |
52 | {isFetching &&

Search for a city to see the weather forecast

} 53 | {hasError &&

Error occurred when fetching weather forecast

} 54 | {hasWeatherData && } 55 | {missingData && ( 56 |

No results found for "{city}", please search another location

57 | )} 58 |
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /examples/getting-started-project-theme/src/getting-started-theme/components/modules/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import { ModuleFields, TextField } from '@hubspot/cms-components/fields'; 2 | import footerStyles from '../../../styles/footer.module.css'; 3 | 4 | export function Component({ fieldValues }: any) { 5 | return ( 6 | 9 | ); 10 | } 11 | 12 | export const fields = ( 13 | 14 | 15 | 16 | ); 17 | 18 | export const meta = { 19 | label: 'Footer Module', 20 | }; 21 | -------------------------------------------------------------------------------- /examples/getting-started-project-theme/src/getting-started-theme/components/modules/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Menu, logInfo } from '@hubspot/cms-components'; 3 | import { 4 | ImageField, 5 | MenuField, 6 | ModuleFields, 7 | } from '@hubspot/cms-components/fields'; 8 | import logo from '../../../assets/sprocket.svg'; 9 | import headerStyles from '../../../styles/header.module.css'; 10 | 11 | export function Component({ fieldValues }: any) { 12 | const { src, alt, width, height } = fieldValues.logo; 13 | 14 | return ( 15 |
16 | 20 |
21 | ); 22 | } 23 | 24 | const DEFAULT_MENU_ID = ' 28 | 34 | 35 | 36 | ); 37 | 38 | export const meta = { 39 | label: 'Header Module', 40 | }; 41 | -------------------------------------------------------------------------------- /examples/getting-started-project-theme/src/getting-started-theme/components/modules/Weather/index.tsx: -------------------------------------------------------------------------------- 1 | import { Island } from '@hubspot/cms-components'; 2 | import WeatherForecast from '../../islands/WeatherForecast.tsx?island'; 3 | import { ModuleFields, TextField } from '@hubspot/cms-components/fields'; 4 | 5 | export function Component({ fieldValues }: any) { 6 | const { headline } = fieldValues; 7 | return ; 8 | } 9 | 10 | export const fields = ( 11 | 12 | 17 | 18 | ); 19 | 20 | export const meta = { 21 | label: 'Weather Module', 22 | }; 23 | -------------------------------------------------------------------------------- /examples/getting-started-project-theme/src/getting-started-theme/constants.ts: -------------------------------------------------------------------------------- 1 | import CLEAR from './assets/clear.svg'; 2 | import RAIN from './assets/rain.svg'; 3 | import SNOW from './assets/snow.svg'; 4 | import THUNDERSTORM from './assets/thunderstorm.svg'; 5 | import FOG from './assets/fog.svg'; 6 | import CLOUDY from './assets/cloudy.svg'; 7 | 8 | export const ICON_MAP = { 9 | CLEAR, 10 | RAIN, 11 | SNOW, 12 | THUNDERSTORM, 13 | FOG, 14 | CLOUDY, 15 | }; 16 | 17 | export const FORECAST_BASE_URL = 'https://api.open-meteo.com/v1/forecast'; // https://open-meteo.com/en/docs 18 | export const LAT_LNG_BASE_URL = 19 | 'https://geocoding-api.open-meteo.com/v1/search'; 20 | 21 | export interface Forecast { 22 | time: string[]; 23 | apparent_temperature_max: number[]; 24 | weather_code: number[]; 25 | } 26 | 27 | export interface ForecastData { 28 | time: string; 29 | apparent_temperature_max: number; 30 | weather_code: number; 31 | } 32 | 33 | interface LocationResult { 34 | latitude: number; 35 | longitude: number; 36 | } 37 | 38 | export interface LocationResponse { 39 | results: LocationResult[]; 40 | } 41 | 42 | export interface ForecastResponse { 43 | daily: Forecast; 44 | } 45 | 46 | export interface WeatherForecast { 47 | city: string; 48 | forecast: ForecastData[]; 49 | error?: string; 50 | } 51 | 52 | interface DailyUnits { 53 | time: string; 54 | apparent_temperature_max: string; 55 | weather_code: string; 56 | } 57 | 58 | export interface ApiResponse { 59 | latitude: number; 60 | longitude: number; 61 | generationtime_ms: number; 62 | utc_offset_seconds: number; 63 | timezone: string; 64 | timezone_abbreviation: string; 65 | elevation: number; 66 | daily_units: DailyUnits; 67 | daily: Forecast; 68 | } 69 | -------------------------------------------------------------------------------- /examples/getting-started-project-theme/src/getting-started-theme/fields.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /examples/getting-started-project-theme/src/getting-started-theme/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getting-started-example-app", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "dependencies": { 6 | "@hubspot/cms-components": "latest", 7 | "dayjs": "^1.11.11", 8 | "prop-types": "^15.8.1", 9 | "react": "^18.1.0" 10 | }, 11 | "devDependencies": { 12 | "@hubspot/cms-dev-server": "latest", 13 | "@testing-library/react": "^13.4.0", 14 | "@vitejs/plugin-react": "^2.1.0", 15 | "jsdom": "^20.0.1", 16 | "vitest": "^0.24.3" 17 | }, 18 | "scripts": { 19 | "start": "hs-cms-dev-server . --ssl", 20 | "test": "vitest" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/getting-started-project-theme/src/getting-started-theme/styles/footer.module.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | text-align: center; 3 | font-weight: bold; 4 | font-size: 2em; 5 | } 6 | -------------------------------------------------------------------------------- /examples/getting-started-project-theme/src/getting-started-theme/styles/header.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | width: 100%; 3 | background-color: transparent; 4 | 5 | & nav { 6 | display: flex; 7 | align-items: center; 8 | justify-content: space-between; 9 | flex-direction: row; 10 | padding: 15px 25px; 11 | 12 | & ul { 13 | gap: 20px; 14 | list-style: none; 15 | margin: 0; 16 | padding: 0; 17 | display: flex; 18 | 19 | & li { 20 | & a { 21 | text-decoration: none; 22 | color: #ff7a59; 23 | &:hover { 24 | color: #2d3e50; 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/getting-started-project-theme/src/getting-started-theme/templates/layouts/base.hubl.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | {% if page_meta.html_title or pageTitle %} 9 | {{ page_meta.html_title or pageTitle }} 10 | {% endif %} 11 | {% if brand_settings.primaryFavicon.src %} 12 | 13 | {% endif %} 14 | 15 | {{ standard_header_includes }} 16 | 17 | 18 |
19 | {% block header %} 20 | {% module 'main header' path="../../components/modules/Header" %} 21 | {% endblock header %} 22 | 23 | {# The main-content ID is used for the navigation skipper in the header.html file. More information on the navigation skipper can be found here: https://github.com/HubSpot/cms-theme-boilerplate/wiki/Accessibility #} 24 | 25 | {% block body %} 26 | {% endblock body %} 27 | 28 | {% block footer %} 29 | {% module 'footer' path="../../components/modules/Footer" %} 30 | {% endblock footer %} 31 |
32 | 33 | {# To see a full list of what is included via standard_footer_includes please reference this article: https://developers.hubspot.com/docs/cms/hubl/variables#required-page-template-variables #} 34 | {{ standard_footer_includes }} 35 | 36 | 37 | -------------------------------------------------------------------------------- /examples/getting-started-project-theme/src/getting-started-theme/templates/weather.hubl.html: -------------------------------------------------------------------------------- 1 | 6 | {% extends "./layouts/base.hubl.html" %} 7 | 8 | {% block body %} 9 | 10 | {% module 11 | "weather" 12 | path="../components/modules/Weather", 13 | %} 14 | 15 | {% endblock body %} 16 | -------------------------------------------------------------------------------- /examples/getting-started-project-theme/src/getting-started-theme/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "CMS React - Getting Started Project Theme" 3 | } 4 | -------------------------------------------------------------------------------- /examples/getting-started-project-theme/src/getting-started-theme/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "allowImportingTsExtensions": true, 10 | "noEmit": true, 11 | "isolatedModules": true, 12 | "jsx": "react-jsx", 13 | "skipLibCheck": true, 14 | "types": ["vite/client", "vitest/globals"] 15 | }, 16 | "include": ["components/**/*"], 17 | "exclude": ["node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /examples/getting-started/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | sourceType: 'module', 4 | ecmaFeatures: { 5 | jsx: true, 6 | }, 7 | }, 8 | env: { 9 | node: true, 10 | es2021: true, 11 | }, 12 | extends: ['eslint:recommended', 'prettier', 'plugin:react/recommended'], 13 | rules: { 14 | 'react/react-in-jsx-scope': 'off', 15 | 'react/prop-types': 'off', 16 | }, 17 | settings: { 18 | react: { 19 | version: '18.1', 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /examples/getting-started/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | hubspot.config.yml 3 | dist 4 | -------------------------------------------------------------------------------- /examples/getting-started/.node-version: -------------------------------------------------------------------------------- 1 | 20.11.0 2 | -------------------------------------------------------------------------------- /examples/getting-started/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /examples/getting-started/getting-started-project/getting-started-app/Globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.css'; 2 | -------------------------------------------------------------------------------- /examples/getting-started/getting-started-project/getting-started-app/assets/clear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/getting-started/getting-started-project/getting-started-app/assets/cloudy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/getting-started/getting-started-project/getting-started-app/cms-assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Getting started", 3 | "outputPath": "" 4 | } 5 | -------------------------------------------------------------------------------- /examples/getting-started/getting-started-project/getting-started-app/components/WeatherCards.tsx: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import weatherStyles from '../styles/weather.module.css'; 3 | import { getWeatherIcon } from '../utils.ts'; 4 | import { ForecastData } from '../constants.ts'; 5 | 6 | interface WeatherProps { 7 | city: string; 8 | forecast: ForecastData[]; 9 | } 10 | interface CurrentWeatherCardProps { 11 | weatherData: WeatherProps; 12 | } 13 | 14 | export function CurrentWeatherCard({ weatherData }: CurrentWeatherCardProps) { 15 | const { forecast, city } = weatherData; 16 | const currentDay = forecast[0]; 17 | 18 | return ( 19 |
20 |
21 | {`${city}-weather-icon-${currentDay.weather_code}`} 25 |

26 | {currentDay.apparent_temperature_max}° 27 | F 28 |

29 |
30 |

{city}

31 |
32 | ); 33 | } 34 | 35 | interface UpcomingWeatherCardProps { 36 | weatherData: WeatherProps; 37 | } 38 | 39 | export function UpcomingWeatherCard({ weatherData }: UpcomingWeatherCardProps) { 40 | const { city, forecast } = weatherData; 41 | 42 | return ( 43 | <> 44 | {forecast?.map((weather, index: number) => { 45 | if (index === 0) return null; 46 | 47 | return ( 48 |
49 | {dayjs(weather.time).format('dddd')} 50 | {`${city}-weather-icon-${weather.weather_code}`} 54 |

55 | {weather.apparent_temperature_max}° 56 | F 57 |

58 |
59 | ); 60 | })} 61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /examples/getting-started/getting-started-project/getting-started-app/components/modules/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import { ModuleFields, TextField } from '@hubspot/cms-components/fields'; 2 | import footerStyles from '../../../styles/footer.module.css'; 3 | 4 | export function Component({ fieldValues }: any) { 5 | return ( 6 |
7 |

{fieldValues.footerText}

8 |
9 | ); 10 | } 11 | 12 | export const fields = ( 13 | 14 | 15 | 16 | ); 17 | 18 | export const meta = { 19 | label: 'Footer Module', 20 | }; 21 | -------------------------------------------------------------------------------- /examples/getting-started/getting-started-project/getting-started-app/components/modules/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Menu, logInfo } from '@hubspot/cms-components'; 3 | import { 4 | ImageField, 5 | MenuField, 6 | ModuleFields, 7 | } from '@hubspot/cms-components/fields'; 8 | import logo from '../../../assets/sprocket.svg'; 9 | import headerStyles from '../../../styles/header.module.css'; 10 | 11 | export function Component({ fieldValues }: any) { 12 | const { src, alt, width, height } = fieldValues.logo; 13 | 14 | return ( 15 |
16 | 20 |
21 | ); 22 | } 23 | 24 | const DEFAULT_MENU_ID = ' 28 | 34 | 35 | 36 | ); 37 | 38 | export const meta = { 39 | label: 'Header Module', 40 | }; 41 | -------------------------------------------------------------------------------- /examples/getting-started/getting-started-project/getting-started-app/components/modules/Weather/index.tsx: -------------------------------------------------------------------------------- 1 | import { Island } from '@hubspot/cms-components'; 2 | import WeatherForecast from '../../islands/WeatherForecast.tsx?island'; 3 | import { ModuleFields, TextField } from '@hubspot/cms-components/fields'; 4 | 5 | export function Component({ fieldValues }: any) { 6 | const { headline } = fieldValues; 7 | return ; 8 | } 9 | 10 | export const fields = ( 11 | 12 | 17 | 18 | ); 19 | 20 | export const meta = { 21 | label: 'Weather Module', 22 | }; 23 | -------------------------------------------------------------------------------- /examples/getting-started/getting-started-project/getting-started-app/constants.ts: -------------------------------------------------------------------------------- 1 | import CLEAR from './assets/clear.svg'; 2 | import RAIN from './assets/rain.svg'; 3 | import SNOW from './assets/snow.svg'; 4 | import THUNDERSTORM from './assets/thunderstorm.svg'; 5 | import FOG from './assets/fog.svg'; 6 | import CLOUDY from './assets/cloudy.svg'; 7 | 8 | export const ICON_MAP = { 9 | CLEAR, 10 | RAIN, 11 | SNOW, 12 | THUNDERSTORM, 13 | FOG, 14 | CLOUDY, 15 | }; 16 | 17 | export const FORECAST_BASE_URL = 'https://api.open-meteo.com/v1/forecast'; // https://open-meteo.com/en/docs 18 | export const LAT_LNG_BASE_URL = 19 | 'https://geocoding-api.open-meteo.com/v1/search'; 20 | 21 | export interface Forecast { 22 | time: string[]; 23 | apparent_temperature_max: number[]; 24 | weather_code: number[]; 25 | } 26 | 27 | export interface ForecastData { 28 | time: string; 29 | apparent_temperature_max: number; 30 | weather_code: number; 31 | } 32 | 33 | interface LocationResult { 34 | latitude: number; 35 | longitude: number; 36 | } 37 | 38 | export interface LocationResponse { 39 | results: LocationResult[]; 40 | } 41 | 42 | export interface ForecastResponse { 43 | daily: Forecast; 44 | } 45 | 46 | export interface WeatherForecast { 47 | city: string; 48 | forecast: ForecastData[]; 49 | error?: string; 50 | } 51 | 52 | interface DailyUnits { 53 | time: string; 54 | apparent_temperature_max: string; 55 | weather_code: string; 56 | } 57 | 58 | export interface ApiResponse { 59 | latitude: number; 60 | longitude: number; 61 | generationtime_ms: number; 62 | utc_offset_seconds: number; 63 | timezone: string; 64 | timezone_abbreviation: string; 65 | elevation: number; 66 | daily_units: DailyUnits; 67 | daily: Forecast; 68 | } 69 | -------------------------------------------------------------------------------- /examples/getting-started/getting-started-project/getting-started-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getting-started-example-app", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "dependencies": { 6 | "@hubspot/cms-components": "latest", 7 | "dayjs": "^1.11.11", 8 | "prop-types": "^15.8.1", 9 | "react": "^18.1.0" 10 | }, 11 | "devDependencies": { 12 | "@hubspot/cms-dev-server": "latest", 13 | "@testing-library/react": "^13.4.0", 14 | "@vitejs/plugin-react": "^2.1.0", 15 | "jsdom": "^20.0.1", 16 | "vitest": "^0.24.3" 17 | }, 18 | "scripts": { 19 | "start": "hs-cms-dev-server .", 20 | "test": "vitest" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/getting-started/getting-started-project/getting-started-app/styles/footer.module.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | text-align: center; 3 | font-weight: bold; 4 | font-size: 2em; 5 | } 6 | -------------------------------------------------------------------------------- /examples/getting-started/getting-started-project/getting-started-app/styles/header.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | width: 100%; 3 | background-color: transparent; 4 | 5 | & nav { 6 | display: flex; 7 | align-items: center; 8 | justify-content: space-between; 9 | flex-direction: row; 10 | padding: 15px 25px; 11 | 12 | & ul { 13 | gap: 20px; 14 | list-style: none; 15 | margin: 0; 16 | padding: 0; 17 | display: flex; 18 | 19 | & li { 20 | & a { 21 | text-decoration: none; 22 | color: #ff7a59; 23 | &:hover { 24 | color: #2d3e50; 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/getting-started/getting-started-project/getting-started-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "allowImportingTsExtensions": true, 10 | "noEmit": true, 11 | "isolatedModules": true, 12 | "jsx": "react-jsx", 13 | "skipLibCheck": true, 14 | "types": ["vite/client", "vitest/globals"] 15 | }, 16 | "include": ["components/**/*"], 17 | "exclude": ["node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /examples/getting-started/getting-started-project/hsproject.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getting-started-project", 3 | "srcDir": ".", 4 | "platformVersion": "2023.2" 5 | } 6 | -------------------------------------------------------------------------------- /examples/getting-started/getting-started-theme/fields.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /examples/getting-started/getting-started-theme/layouts/base.hubl.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | {% if page_meta.html_title or pageTitle %} 9 | {{ page_meta.html_title or pageTitle }} 10 | {% endif %} 11 | {% if brand_settings.primaryFavicon.src %} 12 | 13 | {% endif %} 14 | 15 | {{ standard_header_includes }} 16 | 17 | 18 |
19 | {% block header %} 20 | {% module 'main header' path="@projects/getting-started-project/getting-started-app/components/modules/Header" %} 21 | {% endblock header %} 22 | 23 | {# The main-content ID is used for the navigation skipper in the header.html file. More information on the navigation skipper can be found here: https://github.com/HubSpot/cms-theme-boilerplate/wiki/Accessibility #} 24 | 25 | {% block body %} 26 | {% endblock body %} 27 | 28 | {% block footer %} 29 | {% module 'footer' path="@projects/getting-started-project/getting-started-app/components/modules/Footer" %} 30 | {% endblock footer %} 31 |
32 | 33 | {# To see a full list of what is included via standard_footer_includes please reference this article: https://developers.hubspot.com/docs/cms/hubl/variables#required-page-template-variables #} 34 | {{ standard_footer_includes }} 35 | 36 | 37 | -------------------------------------------------------------------------------- /examples/getting-started/getting-started-theme/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "CMS React - Getting Started Theme" 3 | } 4 | -------------------------------------------------------------------------------- /examples/getting-started/getting-started-theme/weather.hubl.html: -------------------------------------------------------------------------------- 1 | 6 | {% extends "./layouts/base.hubl.html" %} 7 | 8 | {% block body %} 9 | 10 | {% module 11 | "weather" 12 | path="@projects/getting-started-project/getting-started-app/components/modules/Weather", 13 | %} 14 | 15 | {% endblock body %} 16 | -------------------------------------------------------------------------------- /examples/getting-started/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getting-started-example", 3 | "description": "Getting started with CMS-React", 4 | "license": "Apache-2.0", 5 | "devDependencies": { 6 | "@hubspot/cli": "latest", 7 | "@hubspot/prettier-plugin-hubl": "latest", 8 | "eslint": "^8.24.0", 9 | "eslint-config-prettier": "^8.5.0", 10 | "eslint-plugin-react": "^7.31.10", 11 | "prettier": "^2.7.1", 12 | "yarpm": "^1.2.0" 13 | }, 14 | "scripts": { 15 | "start": "cd getting-started-project/getting-started-app && yarpm start", 16 | "postinstall": "cd getting-started-project/getting-started-app && yarpm install", 17 | "lint:js": "eslint . --ext .js,.jsx", 18 | "prettier": "prettier . --check", 19 | "watch:hubl": "hs watch getting-started-theme getting-started-theme", 20 | "upload:hubl": "hs upload getting-started-theme getting-started-theme", 21 | "deploy": "hs project upload" 22 | }, 23 | "engines": { 24 | "node": ">=16.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/graphql-storybook/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | sourceType: 'module', 4 | ecmaFeatures: { 5 | jsx: true, 6 | }, 7 | }, 8 | env: { 9 | node: true, 10 | es2021: true, 11 | }, 12 | extends: ['eslint:recommended', 'prettier', 'plugin:react/recommended'], 13 | rules: { 14 | 'react/react-in-jsx-scope': 'off', 15 | 'react/prop-types': 'off', 16 | }, 17 | ignorePatterns: ['hello-world-project/cms-assets/dist/*'], 18 | settings: { 19 | react: { 20 | version: '18.1', 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /examples/graphql-storybook/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | hubspot.config.yml 3 | dist -------------------------------------------------------------------------------- /examples/graphql-storybook/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "overrides": [ 7 | { 8 | "files": "*.hubl.html", 9 | "options": { 10 | "parser": "hubl" 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /examples/graphql-storybook/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 HubSpot, Inc. 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. -------------------------------------------------------------------------------- /examples/graphql-storybook/gql-storybook-project/.hsignore: -------------------------------------------------------------------------------- 1 | README.md 2 | tsconfig.json 3 | yarn.lock 4 | dist 5 | -------------------------------------------------------------------------------- /examples/graphql-storybook/gql-storybook-project/gql-storybook-app/.node-version: -------------------------------------------------------------------------------- 1 | 16.13.2 2 | -------------------------------------------------------------------------------- /examples/graphql-storybook/gql-storybook-project/gql-storybook-app/cms-assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "JS Building Blocks - GQL Storybook", 3 | "outputPath": "" 4 | } 5 | -------------------------------------------------------------------------------- /examples/graphql-storybook/gql-storybook-project/gql-storybook-app/components/modules/ContactList/ContactList.stories.jsx: -------------------------------------------------------------------------------- 1 | import { moduleStory } from '@hubspot/cms-dev-server/storybook'; 2 | 3 | import { Component, fields } from './index.tsx'; 4 | 5 | export default { 6 | title: 'ContactTable', 7 | component: Component, 8 | }; 9 | 10 | export const Default = moduleStory(Component, fields, { 11 | dataQueryResult: { 12 | data: { 13 | CRM: { 14 | contact_collection: { 15 | items: [ 16 | { 17 | firstname: 'Cool', 18 | lastname: 'Robot', 19 | email: 'coolrobot@hubspot.com', 20 | company: 'HubSpot', 21 | _metadata: { id: 1 }, 22 | }, 23 | { 24 | firstname: 'Brian', 25 | lastname: 'Halligan', 26 | email: 'bh@hubspot.com', 27 | company: 'HubSpot', 28 | _metadata: { id: 2 }, 29 | }, 30 | ], 31 | }, 32 | }, 33 | }, 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /examples/graphql-storybook/gql-storybook-project/gql-storybook-app/components/modules/ContactList/fields.tsx: -------------------------------------------------------------------------------- 1 | import { ModuleFields, ChoiceField } from '@hubspot/cms-components/fields'; 2 | 3 | export const fields = ( 4 | 5 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /examples/graphql-storybook/gql-storybook-project/gql-storybook-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gql-storybook-app", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "dependencies": { 6 | "@emotion/react": "^11.11.0", 7 | "@emotion/styled": "^11.11.0", 8 | "@hubspot/cms-components": "latest", 9 | "@mui/material": "^5.13.0", 10 | "prop-types": "^15.8.1", 11 | "react": "^18.1.0" 12 | }, 13 | "devDependencies": { 14 | "@hubspot/cms-dev-server": "latest", 15 | "@testing-library/react": "^13.4.0", 16 | "@vitejs/plugin-react": "^2.1.0", 17 | "jsdom": "^20.0.1", 18 | "vitest": "^0.24.3" 19 | }, 20 | "scripts": { 21 | "start": "hs-cms-dev-server . --storybook", 22 | "test": "vitest" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/graphql-storybook/gql-storybook-project/gql-storybook-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "jsx": "react-jsx", 10 | "moduleResolution": "node16" 11 | }, 12 | "$schema": "https://json.schemastore.org/tsconfig", 13 | "display": "Recommended" 14 | } 15 | -------------------------------------------------------------------------------- /examples/graphql-storybook/gql-storybook-project/gql-storybook-app/vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import { default as react } from '@vitejs/plugin-react'; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | test: { 7 | environment: 'jsdom', 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /examples/graphql-storybook/gql-storybook-project/hsproject.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gql-storybook-project", 3 | "srcDir": ".", 4 | "platformVersion": "2023.2" 5 | } 6 | -------------------------------------------------------------------------------- /examples/graphql-storybook/gql-storybook-theme/fields.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "Color", 4 | "name": "brand_color", 5 | "label": "Brand Color", 6 | "default": { 7 | "color": "#FF7A59", 8 | "opacity": 100 9 | } 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /examples/graphql-storybook/gql-storybook-theme/gql-storybook.hubl.html: -------------------------------------------------------------------------------- 1 | 6 | {% extends "./layouts/base.hubl.html" %} 7 | {% block body %} 8 | {% module "contact_list" 9 | path="@projects/gql-storybook-project/gql-storybook-app/components/modules/ContactList" %} 10 | {% endblock body %} 11 | -------------------------------------------------------------------------------- /examples/graphql-storybook/gql-storybook-theme/layouts/base.hubl.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | {% if page_meta.html_title or pageTitle %} 9 | {{ page_meta.html_title or pageTitle }} 10 | {% endif %} 11 | {% if brand_settings.primaryFavicon.src %} 12 | 13 | {% endif %} 14 | 15 | {{ standard_header_includes }} 16 | 17 | 18 |
19 | {% block header %} 20 | {% endblock header %} 21 | 22 | {% block body %} 23 | {% endblock body %} 24 | 25 | {% block footer %} 26 | {% endblock footer %} 27 |
28 | 29 | {# To see a full list of what is included via standard_footer_includes please reference this article: https://developers.hubspot.com/docs/cms/hubl/variables#required-page-template-variables #} 30 | {{ standard_footer_includes }} 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/graphql-storybook/gql-storybook-theme/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "JS Building Blocks - GraphQL Storybook", 3 | "preview_path": "./home.hubl.html" 4 | } 5 | -------------------------------------------------------------------------------- /examples/graphql-storybook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-building-blocks-graphql-storybook", 3 | "description": "JS Building Blocks GrapQL and Storybook", 4 | "license": "Apache-2.0", 5 | "devDependencies": { 6 | "@hubspot/cli": "latest", 7 | "@hubspot/prettier-plugin-hubl": "latest", 8 | "eslint": "^8.24.0", 9 | "eslint-config-prettier": "^8.5.0", 10 | "eslint-plugin-react": "^7.31.10", 11 | "prettier": "^2.7.1", 12 | "yarpm": "^1.2.0" 13 | }, 14 | "scripts": { 15 | "start": "cd gql-storybook-project/gql-storybook-app && yarpm start", 16 | "lint:js": "eslint . --ext .js,.jsx", 17 | "prettier": "prettier . --check", 18 | "watch:hubl": "hs watch gql-storybook-theme gql-storybook-theme", 19 | "upload:hubl": "hs upload gql-storybook-theme gql-storybook-theme", 20 | "deploy": "hs project upload" 21 | }, 22 | "dependencies": {}, 23 | "engines": { 24 | "node": ">=16.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/hello-world/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | sourceType: 'module', 4 | ecmaFeatures: { 5 | jsx: true, 6 | }, 7 | }, 8 | env: { 9 | node: true, 10 | es2021: true, 11 | }, 12 | extends: ['eslint:recommended', 'prettier', 'plugin:react/recommended'], 13 | rules: { 14 | 'react/react-in-jsx-scope': 'off', 15 | 'react/prop-types': 'off', 16 | }, 17 | ignorePatterns: ['hello-world-project/hello-world-app/dist/*'], 18 | settings: { 19 | react: { 20 | version: '18.1', 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /examples/hello-world/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | hubspot.config.yml 3 | dist -------------------------------------------------------------------------------- /examples/hello-world/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "overrides": [ 7 | { 8 | "files": "*.hubl.html", 9 | "options": { 10 | "parser": "hubl" 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/.hsignore: -------------------------------------------------------------------------------- 1 | README.md 2 | tsconfig.json 3 | yarn.lock 4 | dist 5 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/.node-version: -------------------------------------------------------------------------------- 1 | 16.13.2 2 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/assets/huayra.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/cms-react/6f4a05d12d121080d518363fc5643e90a80c6ab7/examples/hello-world/hello-world-project/hello-world-app/assets/huayra.jpg -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/assets/orange-line.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/cms-react/6f4a05d12d121080d518363fc5643e90a80c6ab7/examples/hello-world/hello-world-project/hello-world-app/assets/orange-line.jpg -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/assets/tacoma.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/cms-react/6f4a05d12d121080d518363fc5643e90a80c6ab7/examples/hello-world/hello-world-project/hello-world-app/assets/tacoma.jpg -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/assets/vw-id.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/cms-react/6f4a05d12d121080d518363fc5643e90a80c6ab7/examples/hello-world/hello-world-project/hello-world-app/assets/vw-id.jpg -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/cms-assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Hello World - React", 3 | "outputPath": "" 4 | } 5 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/components/HubSpotSprocket.jsx: -------------------------------------------------------------------------------- 1 | function HubSpotSprocket(props) { 2 | return ( 3 | 4 | 12 | 13 | ); 14 | } 15 | 16 | export default HubSpotSprocket; 17 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/components/Layout.jsx: -------------------------------------------------------------------------------- 1 | import layoutStyles from '../styles/layout.module.css'; 2 | 3 | function Layout({ children }) { 4 | return
{children}
; 5 | } 6 | 7 | export default Layout; 8 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/components/Link.jsx: -------------------------------------------------------------------------------- 1 | function Link(props) { 2 | return {props.title}; 3 | } 4 | 5 | export default Link; 6 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/components/islands/Button.jsx: -------------------------------------------------------------------------------- 1 | import styles from '../../styles/todo.module.css'; 2 | 3 | function Button(props) { 4 | return ( 5 | 13 | ); 14 | } 15 | 16 | export default Button; 17 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/components/modules/TodoList/fields.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | FieldGroup, 4 | ModuleFields, 5 | BooleanField, 6 | TextField, 7 | ColorField, 8 | NumberField, 9 | } from '@hubspot/cms-components/fields'; 10 | 11 | /** 12 | * Here we are defining module fields that will show up for marketers in the page editor so they can customize the module 13 | * We also define default field values 14 | */ 15 | export const fields = ( 16 | 17 | 18 | 24 | 25 | 26 | 31 | 40 | 41 | ); 42 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/components/modules/TodoList/index.jsx: -------------------------------------------------------------------------------- 1 | import { Island } from '@hubspot/cms-components'; 2 | import TodoList from '../../islands/TodoList.jsx?island'; 3 | import Layout from '../../Layout.jsx'; 4 | 5 | /** 6 | * Here we are exporting the 3 requirements of a module: Component, fields, and meta 7 | * 8 | * 'props' contains the resolved values from the defined module fields (./fields.jsx) 9 | * This will be the value provided by a marketer in the page editor, or will fallback to the defined default value 10 | * e.g., props = { 11 | * "default_todo": {"text": "Todo Test 1", "completed": false} 12 | * } 13 | * 14 | * is used here to enable client-side interactivty of a component 15 | * the `hydrateOn` prop allows customization of hydration, i.e., hydrate on page load, or hydrate when the component first becomes visible 16 | * 17 | * The Island component takes on the props of the component it is wrapping 18 | * Note: only props that can be serialized are supported 19 | */ 20 | export const Component = ({ fieldValues, hublParameters = {} }) => { 21 | const { 22 | default_todo: defaultTodos, 23 | button_color: buttonColor, 24 | complete_todo_opacity: completeTodoOpacity, 25 | } = fieldValues; 26 | const { title } = hublParameters; 27 | return ( 28 | 29 |

{title || 'Todo'}

30 | 38 |
39 | ); 40 | }; 41 | export { fields } from './fields.jsx'; 42 | export const meta = { 43 | label: `Todo List Module`, 44 | }; 45 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/components/partials/Cars.jsx: -------------------------------------------------------------------------------- 1 | import Layout from '../Layout.jsx'; 2 | 3 | import styles from '../../styles/cars.module.css'; 4 | 5 | import tacoma from '../../assets/tacoma.jpg'; 6 | import vwid from '../../assets/vw-id.jpg'; 7 | import orangeLine from '../../assets/orange-line.jpg'; 8 | import huayra from '../../assets/huayra.jpg'; 9 | 10 | const cars = [ 11 | { 12 | year: 2009, 13 | make: 'Toyota', 14 | model: 'Tacoma', 15 | id: 1, 16 | img: tacoma, 17 | contact: { 18 | firstname: 'Byron', 19 | lastname: 'Matto', 20 | }, 21 | }, 22 | { 23 | year: 1979, 24 | make: 'MBTA', 25 | model: 'Orange Line', 26 | id: 2, 27 | img: orangeLine, 28 | contact: { 29 | firstname: 'Ben', 30 | lastname: 'Briggs', 31 | }, 32 | }, 33 | { 34 | year: 2021, 35 | make: 'Volkswagen', 36 | model: 'ID', 37 | id: 3, 38 | img: vwid, 39 | contact: { 40 | firstname: 'Timothy', 41 | lastname: 'Finley', 42 | }, 43 | }, 44 | { 45 | year: 2019, 46 | make: 'Pagani', 47 | model: 'Huayra', 48 | id: 4, 49 | img: huayra, 50 | contact: { 51 | firstname: 'Charles', 52 | lastname: 'Boamah', 53 | }, 54 | }, 55 | ]; 56 | 57 | function Cars() { 58 | return ( 59 | 60 |

{`My Teammates' Cars`}

61 |
62 | {cars.map((car) => { 63 | return ( 64 |
65 |
66 |
67 |

68 | {car.contact.firstname} {car.contact.lastname} 69 |

70 |

71 | 72 | {car.year} {car.make} {car.model} 73 | 74 |

{' '} 75 |
76 |
77 | 78 |
79 |
80 |
81 | ); 82 | })} 83 |
84 |
85 | ); 86 | } 87 | 88 | export default Cars; 89 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/components/partials/Footer.jsx: -------------------------------------------------------------------------------- 1 | import footerStyles from '../../styles/footer.module.css'; 2 | 3 | function Footer() { 4 | return ( 5 |
6 |

Be Well.

7 |
8 | ); 9 | } 10 | 11 | export default Footer; 12 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/components/partials/Header.jsx: -------------------------------------------------------------------------------- 1 | import headerStyles from '../../styles/header.module.css'; 2 | 3 | function Header({ 4 | brandColor = { 5 | opacity: 100, 6 | color: '#FF7A59', 7 | }, 8 | text = 'Hello from React!', 9 | }) { 10 | const navLinks = [ 11 | { 12 | href: '/hello-world-home', 13 | label: 'Home', 14 | }, 15 | { 16 | href: '/hello-world-todo', 17 | label: 'Todo', 18 | }, 19 | { 20 | href: '/hello-world-cars', 21 | label: 'Cars', 22 | }, 23 | ]; 24 | 25 | return ( 26 |
27 |

{text}

28 | 43 |
44 | ); 45 | } 46 | 47 | export default Header; 48 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/components/partials/Home.jsx: -------------------------------------------------------------------------------- 1 | import homeStyles from '../../styles/home.module.css'; 2 | 3 | import Layout from '../Layout.jsx'; 4 | import HubSpotSprocket from '../HubSpotSprocket'; 5 | 6 | function Home() { 7 | return ( 8 | 9 |
10 | 11 |
12 |
13 | ); 14 | } 15 | 16 | export default Home; 17 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world-app", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "dependencies": { 6 | "@hubspot/cms-components": "latest", 7 | "prop-types": "^15.8.1", 8 | "react": "^18.1.0" 9 | }, 10 | "devDependencies": { 11 | "@hubspot/cms-dev-server": "latest", 12 | "@testing-library/react": "^13.4.0", 13 | "@vitejs/plugin-react": "^2.1.0", 14 | "jsdom": "^20.0.1", 15 | "vitest": "^0.24.3" 16 | }, 17 | "scripts": { 18 | "start": "hs-cms-dev-server .", 19 | "test": "vitest" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/styles/cars.module.css: -------------------------------------------------------------------------------- 1 | .cars { 2 | display: flex; 3 | flex-wrap: wrap; 4 | width: 100%; 5 | max-width: 800px; 6 | } 7 | 8 | .carGrid { 9 | width: calc(100% / 2); 10 | flex-shrink: 0; 11 | } 12 | 13 | .car { 14 | margin: 5px; 15 | border: 2px solid #ff7a59; 16 | border-radius: 4px; 17 | padding: 14px 7px; 18 | display: flex; 19 | } 20 | 21 | .car h2 { 22 | font-size: 1em; 23 | } 24 | 25 | .carDetails { 26 | height: 100px; 27 | margin-left: auto; 28 | } 29 | 30 | .carDetails img { 31 | margin: 0; 32 | height: 100%; 33 | width: auto; 34 | } 35 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/styles/footer.module.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | text-align: center; 3 | font-weight: bold; 4 | font-size: 2em; 5 | } 6 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/styles/header.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | text-align: center; 3 | font-weight: bold; 4 | font-size: 1.6em; 5 | } 6 | 7 | .header h1 { 8 | font-size: 2em; 9 | margin-top: 0; 10 | } 11 | 12 | .nav { 13 | display: flex; 14 | } 15 | 16 | .nav a { 17 | flex: 0; 18 | margin: 10px; 19 | padding: 10px; 20 | border-width: 0; 21 | border-style: solid; 22 | border-bottom-width: 1px; 23 | text-decoration: none; 24 | } 25 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/styles/home.module.css: -------------------------------------------------------------------------------- 1 | .logo { 2 | margin: auto; 3 | height: auto; 4 | width: 40vw; 5 | 6 | animation-name: spin; 7 | animation-duration: 5000ms; 8 | animation-iteration-count: infinite; 9 | animation-timing-function: linear; 10 | } 11 | 12 | @keyframes spin { 13 | from { 14 | transform: rotate3d(0, 1, 0, 0deg); 15 | } 16 | to { 17 | transform: rotate3d(0, 1, 0, 360deg); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/styles/layout.module.css: -------------------------------------------------------------------------------- 1 | :global(html) { 2 | height: 100%; 3 | } 4 | 5 | :global(body) { 6 | font-family: Arial, Helvetica, sans-serif; 7 | height: 100%; 8 | margin: 0; 9 | padding: 0; 10 | } 11 | 12 | :global(.body-wrapper) { 13 | height: 100%; 14 | display: flex; 15 | flex-direction: column; 16 | } 17 | 18 | 19 | :global(.todo-js-module), 20 | .layout { 21 | flex: 1; 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | flex-direction: column; 26 | } 27 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/tests/components/__snapshots__/partials.home.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1 2 | 3 | exports[`Home > renders Home 1`] = ` 4 | 5 | 32 | 33 | `; 34 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/tests/components/partials.home.test.jsx: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { render } from '@testing-library/react'; 3 | 4 | import Home from '../../components/partials/Home.jsx'; 5 | 6 | describe('Home', () => { 7 | it('renders Home', () => { 8 | const { asFragment } = render(); 9 | 10 | expect(asFragment()).toMatchSnapshot(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hello-world-app/vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import { default as react } from '@vitejs/plugin-react'; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | test: { 7 | environment: 'jsdom', 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-project/hsproject.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world-project", 3 | "srcDir": ".", 4 | "platformVersion": "2023.2" 5 | } 6 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-theme/cars.hubl.html: -------------------------------------------------------------------------------- 1 | 6 | {% extends "./layouts/base.hubl.html" %} 7 | 8 | {% block body %} 9 | 10 | {% js_partial 11 | path="@projects/hello-world-project/hello-world-app/components/partials/Cars.jsx", 12 | no_wrapper=True %} 13 | 14 | {% endblock body %} 15 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-theme/fields.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "Color", 4 | "name": "brand_color", 5 | "label": "Brand Color", 6 | "default": { 7 | "color": "#FF7A59", 8 | "opacity": 100 9 | } 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-theme/home.hubl.html: -------------------------------------------------------------------------------- 1 | 6 | {% extends "./layouts/base.hubl.html" %} 7 | 8 | {% block body %} 9 | 10 | {% js_partial 11 | path="@projects/hello-world-project/hello-world-app/components/partials/Home.jsx", 12 | no_wrapper=True %} 13 | 14 | {% dnd_area "dnd_area", class="main" %} 15 | {% dnd_section %} 16 | 17 | {% end_dnd_section %} 18 | {% end_dnd_area %} 19 | 20 | {% endblock body %} 21 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-theme/layouts/base.hubl.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | {% if page_meta.html_title or pageTitle %} 9 | {{ page_meta.html_title or pageTitle }} 10 | {% endif %} 11 | {% if brand_settings.primaryFavicon.src %} 12 | 13 | {% endif %} 14 | 15 | {{ standard_header_includes }} 16 | 17 | 18 |
19 | {% block header %} 20 | {% js_partial 21 | path="@projects/hello-world-project/hello-world-app/components/partials/Header.jsx", 22 | brandColor="{{ theme.brand_color }}", 23 | text="Hello from HubL!", 24 | no_wrapper=True %} 25 | {% endblock header %} 26 | 27 | {# The main-content ID is used for the navigation skipper in the header.html file. More information on the navigation skipper can be found here: https://github.com/HubSpot/cms-theme-boilerplate/wiki/Accessibility #} 28 | 29 | {% block body %} 30 | {% endblock body %} 31 | 32 | {% block footer %} 33 | {% js_partial 34 | path="@projects/hello-world-project/hello-world-app/components/partials/Footer.jsx", 35 | no_wrapper=True %} 36 | {% endblock footer %} 37 |
38 | 39 | {# To see a full list of what is included via standard_footer_includes please reference this article: https://developers.hubspot.com/docs/cms/hubl/variables#required-page-template-variables #} 40 | {{ standard_footer_includes }} 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-theme/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "JS Building Blocks - Hello World", 3 | "preview_path": "./home.hubl.html" 4 | } 5 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world-theme/todo.hubl.html: -------------------------------------------------------------------------------- 1 | 6 | {% extends "./layouts/base.hubl.html" %} 7 | 8 | {% block body %} 9 | 10 | {# Can also use the module tag: 11 | {% module "todo_js_module" 12 | path="@projects/hello-world-project/hello-world-app/components/modules/TodoList" 13 | %} 14 | #} 15 | 16 | {% dnd_area "dnd_area", class="main" %} 17 | {% dnd_section %} 18 | {% dnd_module extra_classes="todo-js-module", path="@projects/hello-world-project/hello-world-app/components/modules/TodoList" %} 19 | {% end_dnd_module %} 20 | {% end_dnd_section %} 21 | {% end_dnd_area %} 22 | 23 | {% endblock body %} 24 | -------------------------------------------------------------------------------- /examples/hello-world/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "NodeNext" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/hello-world/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-building-blocks-hello-world", 3 | "description": "JS Building Blocks Hello World", 4 | "license": "Apache-2.0", 5 | "devDependencies": { 6 | "@hubspot/cli": "latest", 7 | "@hubspot/prettier-plugin-hubl": "latest", 8 | "eslint": "^8.24.0", 9 | "eslint-config-prettier": "^8.5.0", 10 | "eslint-plugin-react": "^7.31.10", 11 | "prettier": "^2.7.1", 12 | "yarpm": "^1.2.0" 13 | }, 14 | "scripts": { 15 | "start": "cd hello-world-project/hello-world-app && yarpm start", 16 | "postinstall": "cd hello-world-project/hello-world-app && yarpm install", 17 | "lint:js": "eslint . --ext .js,.jsx", 18 | "prettier": "prettier . --check", 19 | "watch:hubl": "hs watch hello-world-theme hello-world-theme", 20 | "upload:hubl": "hs upload hello-world-theme hello-world-theme", 21 | "deploy": "hs project upload" 22 | }, 23 | "dependencies": {}, 24 | "engines": { 25 | "node": ">=16.0.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/islands/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | sourceType: 'module', 4 | ecmaFeatures: { 5 | jsx: true, 6 | }, 7 | }, 8 | env: { 9 | node: true, 10 | es2021: true, 11 | }, 12 | extends: ['eslint:recommended', 'prettier', 'plugin:react/recommended'], 13 | rules: { 14 | 'react/react-in-jsx-scope': 'off', 15 | 'react/prop-types': 'off', 16 | }, 17 | ignorePatterns: ['hello-world-project/cms-assets/dist/*'], 18 | settings: { 19 | react: { 20 | version: '18.1', 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /examples/islands/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | hubspot.config.yml 3 | dist -------------------------------------------------------------------------------- /examples/islands/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "overrides": [ 7 | { 8 | "files": "*.hubl.html", 9 | "options": { 10 | "parser": "hubl" 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /examples/islands/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 HubSpot, Inc. 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. -------------------------------------------------------------------------------- /examples/islands/islands-project/.hsignore: -------------------------------------------------------------------------------- 1 | README.md 2 | tsconfig.json 3 | yarn.lock 4 | dist 5 | -------------------------------------------------------------------------------- /examples/islands/islands-project/hsproject.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "islands-project", 3 | "srcDir": ".", 4 | "platformVersion": "2023.2" 5 | } 6 | -------------------------------------------------------------------------------- /examples/islands/islands-project/islands-app/.node-version: -------------------------------------------------------------------------------- 1 | 16.13.2 2 | -------------------------------------------------------------------------------- /examples/islands/islands-project/islands-app/Globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.css'; 2 | -------------------------------------------------------------------------------- /examples/islands/islands-project/islands-app/cms-assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "JS Building Blocks - Islands", 3 | "outputPath": "" 4 | } 5 | -------------------------------------------------------------------------------- /examples/islands/islands-project/islands-app/components/ButtonCounter/ButtonCounter.module.css: -------------------------------------------------------------------------------- 1 | .hydrationText { 2 | font-style: italic; 3 | color: #aaa; 4 | margin-left: 1em; 5 | visibility: hidden; 6 | } 7 | 8 | .hydrated .hydrationText { 9 | visibility: visible; 10 | } 11 | 12 | .button { 13 | font-size: inherit; 14 | } 15 | 16 | .button:hover:not(:disabled) { 17 | cursor: pointer; 18 | } 19 | -------------------------------------------------------------------------------- /examples/islands/islands-project/islands-app/components/ButtonCounter/ButtonCounter.tsx: -------------------------------------------------------------------------------- 1 | import { useState, CSSProperties } from 'react'; 2 | import { useAfterIslandHydration } from '@hubspot/cms-components'; 3 | import classes from './ButtonCounter.module.css'; 4 | 5 | type ButtonCounterProps = { 6 | defaultCount: number; 7 | style: CSSProperties; 8 | }; 9 | 10 | type DisabledProps = { 11 | disabled?: boolean; 12 | }; 13 | 14 | const ButtonCounter = (props: ButtonCounterProps) => { 15 | const { defaultCount = 0, style } = props; 16 | const [count, setCount] = useState(defaultCount); 17 | const afterHydration = useAfterIslandHydration(); 18 | 19 | let disabledProps: DisabledProps = { disabled: true }; 20 | 21 | if (afterHydration) { 22 | disabledProps = {}; 23 | } 24 | 25 | return ( 26 | <> 27 |

This is a button!

28 |

29 | 37 | hydrated 38 |

39 | 40 | ); 41 | }; 42 | 43 | export default ButtonCounter; 44 | -------------------------------------------------------------------------------- /examples/islands/islands-project/islands-app/components/ButtonCounter/index.tsx: -------------------------------------------------------------------------------- 1 | export { default } from './ButtonCounter'; 2 | -------------------------------------------------------------------------------- /examples/islands/islands-project/islands-app/components/Clock.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export default function Clock({ startTimestamp, enabled = true }) { 4 | // If curious you can see the imlementation details of useCurrTimestamp below 5 | // but for the sake of starting to understand islands, those details are not critical. 6 | const timestamp = useCurrentTimestamp(startTimestamp, enabled); 7 | 8 | const labelSuffix = enabled ? '(enabled)' : '(disabled)'; 9 | return ( 10 | <> 11 | Time {labelSuffix}: {new Date(timestamp).toLocaleTimeString()} 12 | 13 | ); 14 | } 15 | 16 | // React hook that: 17 | // - Takes an initial timestamp and enabled boolean flag 18 | // - On the server, it will always return the initial timestamp 19 | // - But in the browser, if enabled, it will return newly updated timestamp every second 20 | function useCurrentTimestamp(startTimestamp, enabled) { 21 | const [currTimestamp, setCurrTimestamp] = useState(startTimestamp); 22 | 23 | // This effect that updates the current timestamp state every second will only 24 | // run in the browser. 25 | useEffect(() => { 26 | if (enabled) { 27 | const intervalId = setInterval(() => setCurrTimestamp(Date.now(), 1000)); 28 | 29 | // Cleanup function to stop the timer if/when this component is unmounted 30 | return function cleanup() { 31 | clearInterval(intervalId); 32 | }; 33 | } 34 | }, [enabled]); 35 | 36 | return currTimestamp; 37 | } 38 | -------------------------------------------------------------------------------- /examples/islands/islands-project/islands-app/components/partials/BasicIslandClock.jsx: -------------------------------------------------------------------------------- 1 | import { Island } from '@hubspot/cms-components'; 2 | import ClockIsland from '../Clock?island'; 3 | 4 | export default function BasicIslandClock() { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /examples/islands/islands-project/islands-app/components/partials/IslandsTester.jsx: -------------------------------------------------------------------------------- 1 | import { Island } from '@hubspot/cms-components'; 2 | 3 | import ButtonCounterIsland from '../ButtonCounter?island'; 4 | 5 | const IslandsTester = ({ 6 | numIslands = 1, 7 | marginTop, 8 | marginBetween = '50vh', 9 | hydrateOn = 'load', 10 | islandIdPrefix = '', 11 | }) => { 12 | numIslands = parseInt(numIslands, 10); 13 | 14 | const islandElements = new Array(numIslands).fill('').map((_, i) => { 15 | let style = { marginTop: marginTop ?? marginBetween }; 16 | if (i === 0 && marginTop == null) { 17 | delete style.marginTop; 18 | } 19 | 20 | const islandId = `${islandIdPrefix}island-${i}`; 21 | 22 | return ( 23 | 31 | ); 32 | }); 33 | return
{islandElements}
; 34 | }; 35 | 36 | export default IslandsTester; 37 | -------------------------------------------------------------------------------- /examples/islands/islands-project/islands-app/components/partials/IslandsTesterPlus.jsx: -------------------------------------------------------------------------------- 1 | import IslandTester from './IslandsTester'; 2 | 3 | const IslandsColumn = ({ numIslands, marginBetween, hydrateOn }) => { 4 | return ( 5 |
6 |

On {hydrateOn}

7 | 13 |
14 | ); 15 | }; 16 | const IslandsTesterPlus = ({ numIslands = 1, marginBetween = '50vh' }) => { 17 | return ( 18 |
19 | 24 | 30 | 35 |
36 | ); 37 | }; 38 | 39 | export default IslandsTesterPlus; 40 | -------------------------------------------------------------------------------- /examples/islands/islands-project/islands-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "islands-app", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "dependencies": { 6 | "@hubspot/cms-components": "latest", 7 | "prop-types": "^15.8.1", 8 | "react": "^18.1.0", 9 | "typescript-plugin-css-modules": "^5.0.1" 10 | }, 11 | "devDependencies": { 12 | "@hubspot/cms-dev-server": "latest", 13 | "@testing-library/react": "^13.4.0", 14 | "@vitejs/plugin-react": "^2.1.0", 15 | "jsdom": "^20.0.1", 16 | "vitest": "^0.24.3" 17 | }, 18 | "scripts": { 19 | "start": "hs-cms-dev-server .", 20 | "test": "vitest" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/islands/islands-project/islands-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "module": "commonjs", 5 | "moduleResolution": "node16", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "jsx": "react-jsx", 11 | "plugins": [ 12 | { 13 | "name": "typescript-plugin-css-modules" 14 | } 15 | ] 16 | }, 17 | "$schema": "https://json.schemastore.org/tsconfig", 18 | "display": "Recommended" 19 | } 20 | -------------------------------------------------------------------------------- /examples/islands/islands-project/islands-app/vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import { default as react } from '@vitejs/plugin-react'; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | test: { 7 | environment: 'jsdom', 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /examples/islands/islands-theme/fields.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "Color", 4 | "name": "brand_color", 5 | "label": "Brand Color", 6 | "default": { 7 | "color": "#FF7A59", 8 | "opacity": 100 9 | } 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /examples/islands/islands-theme/islands.hubl.html: -------------------------------------------------------------------------------- 1 | 6 | {% extends "./layouts/base.hubl.html" %} 7 | 8 | {% block body %} 9 | {% js_partial path="@projects/islands-project/islands-app/components/partials/BasicIslandClock.jsx" %} 10 | 11 | {% js_partial 12 | path="@projects/islands-project/islands-app/components/partials/IslandsTesterPlus.jsx", 13 | numIslands="12" %} 14 | 15 | {% endblock body %} 16 | -------------------------------------------------------------------------------- /examples/islands/islands-theme/layouts/base.hubl.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | {% if page_meta.html_title or pageTitle %} 9 | {{ page_meta.html_title or pageTitle }} 10 | {% endif %} 11 | {% if brand_settings.primaryFavicon.src %} 12 | 13 | {% endif %} 14 | 15 | {{ standard_header_includes }} 16 | 17 | 18 |
19 | {% block header %} 20 | {% endblock header %} 21 | 22 | {% block body %} 23 | {% endblock body %} 24 | 25 | {% block footer %} 26 | {% endblock footer %} 27 |
28 | 29 | {# To see a full list of what is included via standard_footer_includes please reference this article: https://developers.hubspot.com/docs/cms/hubl/variables#required-page-template-variables #} 30 | {{ standard_footer_includes }} 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/islands/islands-theme/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "JS Building Blocks - Islands", 3 | "preview_path": "./islands.hubl.html" 4 | } 5 | -------------------------------------------------------------------------------- /examples/islands/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-building-blocks-hello-world", 3 | "description": "JS Building Blocks Hello World", 4 | "license": "Apache-2.0", 5 | "devDependencies": { 6 | "@hubspot/cli": "latest", 7 | "@hubspot/prettier-plugin-hubl": "latest", 8 | "eslint": "^8.24.0", 9 | "eslint-config-prettier": "^8.5.0", 10 | "eslint-plugin-react": "^7.31.10", 11 | "prettier": "^2.7.1", 12 | "yarpm": "^1.2.0" 13 | }, 14 | "scripts": { 15 | "start": "cd islands-project/islands-app && yarpm start --", 16 | "postinstall": "cd islands-project/islands-app && yarpm install", 17 | "lint:js": "eslint . --ext .js,.jsx", 18 | "prettier": "prettier . --check", 19 | "watch:hubl": "hs watch islands-theme islands-theme", 20 | "upload:hubl": "hs upload islands-theme islands-theme", 21 | "deploy": "hs project upload" 22 | }, 23 | "engines": { 24 | "node": ">=16.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/module-components/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | sourceType: 'module', 4 | ecmaFeatures: { 5 | jsx: true, 6 | }, 7 | }, 8 | env: { 9 | node: true, 10 | es2021: true, 11 | }, 12 | extends: ['eslint:recommended', 'prettier', 'plugin:react/recommended'], 13 | rules: { 14 | 'react/react-in-jsx-scope': 'off', 15 | 'react/prop-types': 'off', 16 | }, 17 | ignorePatterns: ['hello-world-project/hello-world-app/dist/*'], 18 | settings: { 19 | react: { 20 | version: '18.1', 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /examples/module-components/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | hubspot.config.yml 3 | dist -------------------------------------------------------------------------------- /examples/module-components/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "overrides": [ 7 | { 8 | "files": "*.hubl.html", 9 | "options": { 10 | "parser": "hubl" 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /examples/module-components/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "NodeNext" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/module-components/module-components-project/.hsignore: -------------------------------------------------------------------------------- 1 | README.md 2 | tsconfig.json 3 | yarn.lock 4 | dist 5 | -------------------------------------------------------------------------------- /examples/module-components/module-components-project/hsproject.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "module-components-project", 3 | "srcDir": ".", 4 | "platformVersion": "2023.2" 5 | } 6 | -------------------------------------------------------------------------------- /examples/module-components/module-components-project/module-components-app/cms-assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Components - CMS React", 3 | "outputPath": "" 4 | } 5 | -------------------------------------------------------------------------------- /examples/module-components/module-components-project/module-components-app/components/modules/IconExample/index.jsx: -------------------------------------------------------------------------------- 1 | import { ModuleFields, IconField } from '@hubspot/cms-components/fields'; 2 | import { Icon } from '@hubspot/cms-components'; 3 | 4 | export function Component() { 5 | return ( 6 | <> 7 |

Icon Examples

8 |
15 | {/* The Icon component takes a field path as defined in the "fields" export */} 16 | 17 | 18 | 19 |
20 | 21 | ); 22 | } 23 | 24 | export const fields = ( 25 | 26 | 33 | 40 | 47 | 48 | ); 49 | 50 | export const meta = { 51 | label: 'Icon Example', 52 | }; 53 | -------------------------------------------------------------------------------- /examples/module-components/module-components-project/module-components-app/components/modules/RichTextExample/index.jsx: -------------------------------------------------------------------------------- 1 | import { ModuleFields, RichTextField } from '@hubspot/cms-components/fields'; 2 | import { RichText, logInfo } from '@hubspot/cms-components'; 3 | 4 | export function Component(props) { 5 | logInfo(props); 6 | 7 | return ( 8 | <> 9 |

Rich Text Example

10 |
15 | 16 |
17 | 18 | ); 19 | } 20 | 21 | export const fields = ( 22 | 23 | 28 | 29 | ); 30 | export const meta = { 31 | label: 'Rich Text Example', 32 | }; 33 | -------------------------------------------------------------------------------- /examples/module-components/module-components-project/module-components-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "module-components-app", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "dependencies": { 6 | "@hubspot/cms-components": "latest", 7 | "prop-types": "^15.8.1", 8 | "react": "^18.1.0" 9 | }, 10 | "devDependencies": { 11 | "@hubspot/cms-dev-server": "latest", 12 | "@testing-library/react": "^13.4.0", 13 | "@vitejs/plugin-react": "^2.1.0", 14 | "jsdom": "^20.0.1", 15 | "vitest": "^0.24.3" 16 | }, 17 | "scripts": { 18 | "start": "hs-cms-dev-server .", 19 | "test": "vitest" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/module-components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "module-components", 3 | "description": "CMS React Module Components Example", 4 | "license": "Apache-2.0", 5 | "devDependencies": { 6 | "@hubspot/cli": "latest", 7 | "@hubspot/prettier-plugin-hubl": "latest", 8 | "eslint": "^8.24.0", 9 | "eslint-config-prettier": "^8.5.0", 10 | "eslint-plugin-react": "^7.31.10", 11 | "prettier": "^2.7.1", 12 | "yarpm": "^1.2.0" 13 | }, 14 | "scripts": { 15 | "start": "cd module-components-project/module-components-app && yarpm start", 16 | "postinstall": "cd module-components-project/module-components-app && yarpm install", 17 | "lint:js": "eslint . --ext .js,.jsx", 18 | "prettier": "prettier . --check", 19 | "deploy": "hs project upload" 20 | }, 21 | "dependencies": {}, 22 | "engines": { 23 | "node": ">=16.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/routing/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | sourceType: 'module', 4 | ecmaFeatures: { 5 | jsx: true, 6 | }, 7 | }, 8 | env: { 9 | node: true, 10 | es2021: true, 11 | }, 12 | extends: ['eslint:recommended', 'prettier', 'plugin:react/recommended'], 13 | rules: { 14 | 'react/react-in-jsx-scope': 'off', 15 | 'react/prop-types': 'off', 16 | }, 17 | ignorePatterns: ['hello-world-project/cms-assets/dist/*'], 18 | settings: { 19 | react: { 20 | version: '18.1', 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /examples/routing/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | hubspot.config.yml 3 | dist 4 | -------------------------------------------------------------------------------- /examples/routing/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "overrides": [ 7 | { 8 | "files": "*.hubl.html", 9 | "options": { 10 | "parser": "hubl" 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /examples/routing/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 HubSpot, Inc. 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | -------------------------------------------------------------------------------- /examples/routing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cms-react-routing", 3 | "description": "CMS React - Routing", 4 | "license": "Apache-2.0", 5 | "devDependencies": { 6 | "@hubspot/cli": "latest", 7 | "@hubspot/prettier-plugin-hubl": "latest", 8 | "eslint": "^8.24.0", 9 | "eslint-config-prettier": "^8.5.0", 10 | "eslint-plugin-react": "^7.31.10", 11 | "prettier": "^2.7.1", 12 | "yarpm": "^1.2.0" 13 | }, 14 | "scripts": { 15 | "start": "cd routing-project/routing-app && yarpm start --", 16 | "postinstall": "cd routing-project/routing-app && yarpm install", 17 | "lint:js": "eslint . --ext .js,.jsx", 18 | "prettier": "prettier . --check", 19 | "watch:hubl": "hs watch routing-theme routing-theme", 20 | "upload:hubl": "hs upload routing-theme routing-theme", 21 | "deploy": "hs project upload" 22 | }, 23 | "engines": { 24 | "node": ">=16.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/routing/routing-project/hsproject.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "routing-project", 3 | "srcDir": ".", 4 | "platformVersion": "2023.2" 5 | } 6 | -------------------------------------------------------------------------------- /examples/routing/routing-project/routing-app/Global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.css'; 2 | -------------------------------------------------------------------------------- /examples/routing/routing-project/routing-app/assets/dynamic-slug-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/cms-react/6f4a05d12d121080d518363fc5643e90a80c6ab7/examples/routing/routing-project/routing-app/assets/dynamic-slug-screenshot.png -------------------------------------------------------------------------------- /examples/routing/routing-project/routing-app/cms-assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "CMS React - Routing", 3 | "outputPath": "" 4 | } 5 | -------------------------------------------------------------------------------- /examples/routing/routing-project/routing-app/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { NavLink } from 'react-router-dom'; 2 | import navigationStyles from '../styles/header.module.css'; 3 | import pokeball from '../assets/pokeball.svg'; 4 | 5 | export default function Header() { 6 | return ( 7 |
8 | 9 | Pokeball 10 | 11 | 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /examples/routing/routing-project/routing-app/components/Home.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | import pageStyles from '../styles/page.module.css'; 3 | 4 | export default function Home() { 5 | return ( 6 |
7 |

CMS React Routing

8 | See Pokedex 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /examples/routing/routing-project/routing-app/components/ListingCard.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | import cardStyles from '../styles/card.module.css'; 3 | 4 | export function ListingCard({ pokemonList }: { pokemonList: any }) { 5 | return ( 6 |
7 | {pokemonList.map((pokemon) => ( 8 | 9 |
10 | {pokemon.name} 15 |
16 |

{pokemon.pokemon_v2_pokemontypes[0].pokemon_v2_type.name}

17 |

{pokemon.name}

18 |
19 |
20 | 21 | ))} 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /examples/routing/routing-project/routing-app/components/Pokedex.tsx: -------------------------------------------------------------------------------- 1 | import pageStyles from '../styles/page.module.css'; 2 | import { ListingCard } from './ListingCard.tsx'; 3 | 4 | const POKEMON_GRAPHQL_SCHEMA_URL = 'https://beta.pokeapi.co/graphql/v1beta/'; 5 | 6 | export default function Pokedex({ pokemonList }: { pokemonList: any }) { 7 | return ( 8 |
9 |

Pokedex

10 | 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/routing/routing-project/routing-app/components/Pokemon.tsx: -------------------------------------------------------------------------------- 1 | import { Link, useParams } from 'react-router-dom'; 2 | import pageStyles from '../styles/page.module.css'; 3 | import ProfileCard from './ProfileCard.tsx'; 4 | 5 | export default function Pokemon({ pokemonList }: { pokemonList: any }) { 6 | const params = useParams(); 7 | const pokemon = pokemonList.find((pokemon) => pokemon.name === params.name); 8 | return ( 9 |
10 |

Profile

11 | 12 |
13 | Back to Pokedex 14 |
15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /examples/routing/routing-project/routing-app/components/ProfileCard.tsx: -------------------------------------------------------------------------------- 1 | import pageStyles from '../styles/page.module.css'; 2 | 3 | export default function ProfileCard({ pokemon }: any) { 4 | const { 5 | name, 6 | height, 7 | weight, 8 | base_experience, 9 | pokemon_v2_pokemonmoves: moves, 10 | pokemon_v2_pokemonsprites: sprites, 11 | pokemon_v2_pokemontypes: types, 12 | } = pokemon; 13 | 14 | const profileImageSrc = sprites[0].sprites; 15 | 16 | const listOfMoves = moves.map((move: any) => ( 17 |
  • {move.pokemon_v2_move.name}
  • 18 | )); 19 | 20 | const listOfTypes = types.map((type: any) => ( 21 |
  • {type.pokemon_v2_type.name}
  • 22 | )); 23 | 24 | return ( 25 |
    26 | {name} 27 |
    28 |
    29 |

    {name}

    30 |
    31 |
      32 |
    • STATS
    • 33 |
    • HP: {base_experience}
    • 34 |
    • HEIGHT: {height}m
    • 35 |
    • WEIGHT: {weight}kg
    • 36 |
    37 |
    38 |
    39 |
      40 |
      41 |
    • TYPES
    • 42 | {listOfTypes} 43 |
      44 |
      45 |
    • MOVES
    • 46 | {listOfMoves} 47 |
      48 |
    49 |
    50 |
    51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /examples/routing/routing-project/routing-app/components/islands/App.tsx: -------------------------------------------------------------------------------- 1 | import { Routes, Route, BrowserRouter, Link } from 'react-router-dom'; 2 | import { StaticRouter } from 'react-router-dom/server'; 3 | import { 4 | useIsServerRender, 5 | usePageUrl, 6 | useBasePath, 7 | } from '@hubspot/cms-components'; 8 | import Header from '../Header.tsx'; 9 | import Pokedex from '../Pokedex.tsx'; 10 | import Pokemon from '../Pokemon.tsx'; 11 | import Home from '../Home.tsx'; 12 | import { pokemonList } from '../../constants.ts'; 13 | 14 | const AppRoutes = () => { 15 | return ( 16 | 17 | } /> 18 | } /> 19 | } 22 | /> 23 | 24 | ); 25 | }; 26 | 27 | const App = () => { 28 | const isServerRender = useIsServerRender(); 29 | const pageUrl = usePageUrl(); 30 | const basePath = useBasePath(); 31 | 32 | let app: JSX.Element; 33 | 34 | if (isServerRender) { 35 | app = ( 36 | 37 |
    38 | 39 | 40 | ); 41 | } else { 42 | app = ( 43 | 44 |
    45 | 46 | 47 | ); 48 | } 49 | 50 | return app; 51 | }; 52 | 53 | export default App; 54 | -------------------------------------------------------------------------------- /examples/routing/routing-project/routing-app/components/modules/Router/index.tsx: -------------------------------------------------------------------------------- 1 | import { Island } from '@hubspot/cms-components'; 2 | import AppIsland from '../../islands/App?island'; 3 | 4 | export const Component = () => { 5 | return ; 6 | }; 7 | 8 | export const fields = []; 9 | 10 | export const meta = { 11 | label: 'Router Module', 12 | }; 13 | -------------------------------------------------------------------------------- /examples/routing/routing-project/routing-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "routing-app", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "dependencies": { 6 | "@hubspot/cms-components": "latest", 7 | "prop-types": "^15.8.1", 8 | "react": "^18.1.0", 9 | "react-router-dom": "^6.18.0", 10 | "graphql": "^16.8.1", 11 | "graphql-request": "^6.1.0" 12 | }, 13 | "devDependencies": { 14 | "@hubspot/cms-dev-server": "latest", 15 | "@vitejs/plugin-react": "^2.1.0", 16 | "jsdom": "^20.0.1", 17 | "vitest": "^0.24.3" 18 | }, 19 | "scripts": { 20 | "start": "hs-cms-dev-server ." 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/routing/routing-project/routing-app/styles/card.module.css: -------------------------------------------------------------------------------- 1 | .cards { 2 | display: flex; 3 | flex-wrap: wrap; 4 | justify-content: center; 5 | align-items: flex-start; 6 | gap: 15px; 7 | } 8 | 9 | .cards a { 10 | text-decoration: none; 11 | width: 30%; 12 | min-width: 300px; 13 | } 14 | 15 | .card { 16 | background-color: black; 17 | color: white; 18 | display: flex; 19 | justify-content: center; 20 | align-items: center; 21 | gap: 25px; 22 | padding: 10px 15px; 23 | border-radius: 5px; 24 | height: 125px; 25 | cursor: pointer; 26 | } 27 | 28 | .card p, 29 | .card h2 { 30 | margin: 0; 31 | } 32 | 33 | .card img { 34 | border-radius: 5px; 35 | } 36 | -------------------------------------------------------------------------------- /examples/routing/routing-project/routing-app/styles/header.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | width: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | justify-content: space-between; 7 | font-family: 'Mono', monospace; 8 | gap: 20px; 9 | padding: 15px 0; 10 | } 11 | 12 | .header nav ul { 13 | list-style: none; 14 | display: flex; 15 | flex-direction: row; 16 | align-items: center; 17 | gap: 30px; 18 | margin: 0; 19 | padding: 0; 20 | } 21 | 22 | .header nav ul li a { 23 | color: #33475b; 24 | text-decoration: none; 25 | padding-bottom: 4px; 26 | transition: all 0.2s ease; 27 | border-bottom: 2px solid transparent; 28 | } 29 | 30 | .header nav ul li a:hover { 31 | color: #ff7a59; 32 | border-bottom: 2px solid #ff7a59; 33 | transition: all 0.3s ease; 34 | } 35 | 36 | .header nav ul li a.active { 37 | color: #ff7a59 !important; 38 | border-bottom: 2px solid #ff7a59; 39 | } 40 | 41 | .header .btn { 42 | list-style: none; 43 | background-color: #33475b; 44 | padding: 10px 20px; 45 | border: none; 46 | border-radius: 4px; 47 | cursor: pointer; 48 | transition: all 0.2s ease; 49 | } 50 | 51 | .header .btn:hover { 52 | background-color: #ff7a59; 53 | transition: all 0.3s ease; 54 | } 55 | 56 | .header .btn a { 57 | text-decoration: none; 58 | color: white; 59 | } 60 | -------------------------------------------------------------------------------- /examples/routing/routing-project/routing-app/styles/page.module.css: -------------------------------------------------------------------------------- 1 | .page { 2 | font-family: 'Mono', monospace; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: center; 6 | align-items: center; 7 | margin-top: 40px; 8 | } 9 | 10 | .page h1 { 11 | margin-top: 0; 12 | } 13 | 14 | .page .back { 15 | text-align: center; 16 | } 17 | 18 | .page ul { 19 | list-style-type: none; 20 | padding: 0; 21 | } 22 | 23 | .page ul li { 24 | margin-bottom: 10px; 25 | } 26 | 27 | .profile { 28 | margin: 50px auto; 29 | display: flex; 30 | justify-content: center; 31 | align-items: center; 32 | gap: 50px; 33 | } 34 | 35 | .home { 36 | width: 100%; 37 | text-align: center; 38 | } 39 | 40 | .home h1 { 41 | margin-bottom: 50px; 42 | } 43 | 44 | .home a { 45 | text-decoration: none; 46 | color: white; 47 | background-color: red; 48 | padding: 12px 15px; 49 | border-radius: 5px; 50 | } 51 | 52 | .attributes { 53 | display: flex; 54 | align-items: center; 55 | justify-content: space-between; 56 | flex-direction: column; 57 | gap: 20px; 58 | } 59 | 60 | .attributes > div { 61 | display: flex; 62 | flex-direction: row; 63 | gap: 25px; 64 | } 65 | 66 | .stacked { 67 | display: flex; 68 | flex-direction: column; 69 | } 70 | 71 | .top { 72 | display: flex; 73 | flex-direction: row; 74 | } 75 | 76 | .card { 77 | background-color: #3e2073; 78 | border-radius: 15px; 79 | padding: 25px 50px 25px 15px; 80 | width: 600px; 81 | color: #f0e68c; 82 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); 83 | display: flex; 84 | justify-content: space-between; 85 | gap: 50px; 86 | margin-bottom: 40px; 87 | } 88 | 89 | .card img { 90 | height: auto; 91 | } 92 | 93 | .card .info { 94 | flex-grow: 1; 95 | } 96 | 97 | .moves { 98 | display: flex; 99 | flex-direction: column; 100 | gap: 20px; 101 | } 102 | 103 | .attributeTitle { 104 | padding: 5px; 105 | background-color: #f0e68c; 106 | color: #3e2073; 107 | } 108 | -------------------------------------------------------------------------------- /examples/routing/routing-project/routing-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "allowImportingTsExtensions": true, 10 | "noEmit": true, 11 | "isolatedModules": true, 12 | "jsx": "react-jsx", 13 | "skipLibCheck": true, 14 | "types": ["vite/client", "vitest/globals"] 15 | }, 16 | "include": ["components/**/*"], 17 | "exclude": ["node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /examples/routing/routing-theme/fields.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /examples/routing/routing-theme/layouts/base.hubl.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | {% if page_meta.html_title or pageTitle %} 9 | {{ page_meta.html_title or pageTitle }} 10 | {% endif %} 11 | {% if brand_settings.primaryFavicon.src %} 12 | 13 | {% endif %} 14 | 15 | {{ standard_header_includes }} 16 | 17 | 18 |
    19 | {% block header %} 20 | {% endblock header %} 21 | 22 | {# The main-content ID is used for the navigation skipper in the header.html file. More information on the navigation skipper can be found here: https://github.com/HubSpot/cms-theme-boilerplate/wiki/Accessibility #} 23 | 24 | {% block body %} 25 | {% endblock body %} 26 | 27 | {% block footer %} 28 | {% endblock footer %} 29 |
    30 | 31 | {# To see a full list of what is included via standard_footer_includes please reference this article: https://developers.hubspot.com/docs/cms/hubl/variables#required-page-template-variables #} 32 | {{ standard_footer_includes }} 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/routing/routing-theme/router-page.hubl.html: -------------------------------------------------------------------------------- 1 | 6 | {% extends "./layouts/base.hubl.html" %} 7 | 8 | {% block body %} 9 | 10 | {% module "router-module" 11 | path="@projects/routing-project/routing-app/components/modules/Router" 12 | %} 13 | 14 | {% endblock body %} 15 | -------------------------------------------------------------------------------- /examples/routing/routing-theme/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "CMS React - Routing" 3 | } 4 | -------------------------------------------------------------------------------- /examples/serverless/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | sourceType: 'module', 4 | ecmaFeatures: { 5 | jsx: true, 6 | }, 7 | }, 8 | env: { 9 | node: true, 10 | es2021: true, 11 | }, 12 | extends: ['eslint:recommended', 'prettier', 'plugin:react/recommended'], 13 | rules: { 14 | 'react/react-in-jsx-scope': 'off', 15 | 'react/prop-types': 'off', 16 | }, 17 | ignorePatterns: ['hello-world-project/hello-world-app/dist/*'], 18 | settings: { 19 | react: { 20 | version: '18.1', 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /examples/serverless/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | hubspot.config.yml 3 | dist -------------------------------------------------------------------------------- /examples/serverless/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "overrides": [ 7 | { 8 | "files": "*.hubl.html", 9 | "options": { 10 | "parser": "hubl" 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /examples/serverless/README.md: -------------------------------------------------------------------------------- 1 | # CMS React Module with Serverless Functions Example 2 | 3 | This directory contains an example of a CMS React Module that makes use of serverless functions on the new HubSpot developer platform. 4 | 5 | ## Overview 6 | 7 | This example demonstrates how to build a CMS React Module that leverages serverless functions to enhance the functionality of your HubSpot CMS. By combining the power of React and serverless functions, you can create dynamic and interactive modules that seamlessly integrate with the HubSpot developer platform. 8 | 9 | ## Resources 10 | 11 | For more information about CMS React Modules and serverless functions on the HubSpot developer platform, refer to the following resources: 12 | 13 | - [HubSpot CMS Developer Documentation](https://developers.hubspot.com/cms/) 14 | - [React Documentation](https://reactjs.org/) 15 | - [HubSpot Developer Platform Serverless Documentation](https://developers.hubspot.com/docs/platform/serverless-functions) 16 | 17 | ## License 18 | 19 | This example is licensed under the [MIT License](LICENSE). 20 | -------------------------------------------------------------------------------- /examples/serverless/cms-with-serverless-project/.hsignore: -------------------------------------------------------------------------------- 1 | README.md 2 | tsconfig.json 3 | yarn.lock 4 | dist 5 | -------------------------------------------------------------------------------- /examples/serverless/cms-with-serverless-project/app/app.functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app.functions", 3 | "version": "1.0.0", 4 | "description": "", 5 | "dependencies": {} 6 | } 7 | -------------------------------------------------------------------------------- /examples/serverless/cms-with-serverless-project/app/app.functions/parrot-function.js: -------------------------------------------------------------------------------- 1 | exports.main = async (context) => { 2 | return { 3 | statusCode: 200, 4 | body: { 5 | message: `SQUAWK: ${context.params.message}`, 6 | }, 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /examples/serverless/cms-with-serverless-project/app/app.functions/serverless.json: -------------------------------------------------------------------------------- 1 | { 2 | "appFunctions": { 3 | "parrotFunction": { 4 | "file": "parrot-function.js", 5 | "endpoint": { 6 | "path": "parrot", 7 | "method": ["GET"] 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/serverless/cms-with-serverless-project/app/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CMS React With Serverless", 3 | "description": "This app contains a serverless function wich can parrot requests", 4 | "scopes": [ 5 | "crm.objects.companies.read", 6 | "crm.objects.companies.write", 7 | "collector.graphql_schema.read", 8 | "collector.graphql_query.execute", 9 | "crm.objects.custom.write", 10 | "crm.objects.custom.read", 11 | "crm.schemas.custom.read", 12 | "crm.schemas.custom.write" 13 | ], 14 | "uid": "cms_react_serverless_app", 15 | "public": false 16 | } 17 | -------------------------------------------------------------------------------- /examples/serverless/cms-with-serverless-project/cms-react/cms-assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "CMS React", 3 | "outputPath": "" 4 | } 5 | -------------------------------------------------------------------------------- /examples/serverless/cms-with-serverless-project/cms-react/components/modules/MakeServerlessRequest/MakeServerlessRequestIsland.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export default function MakeServerlessRequestIsland() { 4 | const [data, setData] = useState([]); 5 | const [message, setMessage] = useState(''); 6 | 7 | function makeRequestToProjectFunction(event) { 8 | event.preventDefault(); 9 | 10 | return fetch(`/hs/serverless/parrot?message=${message}`) 11 | .then((response) => response.json()) 12 | .then((jsonResponse) => { 13 | setData([...data, jsonResponse.message]); 14 | }); 15 | } 16 | 17 | return ( 18 | <> 19 |
    20 |
    21 | 22 | Make a request to the Developer Platform Serverless Function 23 | 24 | 25 | setMessage(e.target.value)} 30 | value={message} 31 | /> 32 |
    33 | 34 |
    35 |
    36 |
    37 |
      44 | {data.map((item) => ( 45 |
    • 46 | {item} 47 |
    • 48 | ))} 49 |
    50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /examples/serverless/cms-with-serverless-project/cms-react/components/modules/MakeServerlessRequest/index.jsx: -------------------------------------------------------------------------------- 1 | import { Island } from '@hubspot/cms-components'; 2 | 3 | import MakeServerlessRequestIsland from './MakeServerlessRequestIsland?island'; 4 | 5 | export function Component() { 6 | return ( 7 |
    8 |

    Parrot Island

    9 | 13 |
    14 | ); 15 | } 16 | 17 | export const fields = []; 18 | 19 | export const meta = { 20 | label: 'Make Serverless Requests', 21 | }; 22 | -------------------------------------------------------------------------------- /examples/serverless/cms-with-serverless-project/cms-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "module-components-app", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "dependencies": { 6 | "@hubspot/cms-components": "latest", 7 | "prop-types": "^15.8.1", 8 | "react": "^18.1.0" 9 | }, 10 | "devDependencies": { 11 | "@hubspot/cms-dev-server": "latest", 12 | "@testing-library/react": "^13.4.0", 13 | "@vitejs/plugin-react": "^2.1.0", 14 | "jsdom": "^20.0.1", 15 | "vitest": "^0.24.3" 16 | }, 17 | "scripts": { 18 | "start": "hs-cms-dev-server .", 19 | "test": "vitest" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/serverless/cms-with-serverless-project/hsproject.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cms-with-serverless-project", 3 | "srcDir": ".", 4 | "platformVersion": "2023.2" 5 | } 6 | -------------------------------------------------------------------------------- /examples/serverless/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "NodeNext" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/serverless/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cms-with-serverless", 3 | "description": "CMS React Module Components Example", 4 | "license": "Apache-2.0", 5 | "devDependencies": { 6 | "@hubspot/cli": "latest", 7 | "@hubspot/prettier-plugin-hubl": "latest", 8 | "eslint": "^8.24.0", 9 | "eslint-config-prettier": "^8.5.0", 10 | "eslint-plugin-react": "^7.31.10", 11 | "prettier": "^2.7.1", 12 | "yarpm": "^1.2.0" 13 | }, 14 | "scripts": { 15 | "start": "cd cms-with-serverless-project/src/cms-react && yarpm start", 16 | "postinstall": "cd cms-with-serverless-project/src/cms-react && yarpm install", 17 | "lint:js": "eslint . --ext .js,.jsx", 18 | "prettier": "prettier . --check", 19 | "deploy": "hs project upload" 20 | }, 21 | "dependencies": {}, 22 | "engines": { 23 | "node": ">=16.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/styling/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | sourceType: 'module', 4 | ecmaFeatures: { 5 | jsx: true, 6 | }, 7 | }, 8 | env: { 9 | node: true, 10 | es2021: true, 11 | }, 12 | extends: ['eslint:recommended', 'prettier', 'plugin:react/recommended'], 13 | rules: { 14 | 'react/react-in-jsx-scope': 'off', 15 | 'react/prop-types': 'off', 16 | }, 17 | ignorePatterns: ['hello-world-project/cms-assets/dist/*'], 18 | settings: { 19 | react: { 20 | version: '18.1', 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /examples/styling/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | hubspot.config.yml 3 | dist -------------------------------------------------------------------------------- /examples/styling/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "overrides": [ 7 | { 8 | "files": "*.hubl.html", 9 | "options": { 10 | "parser": "hubl" 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /examples/styling/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 HubSpot, Inc. 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. -------------------------------------------------------------------------------- /examples/styling/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-building-blocks-styling", 3 | "description": "JS Building Blocks Styling", 4 | "license": "Apache-2.0", 5 | "devDependencies": { 6 | "@hubspot/cli": "latest", 7 | "@hubspot/prettier-plugin-hubl": "latest", 8 | "eslint": "^8.24.0", 9 | "eslint-config-prettier": "^8.5.0", 10 | "eslint-plugin-react": "^7.31.10", 11 | "prettier": "^2.7.1", 12 | "yarpm": "^1.2.0" 13 | }, 14 | "scripts": { 15 | "start": "cd styling-project/styling-app && yarpm start", 16 | "postinstall": "cd styling-project/styling-app && yarpm install", 17 | "lint:js": "eslint . --ext .js,.jsx", 18 | "prettier": "prettier . --check", 19 | "watch:hubl": "hs watch styling-theme styling-theme", 20 | "upload:hubl": "hs upload styling-theme styling-theme", 21 | "deploy": "hs project upload", 22 | "test": "cd styling-project && cd styling-app && yarpm run test" 23 | }, 24 | "engines": { 25 | "node": ">=16.0.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/styling/styling-project/.hsignore: -------------------------------------------------------------------------------- 1 | README.md 2 | tsconfig.json 3 | yarn.lock 4 | dist 5 | -------------------------------------------------------------------------------- /examples/styling/styling-project/hsproject.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "styling-project", 3 | "srcDir": ".", 4 | "platformVersion": "2023.2" 5 | } 6 | -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/.node-version: -------------------------------------------------------------------------------- 1 | 16.13.2 2 | -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/assets/biker-tailwind-ai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/cms-react/6f4a05d12d121080d518363fc5643e90a80c6ab7/examples/styling/styling-project/styling-app/assets/biker-tailwind-ai.png -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/assets/dog-named-tailwind-ai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/cms-react/6f4a05d12d121080d518363fc5643e90a80c6ab7/examples/styling/styling-project/styling-app/assets/dog-named-tailwind-ai.png -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/assets/windy-trail-ai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/cms-react/6f4a05d12d121080d518363fc5643e90a80c6ab7/examples/styling/styling-project/styling-app/assets/windy-trail-ai.png -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/cms-assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "JS Building Blocks - Styling", 3 | "outputPath": "" 4 | } 5 | -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/components/InteractiveStyledComponent.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useId } from 'react'; 2 | import { styled } from 'styled-components'; 3 | 4 | const StyledButton = styled.button` 5 | padding: ${props => 10 + props.$count * 10}px; 6 | `; 7 | 8 | export default function InteractiveStyledComponent() { 9 | const id = useId(); 10 | const [count, setCount] = useState(0); 11 | 12 | return ( 13 | setCount(prevCount => prevCount + 1)}>Current count is: {count} {id} 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/components/InteractiveStyledJSXComponent.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useId } from 'react'; 2 | 3 | export default function InteractiveStyledJSXComponent() { 4 | const id = useId(); 5 | const [count, setCount] = useState(0); 6 | 7 | return ( 8 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/components/StyledComponentsIsland.jsx: -------------------------------------------------------------------------------- 1 | import { Island } from '@hubspot/cms-components'; 2 | import StyledComponentsRegistry from './StyledComponentsRegistry?client'; 3 | // Important! Wrapper expects a lazy component, which you can do easily with `?client` similar to `?island` 4 | 5 | export default function StyledComponentsIsland(props) { 6 | // Using the `Wrapper` prop for `Island` to wrap the contents in a 7 | // `StyledComponentsRegistry` to capture generated styles on the server 8 | return 9 | } 10 | -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/components/StyledComponentsRegistry.jsx: -------------------------------------------------------------------------------- 1 | import { ServerStyleSheet, StyleSheetManager } from 'styled-components'; 2 | import { useInlineHeadAsset } from '@hubspot/cms-components'; 3 | 4 | export default function StyledComponentsRegistry({ children }) { 5 | // On the client, styled-components creates its own stylesheet. We only want 6 | // to create this sheet on the server 7 | const styledComponentsStyleSheet = import.meta.env.SSR 8 | ? new ServerStyleSheet() 9 | : null; 10 | 11 | useInlineHeadAsset(() => { 12 | if (styledComponentsStyleSheet === null) { 13 | return; 14 | } 15 | 16 | // Collect styles generated on the server pass and return them to go in the 17 | // 18 | const styles = styledComponentsStyleSheet.getStyleElement(); 19 | styledComponentsStyleSheet.seal(); 20 | return <>{styles}; 21 | }); 22 | 23 | if (styledComponentsStyleSheet === null) { 24 | return <>{children}; 25 | } 26 | 27 | return ( 28 | 29 | {children} 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/components/StyledJSXIsland.jsx: -------------------------------------------------------------------------------- 1 | import { Island } from '@hubspot/cms-components'; 2 | import StyledJSXRegistry from './StyledJSXRegistry?client'; 3 | // Important! Wrapper expects a lazy component, which you can do easily with `?client` similar to `?island` 4 | 5 | export default function StyledJSXIsland(props) { 6 | // Using the `Wrapper` prop for `Island` to wrap the contents in a 7 | // `StyledJSXRegistry` to capture generated styles on the server 8 | return 9 | } 10 | -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/components/StyledJSXRegistry.jsx: -------------------------------------------------------------------------------- 1 | import { StyleRegistry, useStyleRegistry } from 'styled-jsx'; 2 | import { useInlineHeadAsset } from '@hubspot/cms-components'; 3 | 4 | function InsertHTML() { 5 | const registry = useStyleRegistry(); 6 | useInlineHeadAsset(() => { 7 | // Collect styles generated on the server pass and return them to go in the 8 | // 9 | 10 | const styles = registry.styles(); 11 | return <>{styles}; 12 | }); 13 | } 14 | 15 | export default function StyledJSXRegistry({ children }) { 16 | // for styled-jsx, the registry component needs to be included on the client 17 | // and server to make sure that hook usage is preserved. excluding it on the 18 | // client can result in mismatching results of useId() 19 | 20 | return ( 21 | 22 | 23 | {children} 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/components/islands/ToggleSwitch.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Switch } from '@headlessui/react'; 3 | 4 | export default function SomeToggleSwitch({ id, islandId, defaultChecked }) { 5 | id ??= `${islandId}-switch`; 6 | 7 | const [checked, setChecked] = useState(defaultChecked); 8 | 9 | const onChange = () => { 10 | console.log(`toggle ${id} changed`, !checked); 11 | setChecked(!checked); 12 | }; 13 | return ( 14 | 22 | Enable something 23 | 24 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/components/partials/CSSModulesPartial.jsx: -------------------------------------------------------------------------------- 1 | import classes from '../../styles/CSSModulesPartial.module.css'; 2 | 3 | function CSSModulesPartial({ extraClassNames = '' }) { 4 | return ( 5 |
    6 |

    CSSModulesPartial

    7 |

    8 | Muffin cake candy cookie fruitcake wafer gummies macaroon. sesame 9 | jujubes powder gummies cupcake macaroon brownie apple pie fruitcake. 10 | gingerbread chupa chups pudding apple pie pudding jujubes marshmallow 11 | powder sugar. toffee cake cupcake fruitcake icing topping halvah powder 12 | candy canes chocolate wafer powder danish powder. lollipop tiramisu 13 | brownie marshmallow donut muffin apple pie candy canes apple pie. oat 14 | cake. 15 |

    16 |

    17 | Gummies snaps muffin lemon drops bonbon croissant licorice toffee 18 | lollipop sugar plum. sugar plum oat cake cotton candy croissant candy 19 | jujubes ice cream gingerbread biscuit sugar plum fruitcake. apple pie. 20 |

    21 |
    22 | ); 23 | } 24 | 25 | export default CSSModulesPartial; 26 | -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/components/partials/CSSPropertiesPartial.jsx: -------------------------------------------------------------------------------- 1 | import '../../styles/CSSPropertiesPartial.css'; 2 | 3 | function CSSPropertiesPartial({ 4 | wrapperPadding, 5 | wrapperBG, 6 | wrapperBorder, 7 | wrapperColor, 8 | sharedMargin = '2rem', 9 | }) { 10 | return ( 11 |
    21 |

    CSSPropertiesPartial

    22 |

    23 | Muffin cake candy cookie fruitcake wafer gummies macaroon. sesame 24 | jujubes powder gummies cupcake macaroon brownie apple pie fruitcake. 25 | gingerbread chupa chups pudding apple pie pudding jujubes marshmallow 26 | powder sugar. toffee cake cupcake fruitcake icing topping halvah powder 27 | candy canes chocolate wafer powder danish powder. lollipop tiramisu 28 | brownie marshmallow donut muffin apple pie candy canes apple pie. oat 29 | cake. 30 |

    31 |

    32 | Gummies snaps muffin lemon drops bonbon croissant licorice toffee 33 | lollipop sugar plum. sugar plum oat cake cotton candy croissant candy 34 | jujubes ice cream gingerbread biscuit sugar plum fruitcake. apple pie. 35 |

    36 |
    37 | ); 38 | } 39 | 40 | export default CSSPropertiesPartial; 41 | -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/components/partials/InlineStylesPartial.jsx: -------------------------------------------------------------------------------- 1 | function CSSPropertiesPartial({ 2 | wrapperPadding = '1rem', 3 | wrapperBG = 'lightcoral', 4 | wrapperBorder = '2px dotted crimson', 5 | wrapperColor = 'white', 6 | sharedMargin = '1rem', 7 | }) { 8 | return ( 9 |
    17 |

    18 | InlineStylesPartial 19 |

    20 |

    21 | Muffin cake candy cookie fruitcake wafer gummies macaroon. sesame 22 | jujubes powder gummies cupcake macaroon brownie apple pie fruitcake. 23 | gingerbread chupa chups pudding apple pie pudding jujubes marshmallow 24 | powder sugar. toffee cake cupcake fruitcake icing topping halvah powder 25 | candy canes chocolate wafer powder danish powder. lollipop tiramisu 26 | brownie marshmallow donut muffin apple pie candy canes apple pie. oat 27 | cake. 28 |

    29 |

    30 | Gummies snaps muffin lemon drops bonbon croissant licorice toffee 31 | lollipop sugar plum. sugar plum oat cake cotton candy croissant candy 32 | jujubes ice cream gingerbread biscuit sugar plum fruitcake. apple pie. 33 |

    34 |
    35 | ); 36 | } 37 | 38 | export default CSSPropertiesPartial; 39 | -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/components/partials/StyledComponentsPartial.jsx: -------------------------------------------------------------------------------- 1 | import { styled } from 'styled-components'; 2 | import StyledComponentsRegistry from '../StyledComponentsRegistry' 3 | import StyledComponentsIsland from '../StyledComponentsIsland'; 4 | import InteractiveStyledComponent from '../InteractiveStyledComponent?island'; 5 | 6 | const StyledContainer = styled.div` 7 | padding: 1rem; 8 | background-color: lightcoral; 9 | border: 2px dotted crimson; 10 | color: white; 11 | `; 12 | 13 | const StyledHeader = styled.h2` 14 | margin: 0; 15 | marginBottom: 1rem; 16 | `; 17 | 18 | function StyledComponentsPartial({ 19 | }) { 20 | return ( 21 | 22 | 23 | 24 | StyledComponentsPartial 25 | 26 |

    27 | Muffin cake candy cookie fruitcake wafer gummies macaroon. sesame 28 | jujubes powder gummies cupcake macaroon brownie apple pie fruitcake. 29 | gingerbread chupa chups pudding apple pie pudding jujubes marshmallow 30 | powder sugar. toffee cake cupcake fruitcake icing topping halvah powder 31 | candy canes chocolate wafer powder danish powder. lollipop tiramisu 32 | brownie marshmallow donut muffin apple pie candy canes apple pie. oat 33 | cake. 34 |

    35 |

    36 | Gummies snaps muffin lemon drops bonbon croissant licorice toffee 37 | lollipop sugar plum. sugar plum oat cake cotton candy croissant candy 38 | jujubes ice cream gingerbread biscuit sugar plum fruitcake. apple pie. 39 |

    40 | {/* We need to wrap `Island` in something like `StyledComponentsIsland` 41 | to capture styles from the island subtree */} 42 | 43 |
    44 |
    45 | ); 46 | } 47 | 48 | export default StyledComponentsPartial; 49 | -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/components/partials/StyledJSXPartial.jsx: -------------------------------------------------------------------------------- 1 | import StyledJSXRegistry from '../StyledJSXRegistry'; 2 | import StyledJSXIsland from '../StyledJSXIsland'; 3 | import InteractiveStyledJSXComponent from '../InteractiveStyledJSXComponent?island' 4 | 5 | function StyledJSXPartial() { 6 | return ( 7 | 8 |
    9 |

    10 | StyledJSXPartial 11 |

    12 |

    13 | Muffin cake candy cookie fruitcake wafer gummies macaroon. sesame 14 | jujubes powder gummies cupcake macaroon brownie apple pie fruitcake. 15 | gingerbread chupa chups pudding apple pie pudding jujubes marshmallow 16 | powder sugar. toffee cake cupcake fruitcake icing topping halvah powder 17 | candy canes chocolate wafer powder danish powder. lollipop tiramisu 18 | brownie marshmallow donut muffin apple pie candy canes apple pie. oat 19 | cake. 20 |

    21 |

    22 | Gummies snaps muffin lemon drops bonbon croissant licorice toffee 23 | lollipop sugar plum. sugar plum oat cake cotton candy croissant candy 24 | jujubes ice cream gingerbread biscuit sugar plum fruitcake. apple pie. 25 |

    26 | 45 | {/* We need to wrap `Island` in something like `StyledComponentsIsland` 46 | to capture styles from the island subtree */} 47 | 48 |
    49 |
    50 | ); 51 | } 52 | 53 | export default StyledJSXPartial; 54 | -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/components/partials/TailwindPartial.jsx: -------------------------------------------------------------------------------- 1 | import { Island } from '@hubspot/cms-components'; 2 | import BookOpenIcon from '@heroicons/react/24/solid/esm/BookOpenIcon.js'; 3 | 4 | import ToggleSwitchIsland from '../islands/ToggleSwitch.jsx?island'; 5 | 6 | import biker from '../../assets/biker-tailwind-ai.png'; 7 | import dog from '../../assets/dog-named-tailwind-ai.png'; 8 | import trail from '../../assets/windy-trail-ai.png'; 9 | import '../../styles/tailwind.css'; 10 | 11 | const aiArt = [ 12 | { 13 | name: 'A Biker With a Tailwind', 14 | image: `${biker}?name=calvin&width=150`, 15 | }, 16 | { 17 | name: 'A Dog Named Tailwind', 18 | image: `${dog}?name=kristen&width=150`, 19 | }, 20 | { 21 | name: 'The Windiest Trail', 22 | image: `${trail}?name=ted&width=150`, 23 | }, 24 | ]; 25 | 26 | export default function Example() { 27 | return ( 28 | <> 29 |
      30 | {aiArt.map((art) => ( 31 |
    • 32 | 33 |
      34 |

      {art.name}

      35 |
      36 |
    • 37 | ))} 38 |
    39 | 40 | 45 | 46 |
    47 |
    BookOpen
    48 |
    49 | 50 |
    51 |
    52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "NodeNext" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "styling-app", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "dependencies": { 6 | "@headlessui/react": "^1.7.14", 7 | "@heroicons/react": "^2.0.18", 8 | "@hubspot/cms-components": "latest", 9 | "autoprefixer": "^10.4.14", 10 | "postcss-nested": "^6.0.1", 11 | "prop-types": "^15.8.1", 12 | "react": "^18.1.0", 13 | "styled-jsx": "^5.1.2", 14 | "styled-components": "^6.0.3", 15 | "tailwindcss": "^3.3.2" 16 | }, 17 | "devDependencies": { 18 | "@hubspot/cms-dev-server": "latest", 19 | "@testing-library/react": "^13.4.0", 20 | "@vitejs/plugin-react": "^2.1.0", 21 | "jsdom": "^20.0.1", 22 | "vitest": "^0.24.3" 23 | }, 24 | "scripts": { 25 | "start": "hs-cms-dev-server .", 26 | "test": "vitest" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | import tailwind from 'tailwindcss'; 2 | import autoprefixer from 'autoprefixer'; 3 | import postcssNested from 'postcss-nested'; 4 | import tailwindConfig from './tailwind.config.js'; 5 | 6 | export default { 7 | plugins: [tailwind(tailwindConfig), postcssNested, autoprefixer()], 8 | }; 9 | -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/styles/CSSModulesPartial.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 3 | padding: 1rem; 4 | background-color: lightcoral; 5 | border: 2px dotted crimson; 6 | color: white; 7 | } 8 | 9 | .wrapper h2 { 10 | margin: 0 0 1rem 0; 11 | } 12 | 13 | .wrapper { 14 | p + p { 15 | margin-top: 1rem; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/styles/CSSPropertiesPartial.css: -------------------------------------------------------------------------------- 1 | .css-properties-partial { 2 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 3 | Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 4 | padding: var(--wrapper-padding, 1rem); 5 | background-color: var(--wrapper-bg, lightcoral); 6 | border: var(--wrapper-border, 2px dotted crimson); 7 | color: var(--wrapper-color, white); 8 | 9 | --shared-margin: 1rem; 10 | } 11 | 12 | .css-properties-partial h2 { 13 | margin: 0 0 var(--shared-margin) 0; 14 | } 15 | 16 | .css-properties-partial { 17 | p + p { 18 | margin-top: var(--shared-margin); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/styles/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/tailwind.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'url'; 2 | const componentsDir = fileURLToPath(new URL('./components', import.meta.url)); 3 | 4 | export default { 5 | content: [`${componentsDir}/**/*.{js,ts,jsx,tsx}`], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | }; 11 | -------------------------------------------------------------------------------- /examples/styling/styling-project/styling-app/vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import { default as react } from '@vitejs/plugin-react'; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | test: { 7 | environment: 'jsdom', 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /examples/styling/styling-theme/fields.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "Color", 4 | "name": "brand_color", 5 | "label": "Brand Color", 6 | "default": { 7 | "color": "#FF7A59", 8 | "opacity": 100 9 | } 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /examples/styling/styling-theme/layouts/base.hubl.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | {% if page_meta.html_title or pageTitle %} 9 | {{ page_meta.html_title or pageTitle }} 10 | {% endif %} 11 | {% if brand_settings.primaryFavicon.src %} 12 | 13 | {% endif %} 14 | 15 | {{ standard_header_includes }} 16 | 17 | 18 |
    19 | {% block header %} 20 | {% endblock header %} 21 | 22 | {% block body %} 23 | {% endblock body %} 24 | 25 | {% block footer %} 26 | {% endblock footer %} 27 |
    28 | 29 | {# To see a full list of what is included via standard_footer_includes please reference this article: https://developers.hubspot.com/docs/cms/hubl/variables#required-page-template-variables #} 30 | {{ standard_footer_includes }} 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/styling/styling-theme/styling.hubl.html: -------------------------------------------------------------------------------- 1 | 6 | {% extends "./layouts/base.hubl.html" %} 7 | 8 | {% block body %} 9 | 10 | {% js_partial 11 | path="@projects/styling-project/styling-app/components/partials/CSSModulesPartial.jsx" %} 12 |
    13 | {% js_partial 14 | path="@projects/styling-project/styling-app/components/partials/CSSPropertiesPartial.jsx" %} 15 |
    16 | {% js_partial 17 | path="@projects/styling-project/styling-app/components/partials/InlineStylesPartial.jsx" %} 18 | 19 | {% endblock body %} 20 | -------------------------------------------------------------------------------- /examples/styling/styling-theme/tailwind.hubl.html: -------------------------------------------------------------------------------- 1 | 6 | {% extends "./layouts/base.hubl.html" %} 7 | 8 | {% block body %} 9 | 10 | {% js_partial 11 | path="@projects/styling-project/styling-app/components/partials/TailwindPartial.jsx" %} 12 | 13 | {% endblock body %} 14 | -------------------------------------------------------------------------------- /examples/styling/styling-theme/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "JS Building Blocks - Syling", 3 | "preview_path": "./cmss-modules.hubl.html" 4 | } 5 | -------------------------------------------------------------------------------- /examples/todo-mvc/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | sourceType: 'module', 4 | ecmaFeatures: { 5 | jsx: true, 6 | }, 7 | }, 8 | env: { 9 | node: true, 10 | es2021: true, 11 | }, 12 | extends: ['eslint:recommended', 'prettier', 'plugin:react/recommended'], 13 | rules: { 14 | 'react/react-in-jsx-scope': 'off', 15 | 'react/prop-types': 'off', 16 | }, 17 | ignorePatterns: ['hello-world-project/cms-assets/dist/*'], 18 | settings: { 19 | react: { 20 | version: '18.1', 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /examples/todo-mvc/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | hubspot.config.yml 3 | dist -------------------------------------------------------------------------------- /examples/todo-mvc/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "overrides": [ 7 | { 8 | "files": "*.hubl.html", 9 | "options": { 10 | "parser": "hubl" 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /examples/todo-mvc/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 HubSpot, Inc. 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. -------------------------------------------------------------------------------- /examples/todo-mvc/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "NodeNext" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/todo-mvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-building-blocks-todo-mvc", 3 | "description": "JS Building Blocks Todo MVC", 4 | "license": "Apache-2.0", 5 | "devDependencies": { 6 | "@hubspot/cli": "latest", 7 | "@hubspot/prettier-plugin-hubl": "latest", 8 | "eslint": "^8.24.0", 9 | "eslint-config-prettier": "^8.5.0", 10 | "eslint-plugin-react": "^7.31.10", 11 | "prettier": "^2.7.1", 12 | "yarpm": "^1.2.0" 13 | }, 14 | "scripts": { 15 | "start": "cd todo-mvc-project/todo-mvc-app && yarpm start", 16 | "postinstall": "cd todo-mvc-project/todo-mvc-app && yarpm install", 17 | "lint:js": "eslint . --ext .js,.jsx", 18 | "prettier": "prettier . --check", 19 | "watch:hubl": "hs watch todo-mvc-theme todo-mvc-theme", 20 | "upload:hubl": "hs upload todo-mvc-theme todo-mvc-theme", 21 | "deploy": "hs project upload" 22 | }, 23 | "dependencies": {}, 24 | "engines": { 25 | "node": ">=16.0.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-project/.hsignore: -------------------------------------------------------------------------------- 1 | README.md 2 | tsconfig.json 3 | yarn.lock 4 | dist 5 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-project/hsproject.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-mvc-project", 3 | "srcDir": ".", 4 | "platformVersion": "2023.2" 5 | } 6 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-project/todo-mvc-app/.node-version: -------------------------------------------------------------------------------- 1 | 16.13.2 2 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-project/todo-mvc-app/cms-assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Todo MVC", 3 | "outputPath": "" 4 | } 5 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-project/todo-mvc-app/components/modules/TodoMVCModule/fields.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ModuleFields, TextField } from '@hubspot/cms-components/fields'; 3 | 4 | export const fields = ( 5 | 6 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-project/todo-mvc-app/components/modules/TodoMVCModule/index.jsx: -------------------------------------------------------------------------------- 1 | import TodoMVC from '../../todomvc-separate-islands/index.jsx'; 2 | 3 | const DEFAULT_TODOS_IF_NO_FETCH = [ 4 | { 5 | id: 'default-todo-1', 6 | name: 'Default todo 1', 7 | completed: false, 8 | }, 9 | { 10 | id: 'default-todo-2', 11 | name: 'Default todo 2', 12 | completed: true, 13 | }, 14 | { 15 | id: 'tf-esoteric-nerd-reference', // https://twitter.com/thedungeonrun/status/1508823885948628992 16 | name: 'Human kind, be both 🧡🙏', 17 | completed: false, 18 | } 19 | ]; 20 | 21 | export function Component({ todoPlaceholder, dataQueryResult }) { 22 | const todosFromHubDB = 23 | dataQueryResult?.data?.HUBDB?.todo_js_example_collection?.items; 24 | 25 | const initialTodos = todosFromHubDB ?? DEFAULT_TODOS_IF_NO_FETCH; 26 | 27 | return ( 28 | 29 | ); 30 | } 31 | 32 | export { fields } from './fields.jsx'; 33 | 34 | export const meta = { 35 | label: `TodoMVC module`, 36 | }; 37 | 38 | export const query = `query AllTodos { 39 | HUBDB { 40 | todo_js_example_collection { 41 | items { 42 | id: hs_id 43 | name 44 | completed 45 | } 46 | } 47 | } 48 | }`; 49 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-project/todo-mvc-app/components/todomvc-separate-islands/components/app/app.jsx: -------------------------------------------------------------------------------- 1 | import { Header } from '../header/header.jsx'; 2 | import List from '../list/list.jsx?island'; 3 | import Footer from '../footer/footer.jsx?island'; 4 | import { CopyRight } from '../copy-right/copy-right.jsx'; 5 | import { Island } from '@hubspot/cms-components'; 6 | 7 | export function App({ todoPlaceholder }) { 8 | return ( 9 |
    10 |
    11 |
    12 | 13 | 14 |
    15 | 16 |
    17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-project/todo-mvc-app/components/todomvc-separate-islands/components/copy-right/copy-right.jsx: -------------------------------------------------------------------------------- 1 | export function CopyRight() { 2 | return ( 3 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-project/todo-mvc-app/components/todomvc-separate-islands/components/footer/footer.jsx: -------------------------------------------------------------------------------- 1 | import classNames from 'clsx'; 2 | import { FILTERS } from '../../constants/filter.js'; 3 | import { 4 | selectCompleted, 5 | selectNotCompleted, 6 | } from '../../store/selectors/todo.js'; 7 | import { onClearCompleted } from '../../store/actions/todo.js'; 8 | import { 9 | pushHistoryState, 10 | useSharedIslandReducer, 11 | usePageUrl, 12 | } from '@hubspot/cms-components'; 13 | import useFilterFromURL from '../hooks/useFilterFromURL.js'; 14 | 15 | export default function Footer() { 16 | const pageURL = usePageUrl(); 17 | const [todos, dispatch] = useSharedIslandReducer(); 18 | const filter = useFilterFromURL(); 19 | 20 | const filterTitles = [ 21 | { key: FILTERS.all, value: 'All' }, 22 | { key: FILTERS.active, value: 'Active' }, 23 | { key: FILTERS.completed, value: 'Completed' }, 24 | ]; 25 | const completedCount = selectCompleted(todos).length; 26 | const itemsLeft = selectNotCompleted(todos).length; 27 | const clearCompleted = () => dispatch(onClearCompleted()); 28 | 29 | const filterSelect = (selectedFilter) => { 30 | const newURL = new URL(pageURL); 31 | newURL.searchParams.set('filter', selectedFilter); 32 | pushHistoryState({}, newURL); 33 | }; 34 | 35 | const itemText = itemsLeft === 1 ? 'item' : 'items'; 36 | 37 | if (todos.length === 0) { 38 | return null; 39 | } 40 | 41 | return ( 42 | 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-project/todo-mvc-app/components/todomvc-separate-islands/components/header/TodoInput.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { onCreate } from '../../store/actions/todo.js'; 3 | import { useSharedIslandReducer } from '@hubspot/cms-components'; 4 | 5 | const ENTER_KEY = 'Enter'; 6 | 7 | export default function TodoInput({ className, placeholder }) { 8 | const [name, setName] = useState(''); 9 | const [, dispatch] = useSharedIslandReducer(); 10 | 11 | const handleChange = (event) => setName(event.target.value); 12 | 13 | const handleSubmit = (event) => { 14 | if (event.key !== ENTER_KEY) { 15 | return; 16 | } 17 | 18 | dispatch(onCreate(name)); 19 | onCreate(name); 20 | setName(''); 21 | }; 22 | 23 | return ( 24 | {}} 31 | /> 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-project/todo-mvc-app/components/todomvc-separate-islands/components/header/header.jsx: -------------------------------------------------------------------------------- 1 | import TodoInputIsland from './TodoInput.jsx?island'; 2 | import { Island } from '@hubspot/cms-components'; 3 | 4 | export function Header({ todoPlaceholder }) { 5 | return ( 6 |
    7 |

    todos

    8 | 9 | 14 |
    15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-project/todo-mvc-app/components/todomvc-separate-islands/components/hooks/useFilterFromURL.js: -------------------------------------------------------------------------------- 1 | import { usePageUrl } from '@hubspot/cms-components'; 2 | import { FILTERS } from '../../constants/filter'; 3 | 4 | export default function useFilterFromURL() { 5 | const pageURL = usePageUrl(); 6 | return pageURL.searchParams.get('filter') || FILTERS.all; 7 | } 8 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-project/todo-mvc-app/components/todomvc-separate-islands/components/item/item.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classNames from 'clsx'; 4 | 5 | export function Item({ todo, onUpdate, onRemove }) { 6 | const [editing, setEditing] = useState(false); 7 | const [name, setName] = useState(todo.name); 8 | 9 | const handleEdit = () => setEditing(true); 10 | const handleCompleted = () => 11 | onUpdate({ id: todo.id, completed: !todo.completed }); 12 | const handleRemove = () => onRemove(todo.id); 13 | const handleChange = (event) => setName(event.target.value); 14 | const handleBlur = () => { 15 | onUpdate({ id: todo.id, name }); 16 | setEditing(false); 17 | }; 18 | 19 | const { completed } = todo; 20 | 21 | return ( 22 |
  • 23 |
    24 | 30 | 33 |
    39 | {editing && ( 40 | {}} 46 | /> 47 | )} 48 |
  • 49 | ); 50 | } 51 | 52 | Item.propTypes = { 53 | todo: PropTypes.object.isRequired, 54 | onUpdate: PropTypes.func.isRequired, 55 | onRemove: PropTypes.func.isRequired, 56 | }; 57 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-project/todo-mvc-app/components/todomvc-separate-islands/components/list/list.jsx: -------------------------------------------------------------------------------- 1 | import { Item } from '../item/item.jsx'; 2 | import { selectVisible } from '../../store/selectors/todo.js'; 3 | import { onUpdate, onRemove, onCompleteAll } from '../../store/actions/todo.js'; 4 | import useFilterFromURL from '../hooks/useFilterFromURL.js'; 5 | import { useSharedIslandReducer } from '@hubspot/cms-components'; 6 | 7 | export default function List() { 8 | const [todos, dispatch] = useSharedIslandReducer(); 9 | const filter = useFilterFromURL(); 10 | 11 | const visibleTodos = selectVisible(todos, filter); 12 | const areAllCompleted = todos.length && todos.every((todo) => todo.completed); 13 | const completeAll = () => dispatch(onCompleteAll()); 14 | const update = (values) => dispatch(onUpdate(values)); 15 | const remove = (id) => dispatch(onRemove(id)); 16 | 17 | if (todos.length === 0) { 18 | return null; 19 | } 20 | 21 | return ( 22 |
    23 | 30 |
    38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-project/todo-mvc-app/components/todomvc-separate-islands/constants/action-type.js: -------------------------------------------------------------------------------- 1 | export const ACTION_TYPES = { 2 | load: 'LOAD', 3 | create: 'CREATE', 4 | remove: 'REMOVE', 5 | update: 'UPDATE_TODO', 6 | completeAll: 'COMPLETE_ALL', 7 | clearCompleted: 'CLEAR_COMPLETED', 8 | selectFilter: 'SELECT_FILTER' 9 | }; 10 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-project/todo-mvc-app/components/todomvc-separate-islands/constants/filter.js: -------------------------------------------------------------------------------- 1 | export const FILTERS = { 2 | all: 'all', 3 | active: 'active', 4 | completed: 'completed' 5 | }; 6 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-project/todo-mvc-app/components/todomvc-separate-islands/index.jsx: -------------------------------------------------------------------------------- 1 | import { SharedIslandReducer } from '@hubspot/cms-components'; 2 | import 'todomvc-app-css/index.css'; 3 | import { App } from './components/app/app.jsx'; 4 | import todoReducer from './store/reducers/todo.js?client'; 5 | 6 | export default function TodoMVCReactHooks({ 7 | initialTodos = [], 8 | todoPlaceholder, 9 | }) { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-project/todo-mvc-app/components/todomvc-separate-islands/store/actions/todo.js: -------------------------------------------------------------------------------- 1 | import { ACTION_TYPES } from '../../constants/action-type.js'; 2 | 3 | export const onLoad = (todos) => ({ type: ACTION_TYPES.load, todos }); 4 | export const onCreate = (name) => ({ type: ACTION_TYPES.create, name }); 5 | export const onRemove = (id) => ({ type: ACTION_TYPES.remove, id }); 6 | export const onUpdate = (values) => ({ type: ACTION_TYPES.update, values }); 7 | export const onCompleteAll = () => ({ type: ACTION_TYPES.completeAll }); 8 | export const onClearCompleted = () => ({ type: ACTION_TYPES.clearCompleted }); 9 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-project/todo-mvc-app/components/todomvc-separate-islands/store/reducers/todo.js: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | import { ACTION_TYPES } from '../../constants/action-type.js'; 3 | import { selectCompleted, selectNotCompleted } from '../selectors/todo.js'; 4 | 5 | const areAllCompleted = (state) => 6 | state.length && selectCompleted(state).length === state.length; 7 | 8 | export default function todosReducer(state = [], action) { 9 | switch (action.type) { 10 | case ACTION_TYPES.load: 11 | return [...action.todos]; 12 | case ACTION_TYPES.create: 13 | return [...state, { id: uuidv4(), name: action.name, completed: false }]; 14 | case ACTION_TYPES.update: 15 | return state.map((todo) => 16 | todo.id === action.values.id ? { ...todo, ...action.values } : todo, 17 | ); 18 | case ACTION_TYPES.remove: 19 | return state.filter((todo) => todo.id !== action.id); 20 | case ACTION_TYPES.completeAll: 21 | return state.map((todo) => ({ 22 | ...todo, 23 | ...{ completed: !areAllCompleted(state) }, 24 | })); 25 | case ACTION_TYPES.clearCompleted: 26 | return selectNotCompleted(state); 27 | default: 28 | return state; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-project/todo-mvc-app/components/todomvc-separate-islands/store/selectors/todo.js: -------------------------------------------------------------------------------- 1 | import { FILTERS } from '../../constants/filter.js'; 2 | 3 | export function selectCompleted(todos) { 4 | return todos.filter((todo) => todo.completed); 5 | } 6 | 7 | export function selectNotCompleted(todos) { 8 | return todos.filter((todo) => !todo.completed); 9 | } 10 | 11 | export function selectVisible(todos, filter) { 12 | switch (filter) { 13 | case FILTERS.all: 14 | return [...todos]; 15 | case FILTERS.completed: 16 | return selectCompleted(todos); 17 | case FILTERS.active: 18 | return selectNotCompleted(todos); 19 | default: 20 | return [...todos]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-project/todo-mvc-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-mvc-app", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "dependencies": { 6 | "@hubspot/cms-components": "latest", 7 | "clsx": "^1.2.1", 8 | "prop-types": "^15.8.1", 9 | "react": "^18.1.0", 10 | "todomvc-app-css": "^2.4.2", 11 | "uuid": "^9.0.0" 12 | }, 13 | "devDependencies": { 14 | "@hubspot/cms-dev-server": "latest", 15 | "@testing-library/react": "^13.4.0", 16 | "@vitejs/plugin-react": "^2.1.0", 17 | "jsdom": "^20.0.1", 18 | "vitest": "^0.24.3" 19 | }, 20 | "scripts": { 21 | "start": "hs-cms-dev-server .", 22 | "test": "vitest" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-project/todo-mvc-app/vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import { default as react } from '@vitejs/plugin-react'; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | test: { 7 | environment: 'jsdom', 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-theme/fields.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "Color", 4 | "name": "brand_color", 5 | "label": "Brand Color", 6 | "default": { 7 | "color": "#FF7A59", 8 | "opacity": 100 9 | } 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-theme/layouts/base.hubl.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | {% if page_meta.html_title or pageTitle %} 9 | {{ page_meta.html_title or pageTitle }} 10 | {% endif %} 11 | {% if brand_settings.primaryFavicon.src %} 12 | 13 | {% endif %} 14 | 15 | {{ standard_header_includes }} 16 | 17 | 18 |
    19 | {% block header %} 20 | {% endblock header %} 21 | 22 | {% block body %} 23 | {% endblock body %} 24 | 25 | {% block footer %} 26 | {% endblock footer %} 27 |
    28 | 29 | {# To see a full list of what is included via standard_footer_includes please reference this article: https://developers.hubspot.com/docs/cms/hubl/variables#required-page-template-variables #} 30 | {{ standard_footer_includes }} 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-theme/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "JS Building Blocks - Todo MVC", 3 | "preview_path": "./todo-mvc.hubl.html" 4 | } 5 | -------------------------------------------------------------------------------- /examples/todo-mvc/todo-mvc-theme/todo-mvc.hubl.html: -------------------------------------------------------------------------------- 1 | 6 | {% extends "./layouts/base.hubl.html" %} 7 | 8 | {% block body %} 9 | 10 | {% module "TodoMVC" 11 | path="@projects/todo-mvc-project/todo-mvc-app/components/modules/TodoMVCModule" %} 12 | 13 | {% endblock body %} 14 | --------------------------------------------------------------------------------