Skip to content

Commit

Permalink
Merge pull request #63 from HubSpot/jmiller/routing-example
Browse files Browse the repository at this point in the history
Add Routing example
  • Loading branch information
jontallboy authored Jun 18, 2024
2 parents e9f1153 + 299d776 commit 8e8b006
Show file tree
Hide file tree
Showing 29 changed files with 1,661 additions and 0 deletions.
23 changes: 23 additions & 0 deletions examples/routing/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module.exports = {
parserOptions: {
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
env: {
node: true,
es2021: true,
},
extends: ['eslint:recommended', 'prettier', 'plugin:react/recommended'],
rules: {
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
},
ignorePatterns: ['hello-world-project/cms-assets/dist/*'],
settings: {
react: {
version: '18.1',
},
},
};
3 changes: 3 additions & 0 deletions examples/routing/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
hubspot.config.yml
dist
14 changes: 14 additions & 0 deletions examples/routing/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"overrides": [
{
"files": "*.hubl.html",
"options": {
"parser": "hubl"
}
}
]
}
12 changes: 12 additions & 0 deletions examples/routing/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Copyright 2022 HubSpot, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
68 changes: 68 additions & 0 deletions examples/routing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Routing
With CMS React, you can use popular routing libraries like React-Router to enable SPA-style routing within your CMS website. The Pokedex example in this directory showcases how to set up SPA routing in your own project using React-Router. This documentation will walk through key aspects of our example, but for more information on React-Router, please visit their [docs](https://reactrouter.com/en/main).

## Reference Files
### `App.tsx`
This component is where we set up our routes using `react-router-dom` components. The idea is to define which components should render for given paths, resulting in individual pages for each route. The `AppRoutes` component within this file sets up our paths using both static and dynamic routes.

#### Static Routes
```js
const AppRoutes = () => {
return (
<Routes>
{/* Static Route for Home */}
<Route path="/" element={<Home />} />
{/* Static Route for Pokedex */}
<Route path="/pokemon" element={<Pokedex pokemonList={pokemonList} />} />
{/* Dynamic Route for individual Pokemon */}
<Route
path="/pokemon/:name"
element={<Pokemon pokemonList={pokemonList} />}
/>
</Routes>
);
};
```

In the first route `<Route path="/" element={<Home />}` />, we explicitly tell React-Router to render the `<Home />` component at the `/` path. This means that when a user visits `your-website.com/`, they will see a home page rendered by the `<Home />` component. The final `<Route />` component leverages [React-Router's dynamic segments](https://reactrouter.com/en/main/route/route#dynamic-segments). As you can see in the example above, the path `/pokedex/:name` includes `:name` which is our dynamic segment. This allows us to render different content based on the value of `:name` at render time.

Once our routes are setup, we need to update our `<Pokemon />` component to know what pokemon page to render. As you can see in the example below `useParams` extracts the dynamic segment (:name) from the URL, and we use it to find the corresponding Pokemon in our pokemonList.

```js
import { Link, useParams } from 'react-router-dom';

export default function Pokemon({ pokemonList }: { pokemonList: any }) {
const params = useParams();
const pokemon = pokemonList.find((pokemon) => pokemon.name === params.name);

return (
<main className={pageStyles.page}>
<h1>Profile</h1>
<ProfileCard pokemon={pokemon} />
<div className={pageStyles.back}>
<Link to="/pokemon">Back to Pokedex</Link>
</div>
</main>
);
}
```

### `Router/index.tsx`

To integrate the `App.tsx` router component into your website, import it with the `?island` suffix and pass it to the module prop of the `<Island />` component. This setup ensures that the necessary JavaScript is sent down to enable routing via React-Router. See example below:

```js
import AppIsland from './App?island';

export const Component = () => {
return <Island module={AppIsland} />;
};
```

## Create a website page
The final step is to create a CMS website or landing page using your Router module and add `[:dynamic-slug]` to the `Page URL` which will look something like this:

![dynamic slug example](./routing-project/src/assets/dynamic-slug-screenshot.png "Dynamic slug example")

`[:dynamic-slug]` is a special keyword that lets the CMS know that it should expect to receive arbitrary paths and it should render the page contents when a match is found. Once your slug is updated to include `[:dynamic-slug]` and the CMS page is published, you can view the live page, swap out `[:dynamic-slug]` with a valid dynamic slug (e.g. `/pokedex/[:dynamic-slug] --> /pokedex/pokemon/ivysaur`) and that is all! You now have SPA routing within your CMS website page 🚀

26 changes: 26 additions & 0 deletions examples/routing/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "cms-react-routing",
"description": "CMS React - Routing",
"license": "Apache-2.0",
"devDependencies": {
"@hubspot/cli": "latest",
"@hubspot/prettier-plugin-hubl": "latest",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-react": "^7.31.10",
"prettier": "^2.7.1",
"yarpm": "^1.2.0"
},
"scripts": {
"start": "cd routing-project/src && yarpm start --",
"postinstall": "cd routing-project/src && yarpm install",
"lint:js": "eslint . --ext .js,.jsx",
"prettier": "prettier . --check",
"watch:hubl": "hs watch routing-theme routing-theme",
"upload:hubl": "hs upload routing-theme routing-theme",
"deploy": "hs project upload routing-project"
},
"engines": {
"node": ">=16.0.0"
}
}
5 changes: 5 additions & 0 deletions examples/routing/routing-project/hsproject.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "routing-project",
"srcDir": ".",
"platformVersion": "2023.2"
}
1 change: 1 addition & 0 deletions examples/routing/routing-project/src/Global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module '*.module.css';
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions examples/routing/routing-project/src/assets/pokeball.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions examples/routing/routing-project/src/cms-assets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"label": "CMS React - Routing",
"outputPath": ""
}
37 changes: 37 additions & 0 deletions examples/routing/routing-project/src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { NavLink } from 'react-router-dom';
import navigationStyles from '../styles/header.module.css';
import pokeball from '../assets/pokeball.svg';

