Incorporate images into the project.
Scaffold a NextJS project:
npx create-next-app@latest next-project
Note: running start
in a NextJS app runs from the production build folder. Use npm run dev
instead.
Things to note:
- CSS modules
- Pages folder
_app
file
https://pokemondb.net/pokedex/national
Add images, pokemon.json
and logo.svg
to the public folder.
There is no HTML document in our NextJS project. We cannot add a favicon.
Add _document.js
:
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html>
<Head>
<link rel="icon" href="logo.svg" type="image/svg+xml" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
In a new pages/pokemon.js
file:
import React from "react";
const PokemonRow = ({ pokemon }) => (
<tr key={pokemon.id}>
<td>{pokemon.name.english}</td>
<td>{pokemon.type.join(", ")}</td>
</tr>
);
export default function Pokemon() {
const [pokemon, pokemonSet] = React.useState(null);
React.useEffect(() => {
fetch("/pokemon.json")
.then((resp) => resp.json())
.then((data) => pokemonSet(data));
}, []);
if (!pokemon) {
return <div>Loading data...</div>;
}
return (
<div>
<h1>Pokemon Search</h1>
<div>
<table width="100%">
<tbody>
{pokemon.map((pokemon) => (
<PokemonRow key={pokemon.id} pokemon={pokemon} />
))}
</tbody>
</table>
</div>
</div>
);
}
Still working in pages/pokemon.js
, add a new PokemonInfo component and a button to the Rows.
const PokemonRow = ({ pokemon, onClick }) => (
<tr key={pokemon.id}>
<td>{pokemon.name.english}</td>
<td>{pokemon.type.join(", ")}</td>
{/* NEW table cell */}
<td>
<button onClick={() => onClick(pokemon)}>More Information</button>
</td>
</tr>
);
// NEW component
const PokemonInfo = ({ name: { english }, base }) => (
<div>
<h2>{english}</h2>
<table>
<tbody>
{Object.keys(base).map((key) => (
<tr key={key}>
<td>{key}</td>
<td>{base[key]}</td>
</tr>
))}
</tbody>
</table>
</div>
);
Note the use of Object.keys() - returns an array of strings of the property names.
An example:
const base = {
HP: 45,
Attack: 49,
Defense: 49,
"Sp. Attack": 65,
"Sp. Defense": 65,
Speed: 45,
};
let objKeys = Object.keys(base).map((key) => {
return `${key} : ${base[key]}`;
});
console.log(objKeys);
Compare Object.values() and Object.entries().
Add selectedPokemon
state to the Pokemon component and compose the new PokemonInfo
component.
export default function Pokemon() {
const [pokemon, pokemonSet] = React.useState(null);
// NEW
const [selectedPokemon, selectedPokemonSet] = React.useState(null);
React.useEffect(() => {
fetch("/pokemon.json")
.then((resp) => resp.json())
.then((data) => pokemonSet(data));
}, []);
if (!pokemon) {
return <div>Loading data...</div>;
}
return (
<div>
<h1>Pokemon Search</h1>
<div>
<div>
<table width="100%">
<tbody>
{pokemon.map((pokemon) => (
<PokemonRow
key={pokemon.id}
pokemon={pokemon}
onClick={(pokemon) => selectedPokemonSet(pokemon)}
/>
))}
</tbody>
</table>
</div>
{selectedPokemon && <PokemonInfo {...selectedPokemon} />}
</div>
</div>
);
}
Test the button.
That's a lot of pokemon. We will restrict the number and then allow the user to filter them.
Array.slice()
returns an array:
<tbody>
{pokemon.slice(0, 20).map((pokemon) => (
<PokemonRow
key={pokemon.id}
pokemon={pokemon}
onClick={(pokemon) => selectedPokemonSet(pokemon)}
/>
))}
</tbody>
We will add a new filter
state and setter, an input field, and then use the filtered data as the source of displayed pokemon.
const [filter, filterSet] = React.useState("");
...
<input
type="text"
value={filter}
onChange={(event) => filterSet(event.target.value)}
/>
...
.filter((pokemon) =>
pokemon.name.english
.toLowerCase()
.includes(filter.toLowerCase())
)
Here's the end result:
export default function Pokemon() {
// NEW
const [filter, filterSet] = React.useState("");
const [pokemon, pokemonSet] = React.useState(null);
const [selectedPokemon, selectedPokemonSet] = React.useState(null);
React.useEffect(() => {
fetch("/pokemon.json")
.then((resp) => resp.json())
.then((data) => pokemonSet(data));
}, []);
if (!pokemon) {
return <div>Loading data</div>;
}
return (
<div>
<h1>Pokemon Search</h1>
<div>
<div>
<input
type="text"
value={filter}
onChange={(event) => filterSet(event.target.value)}
/>
<table width="100%">
<tbody>
{pokemon
.filter(({ name: { english } }) =>
english
.toLocaleLowerCase()
.includes(filter.toLocaleLowerCase())
)
.slice(0, 20)
.map((pokemon) => (
<PokemonRow
key={pokemon.id}
pokemon={pokemon}
onClick={(pokemon) => selectedPokemonSet(pokemon)}
/>
))}
</tbody>
</table>
</div>
{selectedPokemon && <PokemonInfo {...selectedPokemon} />}
</div>
</div>
);
}
Note the use of toLocaleLowerCase to convert the English name to lower case.
We will use Material UI as a source of ready made components.
Install Material UI:
npm install @mui/material @emotion/react @emotion/styled
Try using an MUI Button component in PokemonRow
:
import React from "react";
// NEW
import { Button } from "@mui/material";
const PokemonRow = ({ pokemon, onClick }) => (
<>
<tr key={pokemon.id}>
<td>{pokemon.name.english}</td>
<td>{pokemon.type.join(", ")}</td>
<td>
{/* NEW */}
<Button
variant="contained"
color="primary"
onClick={() => onClick(pokemon)}
>
More Information
</Button>
</td>
</tr>
</>
);
Use an MUI Text Field.
import { Button, TextField } from "@mui/material";
...
<TextField
variant="standard"
type="search"
label="Filter Pokemon"
value={filter}
onChange={(event) => filterSet(event.target.value)}
/>
At this point, depending on your light/dark system appearance preferences, they may be display issues.
Try importing the theme provider and setting the mode to either dark or light:
import { ThemeProvider, createTheme } from "@mui/material/styles";
const darkTheme = createTheme({
palette: {
mode: "dark",
},
});
...
return (
<ThemeProvider theme={darkTheme}>
...
</ThemeProvider>
MUI uses Emotion internally. Emotion is similar to Styled Components and can be used without MUI.
MUI offers page layout tools but we will use Emotion instead.
import styled from "@emotion/styled";
// ...
const Title = styled.h1`
text-align: center;
`;
const PageContainer = styled.div`
margin: auto;
width: 800px;
padding-top: 1em;
`;
const TwoColumnLayout = styled.div`
display: grid;
grid-template-columns: 80% 20%;
grid-column-gap: 1rem;
`;
const StyledTextField = styled(TextField)`
width: 100%;
padding: 0.2rem;
font-size: large;
margin: 2rem 0;
`;
export default function Pokemon() {
const [filter, filterSet] = React.useState("");
const [pokemon, pokemonSet] = React.useState(null);
const [selectedPokemon, selectedPokemonSet] = React.useState(null);
React.useEffect(() => {
fetch("/pokemon.json")
.then((resp) => resp.json())
.then((data) => pokemonSet(data));
}, []);
if (!pokemon) {
return <div>Loading data</div>;
}
return (
<PageContainer>
<Title>Pokemon Search</Title>
<TwoColumnLayout>
<div>
<StyledTextField
variant="standard"
type="search"
value={filter}
label="Filter Pokemon"
onChange={(event) => filterSet(event.target.value)}
/>
<table width="100%">
<tbody>
{pokemon
.filter(({ name: { english } }) =>
english
.toLocaleLowerCase()
.includes(filter.toLocaleLowerCase())
)
.slice(0, 20)
.map((pokemon) => (
<PokemonRow
key={pokemon.id}
pokemon={pokemon}
onClick={(pokemon) => selectedPokemonSet(pokemon)}
/>
))}
</tbody>
</table>
</div>
{selectedPokemon && <PokemonInfo {...selectedPokemon} />}
</TwoColumnLayout>
</PageContainer>
);
}
Note the syntax for the StyledTextField
component.
Create a Nav component components/nav.js
:
import Link from "next/link";
export default function Nav() {
return (
<nav>
<li>
<Link href="/">Home</Link>
</li>
<li>
<Link href="/pokemon">Pokemon</Link>
</li>
<li>
<Link href="/movies">Movies</Link>
</li>
</nav>
);
}
Import and compose it in _document.js
. Note the page refresh. This is obviously incorrect. We need to create a layout:
// components/layout.js
import Nav from "./nav";
export default function Layout({ children }) {
return (
<>
<Nav />
<main>{children}</main>
</>
);
}
And compose it in _app.js
:
import "../styles/globals.css";
import type { AppProps } from "next/app";
import Layout from "../components/layout";
export default function App({ Component, pageProps }: AppProps) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
}
Update Nav to use MUI following this formula:
import Link from "next/link";
import styled from "@emotion/styled";
import AppBar from "@mui/material/AppBar";
import Box from "@mui/material/Box";
import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
import MenuIcon from "@mui/icons-material/Menu";
const StyledLink = styled(Link)`
color: white;
text-decoration: none;
padding: 0.5rem;
`;
export default function Nav() {
return (
<Box sx={{ flexGrow: 1 }}>
<AppBar position="static">
<Toolbar>
<IconButton
size="large"
edge="start"
color="inherit"
aria-label="menu"
sx={{ mr: 2 }}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
<StyledLink href="/">Home</StyledLink>
<StyledLink href="/pokemon">Pokemon</StyledLink>
<StyledLink href="/movies">Movies</StyledLink>
</Typography>
<Button color="inherit">Login</Button>
</Toolbar>
</AppBar>
</Box>
);
}
Note: we'll need it install @mui/icons-material
for the above.
NextJS Dynamic Routes are routing utilities for creating pages that use parameters.
We'll use them to display one page per pokemon.
Create components/PokemonRow.js
and move the component from pokemon.js
:
import { Button } from "@mui/material";
export const PokemonRow = ({ pokemon, onClick }) => (
<tr key={pokemon.id}>
<td>{pokemon.name.english}</td>
<td>{pokemon.type.join(", ")}</td>
{/* NEW table cell */}
<td>
<Button
size="small"
variant="contained"
color="primary"
onClick={() => onClick(pokemon)}
>
More Information
</Button>
</td>
</tr>
);
Re-compose it into pokemon with:
import { PokemonRow } from "../components/PokemonRow";
Create a Link in PokemonRow
:
import { Button } from "@mui/material";
import Link from "next/link";
export const PokemonRow = ({ pokemon, onClick }) => (
<>
<tr key={pokemon.id}>
{/* HERE */}
<td>
<Link href={`/pokemon/${pokemon.id}`}>{pokemon.name.english}</Link>
</td>
<td>{pokemon.type.join(", ")}</td>
<td>
<Button
variant="contained"
color="primary"
onClick={() => onClick(pokemon)}
>
More Information
</Button>
</td>
</tr>
</>
);
Create pages/pokemon/[id].js
:
import { useRouter } from "next/router";
import styled from "@emotion/styled";
const PageContainer = styled.div`
margin: auto;
width: 800px;
padding-top: 1em;
`;
export default function SinglePokemon({ pokemon }) {
const router = useRouter();
return <PageContainer>{router.query.id}</PageContainer>;
}
Test the links.
When the app sees a url such as /pokemon/2
it will map the number to query parameter called id and invoke this page.
We need to make the pokemon collection available to this component in order to filter on them and display the info for a single Pokemon.
We will use React Context.
We will use React Context to make the pokemon collection available to any component in our app without prop drilling.
Create src/PokemonContext.jsx
:
import React from "react";
const PokemonContext = React.createContext({});
export default PokemonContext;
Import it into the pokemon page:
import PokemonContext from "../src/PokemonContext";
And enclose the entire component with the Context.Provider method:
// prettier-ignore
return (
<PokemonContext.Provider
value={{
filter,
pokemon,
filterSet,
pokemonSet,
selectedPokemon,
selectedPokemonSet,
}}
>
<PageContainer>
<CssBaseline />
<Title>Pokemon Search</Title>
<TwoColumnLayout>
...
</TwoColumnLayout>
</PageContainer>
</PokemonContext.Provider>
);
Let's test it.
Create components/PokemonFilter.jsx
:
import React, { useContext } from "react";
import { TextField } from "@mui/material";
import styled from "@emotion/styled";
import PokemonContext from "../src/PokemonContext";
const StyledTextField = styled(TextField)`
width: 100%;
padding: 0.2rem;
font-size: large;
margin: 2rem 0;
`;
export const PokemonFilter = () => {
const { filter, filterSet } = useContext(PokemonContext);
return (
<StyledTextField
variant="standard"
type="search"
value={filter}
label="Filter Pokemon"
onChange={(event) => filterSet(event.target.value)}
/>
);
};
And compose it in pokemon.js
:
import { PokemonFilter } from "../components/PokemonFilter";
// prettier-ignore
<div>
<PokemonFilter />
<table width="100%">
...
</table>
</div>
Note: the necessary filter and filterSet props are available in the filter component via Context, not props:
This works for PokemonFilter but not for [id].js
because it is a page, not a child of pokemon.js
. We need to set the Context higher up in the app to make the data available to all pages.
Since NextJS follows a different paradigm and structures the application into individual pages, there is no index.js
like what we saw in the Create React App. It has been abstracted away.
The work around for this is to use the custom app in pages/_app.jsx
import React from "react";
import "../styles/globals.css";
import type { AppProps } from "next/app";
import Layout from "../components/layout";
import PokemonContext from "../src/PokemonContext";
export default function App({ Component, pageProps }: AppProps) {
const [pokemon, pokemonSet] = React.useState([]);
React.useEffect(() => {
fetch("/pokemon.json")
.then((resp) => resp.json())
.then((data) => pokemonSet(data));
}, []);
return (
<PokemonContext.Provider
value={{
pokemon,
pokemonSet,
}}
>
<Layout>
<Component {...pageProps} />
</Layout>
</PokemonContext.Provider>
);
}
Once we have done this we need to change the pokemon page.
- import useContext
- Take the initial pokemon from PokemonContext
- Remove the useEffect call
import React, { useContext } from "react";
// get the pokemon from context
// const [pokemon, pokemonSet] = React.useState(null);
const { pokemon, pokemonSet } = useContext(PokemonContext);
...
// React.useEffect(() => {
// fetch("/pokemon.json")
// .then((resp) => resp.json())
// .then((data) => pokemonSet(data));
// }, []);
...
// <PokemonContext.Provider
// value={{
// filter,
// pokemon,
// filterSet,
// pokemonSet,
// selectedPokemon,
// selectedPokemonSet,
// }}
// >
Ensure we can see the pokemon collection in [id].js
:
import { useRouter } from "next/router";
import { useContext } from "react";
import PokemonContext from "../../src/PokemonContext";
import styled from "@emotion/styled";
const PageContainer = styled.div`
margin: auto;
width: 800px;
padding-top: 1em;
`;
export default function SinglePokemon() {
const { pokemon } = useContext(PokemonContext);
const router = useRouter();
return (
<PageContainer>
<pre>{JSON.stringify(pokemon[router.query.id], null, 2)}</pre>
</PageContainer>
);
}
Let's filter for the pokemon with the id and view it:
export default () => {
const { pokemon } = useContext(PokemonContext);
const router = useRouter();
const currpokemon = pokemon.find((p) => p.id === parseInt(router.query.id));
return (
<PageContainer>
{router.query.id}, {JSON.stringify(currpokemon, null, 2)}
</PageContainer>
);
};
Use MUI to style the content.
import { useContext } from "react";
import { useRouter } from "next/router";
import PokemonContext from "../../src/PokemonContext";
import styled from "@emotion/styled";
import {
CssBaseline,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
} from "@mui/material";
const PageContainer = styled.div`
margin: auto;
width: 800px;
padding-top: 1em;
`;
const TypeHeader = styled.span`
font-weight: bold;
`;
export default function SinglePokemon() {
const { pokemon } = useContext(PokemonContext);
const router = useRouter();
const currpokemon = pokemon.find((p) => p.id === parseInt(router.query.id));
return (
<PageContainer>
<CssBaseline />
<div>
{currpokemon && (
<>
<h1>{currpokemon.name.english}</h1>
<p>
<TypeHeader>Type:</TypeHeader> {" " + currpokemon.type.join(", ")}
</p>
<Table>
<TableHead>
<TableRow>
<TableCell>Attribute</TableCell>
<TableCell>Value</TableCell>
</TableRow>
</TableHead>
<TableBody>
{Object.keys(currpokemon.base).map((key) => (
<TableRow key={key}>
<TableCell>{key}</TableCell>
<TableCell>{currpokemon.base[key]}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<pre>
<code>{JSON.stringify(currpokemon, null, 2)}</code>
</pre>
</>
)}
</div>
</PageContainer>
);
}
Test the filter. It needs access to filter and filterSet. We'll add it to _app.js
along with Material UI's ThemeProvider context provider and the CssBaseLine MUI component.
In _app.js
:
import React from "react";
import PokemonContext from "../src/PokemonContext";
import { ThemeProvider, createTheme } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
const darkTheme = createTheme({
palette: {
mode: "dark",
},
});
export default function App({ Component, pageProps }) {
const [pokemon, pokemonSet] = React.useState([]);
const [filter, filterSet] = React.useState("");
React.useEffect(() => {
fetch("/pokemon.json")
.then((resp) => resp.json())
.then((data) => pokemonSet(data));
}, []);
return (
<ThemeProvider theme={darkTheme}>
<PokemonContext.Provider
value={{
pokemon,
pokemonSet,
filter,
filterSet,
}}
>
<CssBaseline />
<Component {...pageProps} />
</PokemonContext.Provider>
</ThemeProvider>
);
}
export default function Pokemon() {
// const [filter, filterSet] = React.useState("");
// const [pokemon, pokemonSet] = React.useState(null);
const { pokemon, pokemonSet, filter, filterSet } = useContext(PokemonContext);
Save and commit all files. Create a new branch.
In a SPA the page is rendered on the client (browser). In SSR the page is generated on the server when the server gets a request. This allows for superior search engine optimization.
To use SSR for a page, we need to export an async getServerSideProps function. This async function is called each time a request is made for the page.
Add to server-side-pokemon.js
:
export async function getServerSideProps() {
const response = await fetch("http://localhost:3000/pokemon.json");
const pokemon = await response.json();
return {
props: {
pokemon,
},
};
}
Any exported getServerSideProps
function is called on the server before the page is rendered and the server will create properties that are then sent to the page component.
We no longer need pokemon from Context. Remove Context, the provider, and pass the pokemon into the component:
import React from "react";
// import PokemonContext from "../src/PokemonContext";
import styled from "@emotion/styled";
import { CssBaseline } from "@mui/material";
import { PokemonRow } from "../components/PokemonRow";
import { PokemonInfo } from "../components/PokemonInfo";
import { PokemonFilter } from "../components/PokemonFilter";
const Title = styled.h1`
text-align: center;
`;
const PageContainer = styled.div`
margin: auto;
width: 800px;
padding-top: 1em;
`;
const TwoColumnLayout = styled.div`
display: grid;
grid-template-columns: 80% 20%;
grid-column-gap: 1rem;
`;
const Input = styled.input`
width: 100%;
padding: 0.2rem;
font-size: large;
`;
export async function getServerSideProps() {
const response = await fetch("http://localhost:3000/pokemon.json");
const pokemon = await response.json();
// send the list of pokemon to the component
return {
props: {
pokemon,
},
};
}
// NEW
export default function Pokemon({ pokemon }) {
const [filter, filterSet] = React.useState("");
// const { pokemon, pokemonSet } = useContext(PokemonContext);
const [selectedPokemon, selectedPokemonSet] = React.useState(null);
if (!pokemon) {
return <div>Loading data</div>;
}
return (
// <PokemonContext.Provider
// value={{
// filter,
// pokemon,
// filterSet,
// pokemonSet,
// selectedPokemon,
// selectedPokemonSet,
// }}
// >
<PageContainer>
<Title>Pokemon Search</Title>
<TwoColumnLayout>
<div>
<PokemonFilter filter={filter} filterSet={filterSet} />
<table width="100%">
<tbody>
{pokemon
.filter(({ name: { english } }) =>
english
.toLocaleLowerCase()
.includes(filter.toLocaleLowerCase())
)
.slice(0, 20)
.map((pokemon) => (
<PokemonRow
key={pokemon.id}
pokemon={pokemon}
onClick={(pokemon) => selectedPokemonSet(pokemon)}
/>
))}
</tbody>
</table>
</div>
{selectedPokemon && <PokemonInfo {...selectedPokemon} />}
</TwoColumnLayout>
</PageContainer>
// </PokemonContext.Provider>
);
}
View page source. All the the data is in a script tag.
Here, we are only generating the HTML on the server only once for the requested page so that search engines see the proper HTML while the application will behave exactly the same in the browser.
React needs the data in order to create a new tree that matches the tree on the page.
Since the server will return the proper HTML for the page, the user will no longer see a blank screen until all application resources are downloaded.
We can perform SSR on the individual views as well.
Create a new pages/ssr/
directory and save [id].js
into it.
Create an alternate link that uses this path in PokemonRow.js
:
<td>
<Link href={`/pokemon/${pokemon.id}`}>{pokemon.name.english}</Link>
<br />
<Link href={`/ssr/${pokemon.id}`}>SSR {pokemon.name.english}</Link>
</td>
In ssr/[id].js
:
// import { useContext } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
// import PokemonContext from "../../src/PokemonContext";
import {
CssBaseline,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
} from "@mui/material";
import styled from "@emotion/styled";
const PageContainer = styled.div`
margin: auto;
width: 800px;
padding-top: 1em;
`;
const TypeHeader = styled.span`
font-weight: bold;
`;
// NEW
export async function getServerSideProps(context) {
const response = await fetch("http://localhost:3000/pokemon.json");
const allPokemon = await response.json();
const currpokemon = allPokemon.find(
(p) => p.id === parseInt(context.query.id)
);
return {
props: {
currpokemon,
},
};
}
export default ({ currpokemon }) => {
const router = useRouter();
// const { pokemon } = useContext(PokemonContext);
// const currpokemon = pokemon.find((p) => p.id === parseInt(router.query.id));
return (
<PageContainer>
<CssBaseline />
<div>
{currpokemon && (
<>
<h1>SSR: {currpokemon.name.english}</h1>
<p>
<TypeHeader>Type:</TypeHeader> {" " + currpokemon.type.join(", ")}
</p>
<Table>
<TableHead>
<TableRow>
<TableCell>Attribute</TableCell>
<TableCell>Value</TableCell>
</TableRow>
</TableHead>
<TableBody>
{Object.keys(currpokemon.base).map((key) => (
<TableRow key={key}>
<TableCell>{key}</TableCell>
<TableCell>{currpokemon.base[key]}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<Link href={`/`}>Home</Link>
<pre>
<code>{JSON.stringify(currpokemon, null, 2)}</code>
</pre>
</>
)}
</div>
</PageContainer>
);
};
At this point we can comment out the fetch in _app.js
. Note the network tab. There is no fetch. The data is called on the server, not the front end.
For SSG NextJS uses getStaticPaths.
Ensure pokemon.json
is in the src
directory.
In [id].jsx
:
export const getStaticPaths = async () => {
const pokemon = require("../../src/pokemon.json");
const paths = pokemon.map((p) => ({
params: {
id: p.id.toString(),
},
}));
return {
paths,
fallback: false,
};
};
export const getStaticProps = async (context) => {
const allPokemon = require("../../src/pokemon.json");
const currpokemon = allPokemon.find(
(p) => p.id === parseInt(context.params.id)
);
return {
props: { currpokemon },
};
};
Note: at this point the filter is broken.
import React, { useContext } from "react";
import styled from "@emotion/styled";
// import PokemonContext from "../src/PokemonContext";
const Input = styled.input`
width: 100%;
padding: 0.2rem;
font-size: large;
`;
export const PokemonFilter = ({ filter, filterSet }) => {
// const { filter, filterSet } = useContext(PokemonContext);
return (
<Input
type="text"
value={filter}
onChange={(event) => filterSet(event.target.value)}
/>
);
};
And in pokemon.js:
<PokemonFilter filter={filter} filterSet={filterSet} />
In order to run SSG for the home page we need to make a few changes to pokemon.js
:
import React, { useState } from "react";
// import PokemonContext from "../src/PokemonContext";
import styled from "@emotion/styled";
import { CssBaseline } from "@mui/material";
import allpokemon from "../public/pokemon.json";
import { PokemonRow } from "../components/PokemonRow";
import { PokemonFilter } from "../components/PokemonFilter";
// const PokemonInfo = ({ name: { english }, base }) => (
// <div>
// <h2>{english}</h2>
// <table>
// <tbody>
// {Object.keys(base).map((key) => (
// <tr key={key}>
// map
// <td>{key}</td>
// <td>{base[key]}</td>
// </tr>
// ))}
// </tbody>
// </table>
// </div>
// );
const Title = styled.h1`
text-align: center;
`;
const PageContainer = styled.div`
margin: auto;
width: 800px;
padding-top: 1em;
`;
const TwoColumnLayout = styled.div`
display: grid;
grid-template-columns: 80% 20%;
grid-column-gap: 1rem;
`;
export default function Pokemon() {
const [filter, filterSet] = React.useState("");
// const { pokemon, pokemonSet } = useContext(PokemonContext);
const [pokemon, pokemonSet] = React.useState(allpokemon);
const [selectedPokemon, selectedPokemonSet] = React.useState(null);
if (!pokemon) {
return <div>Loading data</div>;
}
return (
<PageContainer>
<CssBaseline />
<Title>Pokemon Search</Title>
<TwoColumnLayout>
<div>
<PokemonFilter />
<table width="100%">
<tbody>
{pokemon
.filter(({ name: { english } }) =>
english
.toLocaleLowerCase()
.includes(filter.toLocaleLowerCase())
)
.slice(0, 20)
.map((pokemon) => (
<PokemonRow
key={pokemon.id}
pokemon={pokemon}
onClick={(pokemon) => selectedPokemonSet(pokemon)}
/>
))}
</tbody>
</table>
</div>
{selectedPokemon && <PokemonInfo {...selectedPokemon} />}
</TwoColumnLayout>
</PageContainer>
);
}
To see the individual pages for all the pokemon run a build.
Create an export script in package.json:
"scripts": {
"dev": "next dev",
"build": "next build",
"export": "next export",
"start": "next start"
},
npm run build
npm run export
cd into the new out
directory and run:
PORT=6789 npx serve
https://github.com/vercel/next.js/tree/canary/examples/with-mongodb-mongoose
Compass
https://www.mongodb.com/try/download/compass
npm install
npm i mongodb mongoose
.env.local:
lib/dbConnect.js:
import mongoose from "mongoose";
/**
Source :
https://github.com/vercel/next.js/tree/canary/examples/with-mongodb-mongoose
**/
const MONGODB_URI = process.env.MONGODB_URI;
if (!MONGODB_URI) {
throw new Error(
"Please define the MONGODB_URI environment variable inside .env.local"
);
}
/**
* Global is used here to maintain a cached connection across hot reloads
* in development. This prevents connections growing exponentially
* during API Route usage.
*/
let cached = global.mongoose;
if (!cached) {
cached = global.mongoose = { conn: null, promise: null };
}
async function dbConnect() {
if (cached.conn) {
return cached.conn;
}
if (!cached.promise) {
const opts = {
useNewUrlParser: true,
};
cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => {
return mongoose;
});
}
cached.conn = await cached.promise;
return cached.conn;
}
export default dbConnect;
models/user.js
import mongoose from "mongoose";
const UserSchema = new mongoose.Schema({
name: String,
email: String,
});
module.exports = mongoose.models.User || mongoose.model("User", UserSchema);
pages/api/users.js
import dbConnect from "../../lib/dbConnect";
import User from "../../models/User";
export default async function handler(req, res) {
const { method } = req;
await dbConnect();
switch (method) {
case "GET":
try {
const users = await User.find({});
res.status(200).json({ success: true, data: users });
} catch (error) {
res.status(400).json({ success: false });
}
break;
case "POST":
try {
const user = await User.create(req.body);
res.status(201).json({ success: true, data: user });
} catch (error) {
res.status(400).json({ success: false });
}
break;
default:
res.status(400).json({ success: false });
break;
}
}
Postman:
GET http://localhost:3000/api/users
POST http://localhost:3000/api/users
curl --request POST \
--url http://localhost:3000/api/users \
--header 'Content-Type: application/json' \
--data '{
"name": "John Doe",
"email": "john@doe.com"
}'
View the users on index.js:
export default function Home({ isConnected }) {
const [users, setUsers] = React.useState([]);
React.useEffect(() => {
getAsyncUsers();
}, []);
async function getAsyncUsers() {
const response = await fetch("http://localhost:3001/api/users");
const data = await response.json();
console.log(data);
setUsers(data.data);
}
...
<ul>
{users.map((user) => (
<li key={user._id}>{user.name}</li>
))}
</ul>