https://courses.wesbos.com/ | https://advancedreact.com/ | https://github.com/wesbos/Advanced-React
Version: 20220729 | Status: Paused at Module 7 - will continue as needed
export default function Homepage() {
return <div></div>
}
npm run dev
) }
Page.propTypes = { children: PropTypes.any, cool: PropTypes.string, }
import Page from ‘../components/Page’;
export default function IndexPage() {
return (
```html
<div>
test
<p>1</p>
<p>2</p>
</div>
export default class MyDocument extends Document {
render() {
return (
<Html lang="en-CA">
<Head></Head>
<body>
<Main />
## Module 02.03 Creating our Header and Nav Components
* Creating a header/nav component
* NextJS uses HTML push.state instead of anchor, you so can use <Link>, `import Link from 'next/link'`
## Module 03.01 An Intro to Styled Components and CSS
* `import styled from 'styled-components'`
* Example of styled components
```jsx
const Logo = styled.h1`
background: red;
a { color: white; }
`;
import { createGlobalStyle } from 'styled-components'
html {
box-sizing: border-box;
/*stuff*/
}
*,*::before,*::after {
box-sizing:inherit;
}
;export default function Page({children,cool}) {
return (
<div>
## Module 03.03 Visualizing Route Changes
* Line across the top for progress (nprogress)
* Example for main `<Page>`
```jsx
import 'Nprogress' from nprogress;
import 'nprogress/nprogress.css'; //Can use your own CSS instead
import Router from 'next/router';
Router.events.on('routeChangeStart', () > NProgress.start());
Router.events.on('routeChangeComplete', () > NProgress.done());
Router.events.on('routeChangeError', () > NProgress.done());
export default class MyDocument extends Document {
static getInitialProps({ renderPage }) {
const sheet = new ServerStyleSheet();
const page = renderPage(App => props => sheet.collectStyles(<App {…props} />));
const styleTags = sheet.getStyleElement();
return { …page, styleTags };
}
render() {
return (
<Html lang="en-CA">
<Head></Head>
<body>
<Main />
## Module 04.01 Setting up MongoDB
* Keystone runs ontop of MongoDB/Postgres
* Mongo Atlas for cloud version
* Rename sample.env to .env
## Module 04.02 An Intro to GraphQL
* GraphQL - specification for requesting/pushing data to server
* Keystone, Apollo build a layer ontop of GraphQL
* In Keystone, can see API Explorer
*
```graphql
query {
allProducts {
name
description
price
}
}
get variable from .env, put in keystone.ts
```typescript
…more stuff…
const databaseURL = process.env.DATABASE_URL || ‘mongodb://localhost/keystone-sick-fits-tutorial’;
const sessionConfig = {
maxAge: 60 * 60 * 24 * 360, //how long they should be signed in
secret: process.env.COOKIE_SECRET
}
export default config({ server: { cors: { origin: [process.env.FRONTEND_URL], credentials: true, } }, db: { adapter: ‘mongoose’, url: databaseURL, //TODO: Add data seeding here } lists: createScema({ //scheme items go in here }), ui: { //change this for roles isAccessAllowed: () => true, }, //TODO: Add Session values here });
* `npm run dev` to run keystone
## Module 04.04 Creating our first User data type
* Every time there's a datatype, we build a schema
* Set up Users section on Keystone
## Module 04.05 Adding Auth to our Application
* Adding Auth to Keystone
* Adding Session to Keystone
## Module 04.06 Creating our Products Data Type
* Creating Product list
## Module 04.07 Uploading Product Images
* Cloudinary - a service with a very generous free tier
* Can use NextJS image tag to display images
* Anytime you need environment variable, need `import 'dotenv/config';`
## Module 04.08 Creating two way data relationships in Keystone
* in the schema, you can do `ProductImage.ts`
`product: relationship({ ref: 'Product.photo' })` for a two way data relationship in Keystone. This is the Product datatype and the photo field
and in `Product.ts`
`photo: relationship({ ref: 'ProductImage.product' })`
*
## Module 04.09 Inserting Seed Data
* In `keystone.ts`, you can add Seed data but we only want to do it if there's an argument
```typescript
db: {
adapter: 'mongoose',
url: databaseURL,
async onConnect(keystone) {
if(process.argv.includes('--seed-data')) {
await insertSeedData(keystone);
}
},
},
npm run seed-data
via package.json
"scripts": {
"seed-data": "keystone-next --seed-data",
}
<Page>
app with ApolloClientOR
export { default } from ‘./products’;
* How to query items from backend
```graphql
query ALL_PRODUCTS_QUERY {
allProducts {
id
name
price
description
photo {
id
image {
publicUrlTransformed
}
}
}
}
import { useQuery } from '@apollo/client';
import gql from "graphql-tag";
const ALL_PRODUCTS_QUERY = gql`
query ALL_PRODUCTS_QUERY {
allProducts {
id
name
price
description
photo {
id
image {
publicUrlTransformed
}
}
}
}
`;
export default function Products() {
const { data, error, loading } = useQuery(ALL_PRODUCTS_QUERY);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
{ data.allProducts.map(product => (
<p key={products.id}>{product.name}</p>
))}
</div>
)
}
product?.photo?.image?.something
Optional chaining, new in JavaScriptexport default function formatMoney(amount = 0) {
const options = {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
};
if (amount % 100 === 0) {
options.minimumFractionDigits = 0;
}
const formatter = Intl.NumberFormat('en-US',options);
return formatter.format(amount/100);
}
export default function CreateProduct() { const [name, setName] = useState(‘Wes’); return ( <input value={name} onChange={(e) => { setName(e.target.value); }} > ) }
* useForm.js
```jsx
import { useEffect, useState } from 'react';
export default function useForm(initial = {}) {
// create a state object for our inputs
const [inputs, setInputs] = useState(initial);
const initialValues = Object.values(initial).join(''); //* this can't be initial or it causes infinite loop
useEffect(() => {
// This function runs when the things we are watching change
setInputs(initial);
}, [initialValues]); //* this can't be initial or it causes infinite loop
// {
// name: 'wes',
// description: 'nice shoes',
// price: 1000
// }
function handleChange(e) {
let { value, name, type } = e.target;
if (type === 'number') {
value = parseInt(value);
}
if (type === 'file') {
[value] = e.target.files; // value = e.target.files.value?
}
setInputs({
// copy the existing state
...inputs,
[name]: value,
});
}
function resetForm() {
setInputs(initial);
}
//blankState is turns object to array, changes values, then converts back to object
function clearForm() {
const blankState = Object.fromEntries( //turn to object
Object.entries(inputs).map(([key, value]) => [key, '']) //turn to array and change values
);
setInputs(blankState);
}
// return the things we want to surface from this custom hook
return {
inputs,
handleChange,
resetForm,
clearForm,
};
}
import { useForm } from '../lib/useForm';
export default function CreateProduct() {
const { inputs, handleChange } = useForm({
name: 'blah', //for testing purposes
price: 234234
});
return (
<input
name="name"
value={inputs.name}
onChange={handleChange}
>
<input
name="price"
value={inputs.price}
onChange={handleChange}
>
)
}
mutation {
createProduct(data:{
name: "Test",
description: "Test",
price: 100,
status: "Available",
}) {
id
price
description
}
}
^ this works for manual running
mutation CREATE_PRODUCT_MUTATION(
# which variables are getting passed in? what types are they
$name: String!
$description: String!
$price: Int!
$image: Upload
) {
createProduct(
data:{
name: $name,
description: $description
price: $price
status: "Available"
photo: {
create: {
image: $image,
altText: $name
}
}
}
) {
id
price
description
name
}
}
;…
const [ createProduct, { loading, error, data }] = useMutation(CREATE_PRODUCT_MUTATION, { variables: inputs });
…
<form onSubmit={ async (e) => { e.preventDefault(); const res = await createProduct(); }}></form>
…
<DisplayError error={error} /> // done by Wes Bos <fieldset disabled={loading} aria-busy={loading}> //for loading
## Module 05.07 Refetching Queries after a Successful Mutation
* Go to a page, add product, go back - it's cached!
* Two options: modify cache directly in Apollo or tell Apollo behind the scenes to refetch
* Refect Query: Export Query (e.g. ALL_PRODUCTS_QUERY) and import it in new file,
```jsx
const [ createProduct, { loading, error, data }] = useMutation(CREATE_PRODUCT_MUTATION, {
variables: inputs,
refetchQueries: [{query: ALL_PRODUCTS_QUERY }] // refetchQueries: [{query: ALL_PRODUCTS_QUERY, variables }]
});
<form onSubmit={
async (e) => {
e.preventDefault();
const res = await createProduct();
clearForm();
Router.push({
pathname: /product/${res.data.createProduct.id}
,
});
}
}></form>
## Module 05.09 Displaying Single Items, Routing and SEO
* Can have files like [id].js which Next will recognize
* You can only query single items (Product) based on unique fields (id)
```jsx
query {
Product(where: {
id: $id
})
}
query {
allProduct(where: {
name_contains_i: "yeti"
}) {
name
price
}
}
console.log({data,loading,error});
const SINGLIE_ITEM_QUERY = gql`
query SINGLE_ITEM_QUERY($id: ID!)` {
Product(where: {
id: $id
}) {
name
price
description
}
}
`;
return (
<div>
<Head>
<title>Sick Fits | {Product.name}</title>
</Head>
<stuff>
</div>
)
useEffect(() => {
// This function (setInputs(initial)) runs when the things we are watching ([initialValues]) change
setInputs(initial);
}, [initialValues]);
function update(cache,payload) {
cache.evict(cache.identify(payload.data.deleteProduct)
}