export default function Header() {
return (
<header className={navigationStyles.header}>
<NavLink to="/">
<img src={pokeball} height="75" width="auto" alt="Pokeball" />
</NavLink>
<nav className={navigationStyles.nav}>
<ul>
<li>
<NavLink
to="/"
className={({ isActive }) =>
isActive ? navigationStyles.active : undefined
}
>
Home
</NavLink>
</li>
<li>
<NavLink
to="/pokemon"
className={({ isActive }) =>
isActive ? navigationStyles.active : undefined
}
>
Pokedex
</NavLink>
</li>
</ul>
</nav>
</header>
);
}
11 changes: 11 additions & 0 deletions examples/routing/routing-project/src/components/Home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Link } from 'react-router-dom';
import pageStyles from '../styles/page.module.css';

export default function Home() {
return (
<div className={pageStyles.home}>
<h1>CMS React Routing</h1>
<Link to="/pokemon">See Pokedex</Link>
</div>
);
}
24 changes: 24 additions & 0 deletions examples/routing/routing-project/src/components/ListingCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Link } from 'react-router-dom';
import cardStyles from '../styles/card.module.css';

export function ListingCard({ pokemonList }: { pokemonList: any }) {
return (
<div className={cardStyles.cards}>
{pokemonList.map((pokemon) => (
<Link key={pokemon.name} to={`${pokemon.name}`}>
<div className={cardStyles.card}>
<img
src={pokemon.pokemon_v2_pokemonsprites[0].sprites}
height={100}
alt={pokemon.name}
/>
<div>
<p>{pokemon.pokemon_v2_pokemontypes[0].pokemon_v2_type.name}</p>
<h2>{pokemon.name}</h2>
</div>
</div>
</Link>
))}
</div>
);
}
13 changes: 13 additions & 0 deletions examples/routing/routing-project/src/components/Pokedex.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import pageStyles from '../styles/page.module.css';
import { ListingCard } from './ListingCard.tsx';

const POKEMON_GRAPHQL_SCHEMA_URL = 'https://beta.pokeapi.co/graphql/v1beta/';

export default function Pokedex({ pokemonList }: { pokemonList: any }) {
return (
<main className={pageStyles.page}>
<h1>Pokedex</h1>
<ListingCard pokemonList={pokemonList} />
</main>
);
}
17 changes: 17 additions & 0 deletions examples/routing/routing-project/src/components/Pokemon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Link, useParams } from 'react-router-dom';
import pageStyles from '../styles/page.module.css';
import ProfileCard from './ProfileCard.tsx';

export default function Pokemon({ pokemonList }: { pokemonList: any }) {
const params = useParams();
const pokemon = pokemonList.find((pokemon) => pokemon.name === params.name);
return (
<main className={pageStyles.page}>
<h1>Profile</h1>
<ProfileCard pokemon={pokemon} />
<div className={pageStyles.back}>
<Link to="/pokemon">Back to Pokedex</Link>
</div>
</main>
);
}
52 changes: 52 additions & 0 deletions examples/routing/routing-project/src/components/ProfileCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import pageStyles from '../styles/page.module.css';

export default function ProfileCard({ pokemon }: any) {
const {
name,
height,
weight,
base_experience,
pokemon_v2_pokemonmoves: moves,
pokemon_v2_pokemonsprites: sprites,
pokemon_v2_pokemontypes: types,
} = pokemon;

const profileImageSrc = sprites[0].sprites;

const listOfMoves = moves.map((move: any) => (
<li key={move.pokemon_v2_move.name}>{move.pokemon_v2_move.name}</li>
));

const listOfTypes = types.map((type: any) => (
<li key={type.pokemon_v2_type.name}>{type.pokemon_v2_type.name}</li>
));

return (
<div className={pageStyles.card}>
<img src={profileImageSrc} alt={name} width={300} />
<div className={pageStyles.info}>
<div>
<h1>{name}</h1>
<div>
<ul>
<li className={pageStyles.attributeTitle}>STATS</li>
<li>HP: {base_experience}</li>
<li>HEIGHT: {height}m</li>
<li>WEIGHT: {weight}kg</li>
</ul>
</div>
</div>
<ul className={pageStyles.moves}>
<div>
<li className={pageStyles.attributeTitle}>TYPES</li>
{listOfTypes}
</div>
<div>
<li className={pageStyles.attributeTitle}>MOVES</li>
{listOfMoves}
</div>
</ul>
</div>
</div>
);
}
53 changes: 53 additions & 0 deletions examples/routing/routing-project/src/components/islands/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Routes, Route, BrowserRouter, Link } from 'react-router-dom';
import { StaticRouter } from 'react-router-dom/server';
import {
useIsServerRender,
usePageUrl,
useBasePath,
} from '@hubspot/cms-components';
import Header from '../Header.tsx';
import Pokedex from '../Pokedex.tsx';
import Pokemon from '../Pokemon.tsx';
import Home from '../Home.tsx';
import { pokemonList } from '../../constants.ts';

const AppRoutes = () => {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/pokemon" element={<Pokedex pokemonList={pokemonList} />} />
<Route
path="/pokemon/:name"
element={<Pokemon pokemonList={pokemonList} />}
/>
</Routes>
);
};

const App = () => {
const isServerRender = useIsServerRender();
const pageUrl = usePageUrl();
const basePath = useBasePath();

let app: JSX.Element;

if (isServerRender) {
app = (
<StaticRouter basename={basePath} location={pageUrl.pathname}>
<Header />
<AppRoutes />
</StaticRouter>
);
} else {
app = (
<BrowserRouter basename={basePath}>
<Header />
<AppRoutes />
</BrowserRouter>
);
}

return app;
};

export default App;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Island } from '@hubspot/cms-components';
import AppIsland from '../../islands/App?island';

export const Component = () => {
return <Island module={AppIsland} />;
};

export const fields = [];

export const meta = {
label: 'Router Module',
};
Loading

0 comments on commit 8e8b006

Please sign in to comment.