├── .eslintrc.json ├── .gitignore ├── README.md ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── 01-js-to-ts.tsx ├── 02-mutually-exclusive.tsx ├── 03-more-strict.tsx ├── 04-validating-ajax-data.tsx ├── 05-inferring-types.tsx ├── 06-generic-prop-types.tsx ├── 07-deriving-prop-types.tsx ├── 08-inferring-schema-types.tsx ├── 09-type-mapping.tsx ├── 10-using-readonly.tsx ├── 11-using-deep-readonly.tsx ├── 12-displaying-types.tsx ├── 13-predicates-assertions.tsx ├── _app.tsx ├── api │ └── hello.ts └── index.tsx ├── public ├── api │ ├── bad-extra-ingredients.json │ ├── good-extra-ingredients.json │ └── pizzas.json ├── favicon.ico ├── img │ ├── funghi.png │ ├── hawaii.png │ ├── margherita.png │ ├── quattro-formaggi.png │ └── rustica.png └── vercel.svg ├── src ├── 01-js-to-ts │ └── alert.tsx ├── 02-mutually-exclusive │ └── dual-alert.tsx ├── 03-more-strict │ ├── menu.ts │ ├── ordered-pizza.tsx │ ├── pizza-on-menu.tsx │ ├── pizza-shop.tsx │ └── types.ts ├── 04-validating-ajax-data │ ├── ordered-pizza.tsx │ ├── pizza-on-menu.tsx │ ├── pizza-shop-data-loader.tsx │ ├── pizza-shop.tsx │ ├── schemas.ts │ └── types.ts ├── 05-inferring-types │ └── configuration.tsx ├── 06-generic-prop-types │ ├── generic-form.tsx │ └── two-forms.tsx ├── 07-deriving-prop-types │ ├── ordered-pizza.tsx │ ├── pizza-on-menu.tsx │ ├── pizza-shop-data-loader.tsx │ ├── pizza-shop.tsx │ ├── schemas.ts │ └── types.ts ├── 08-inferring-schema-types │ ├── ordered-pizza.tsx │ ├── pizza-on-menu.tsx │ ├── pizza-shop-data-loader.tsx │ ├── pizza-shop.tsx │ ├── schemas.ts │ └── types.ts ├── 09-type-mapping │ ├── ordered-pizza.tsx │ ├── pizza-on-menu.tsx │ ├── pizza-shop-data-loader.tsx │ ├── pizza-shop.tsx │ ├── schemas.ts │ └── types.ts ├── 10-using-readonly │ ├── menu.ts │ ├── ordered-pizza.tsx │ ├── pizza-on-menu.tsx │ ├── pizza-shop.tsx │ └── types.ts ├── 11-using-deep-readonly │ ├── menu.ts │ ├── ordered-pizza.tsx │ ├── pizza-on-menu.tsx │ ├── pizza-shop.tsx │ └── types.ts ├── 12-displaying-types │ ├── menu.ts │ ├── ordered-pizza.tsx │ ├── pizza-on-menu.tsx │ ├── pizza-shop.tsx │ └── types.ts ├── 13-predicates-assertions │ ├── book-on-sale.tsx │ ├── book-shop.tsx │ ├── images │ │ ├── book.png │ │ ├── magazine.png │ │ └── pen.png │ ├── item-on-sale.tsx │ ├── magazine-on-sale.tsx │ ├── pen-on-sale.tsx │ └── types.ts ├── components │ ├── checkbox.tsx │ ├── footer.tsx │ ├── header.tsx │ ├── index.ts │ ├── input.tsx │ ├── label.tsx │ ├── labeled-checkbox.tsx │ └── labeled-input.tsx └── messages.ts ├── styles ├── Home.module.css └── globals.css └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "advanced-react-typescript-2022", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "compile": "tsc --noEmit", 8 | "compile:watch": "tsc --noEmit --watch", 9 | "build": "next build", 10 | "start": "next start", 11 | "lint": "next lint" 12 | }, 13 | "dependencies": { 14 | "bootstrap": "5.2.1", 15 | "next": "12.3.1", 16 | "react": "18.2.0", 17 | "react-dom": "18.2.0", 18 | "react-intl": "6.1.2", 19 | "swr": "1.3.0", 20 | "zod": "3.19.1" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "18.7.23", 24 | "@types/react": "18.0.21", 25 | "@types/react-dom": "18.0.6", 26 | "eslint": "8.24.0", 27 | "eslint-config-next": "12.3.1", 28 | "typescript": "4.8.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pages/01-js-to-ts.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | 3 | import { Alert } from '../src/01-js-to-ts/alert'; 4 | 5 | const JsToTs: NextPage = () => { 6 | return ( 7 |
8 |

Converting JavaScript to TypeScript

9 | 10 | 11 |
12 | ); 13 | }; 14 | 15 | export default JsToTs; 16 | -------------------------------------------------------------------------------- /pages/02-mutually-exclusive.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | 3 | import { DualAlert } from '../src/02-mutually-exclusive/dual-alert'; 4 | 5 | const MutuallyExclusive: NextPage = () => { 6 | return ( 7 |
8 |

Mutually exclusive props

9 | 10 | 14 |
15 | ); 16 | }; 17 | 18 | export default MutuallyExclusive; 19 | -------------------------------------------------------------------------------- /pages/03-more-strict.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import { PizzaShop } from '../src/03-more-strict/pizza-shop'; 3 | 4 | const MoreStrict: NextPage = () => { 5 | return ( 6 |
7 |

More strict features

8 | 9 | 10 |
11 | ); 12 | }; 13 | 14 | export default MoreStrict; 15 | -------------------------------------------------------------------------------- /pages/04-validating-ajax-data.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import { PizzaShopDataLoader } from '../src/04-validating-ajax-data/pizza-shop-data-loader'; 3 | 4 | const ValidatingAjaxData: NextPage = () => { 5 | return ( 6 |
7 |

Validating data at the boundary

8 | 9 | 10 |
11 | ); 12 | }; 13 | 14 | export default ValidatingAjaxData; 15 | -------------------------------------------------------------------------------- /pages/05-inferring-types.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import { Configuration } from '../src/05-inferring-types/configuration'; 3 | 4 | const InferringTypes: NextPage = () => { 5 | return ( 6 |
7 |

Inferring TypeScript types

8 | 9 | 10 |
11 | ); 12 | }; 13 | 14 | export default InferringTypes; 15 | -------------------------------------------------------------------------------- /pages/06-generic-prop-types.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import { TwoForms } from '../src/06-generic-prop-types/two-forms'; 3 | 4 | const InferringTypes: NextPage = () => { 5 | return ( 6 |
7 |

Generic React prop types

8 | 9 | 10 |
11 | ); 12 | }; 13 | 14 | export default InferringTypes; 15 | -------------------------------------------------------------------------------- /pages/07-deriving-prop-types.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import { PizzaShopDataLoader } from '../src/07-deriving-prop-types/pizza-shop-data-loader'; 3 | 4 | const DerivingPropTypes: NextPage = () => { 5 | return ( 6 |
7 |

Deriving component prop types

8 | 9 | 10 |
11 | ); 12 | }; 13 | 14 | export default DerivingPropTypes; 15 | -------------------------------------------------------------------------------- /pages/08-inferring-schema-types.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import { PizzaShopDataLoader } from '../src/08-inferring-schema-types/pizza-shop-data-loader'; 3 | 4 | const InferringSchemaTypes: NextPage = () => { 5 | return ( 6 |
7 |

Inferring Zod schema types

8 | 9 | 10 |
11 | ); 12 | }; 13 | 14 | export default InferringSchemaTypes; 15 | -------------------------------------------------------------------------------- /pages/09-type-mapping.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import { PizzaShopDataLoader } from '../src/09-type-mapping/pizza-shop-data-loader'; 3 | 4 | const TypeMapping: NextPage = () => { 5 | return ( 6 |
7 |

8 | Type mapping with Omit<> and/or Pick<> 9 |

10 | 11 | 12 |
13 | ); 14 | }; 15 | 16 | export default TypeMapping; 17 | -------------------------------------------------------------------------------- /pages/10-using-readonly.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import { PizzaShop } from '../src/10-using-readonly/pizza-shop'; 3 | 4 | const UsingReadonly: NextPage = () => { 5 | return ( 6 |
7 |

Using Readonly<T>

8 | 9 | 10 |
11 | ); 12 | }; 13 | 14 | export default UsingReadonly; 15 | -------------------------------------------------------------------------------- /pages/11-using-deep-readonly.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import { PizzaShop } from '../src/11-using-deep-readonly/pizza-shop'; 3 | 4 | const UsingDeepReadonly: NextPage = () => { 5 | return ( 6 |
7 |

Custom type mapping DeepReadonly<T>

8 | 9 | 10 |
11 | ); 12 | }; 13 | 14 | export default UsingDeepReadonly; 15 | -------------------------------------------------------------------------------- /pages/12-displaying-types.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import { PizzaShop } from '../src/12-displaying-types/pizza-shop'; 3 | 4 | const DisplayingTypes: NextPage = () => { 5 | return ( 6 |
7 |

Displaying types

8 | 9 | 10 |
11 | ); 12 | }; 13 | 14 | export default DisplayingTypes; 15 | -------------------------------------------------------------------------------- /pages/13-predicates-assertions.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import { BookShop } from '../src/13-predicates-assertions/book-shop'; 3 | 4 | const PredicatesAssertions: NextPage = () => { 5 | return ( 6 |
7 |

Predicates, Assertions & Exhaustiveness Checking

8 | 9 |
10 | ); 11 | }; 12 | 13 | export default PredicatesAssertions; 14 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app'; 2 | import Head from 'next/head'; 3 | import { IntlProvider } from 'react-intl'; 4 | 5 | import 'bootstrap/dist/css/bootstrap.min.css'; 6 | 7 | import { Footer, Header } from '../src/components'; 8 | import { messages } from '../src/messages'; 9 | 10 | function MyApp({ Component, pageProps }: AppProps) { 11 | return ( 12 | 13 | 14 | Advanced TypeScript for React developers 15 | 19 | 20 | 21 | 22 |
23 |
24 |
25 | 26 |
27 |
29 |
30 | ); 31 | } 32 | 33 | export default MyApp; 34 | -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | 3 | import Link from 'next/link'; 4 | 5 | const mainMenu = [ 6 | { 7 | label: '1 - React Components using TypeScript', 8 | href: '/01-js-to-ts', 9 | }, 10 | { 11 | label: '2 - Mutually exclusive props', 12 | href: '/02-mutually-exclusive', 13 | }, 14 | { 15 | label: '3 - More strict features', 16 | href: '/03-more-strict', 17 | }, 18 | { 19 | label: '4 - Validating data at the boundary', 20 | href: '/04-validating-ajax-data', 21 | }, 22 | { 23 | label: '5 - Inferring TypeScript types', 24 | href: '/05-inferring-types', 25 | }, 26 | { 27 | label: '6 - Generic React prop types', 28 | href: '/06-generic-prop-types', 29 | }, 30 | { 31 | label: '7 - Deriving component prop types', 32 | href: '/07-deriving-prop-types', 33 | }, 34 | { 35 | label: '8 - Inferring Zod schema types', 36 | href: '/08-inferring-schema-types', 37 | }, 38 | { 39 | label: '9 - Type mapping with Omit<> and/or Pick<>', 40 | href: '/09-type-mapping', 41 | }, 42 | { 43 | label: '10 - Using Readonly<>', 44 | href: '/10-using-readonly', 45 | }, 46 | { 47 | label: '11 - Custom type mapping DeepReadonly<>', 48 | href: '/11-using-deep-readonly', 49 | }, 50 | { 51 | label: '12 - Displaying types', 52 | href: '/12-displaying-types', 53 | }, 54 | { 55 | label: '13 - Type predicate functions', 56 | href: '/13-predicates-assertions', 57 | }, 58 | { 59 | label: '14 - Type assertion functions', 60 | href: '/13-predicates-assertions', 61 | }, 62 | { 63 | label: '15 - Exhaustiveness checking', 64 | href: '/13-predicates-assertions', 65 | }, 66 | ]; 67 | 68 | const Home: NextPage = () => { 69 | return ( 70 |
    71 | {mainMenu.map((item, index) => ( 72 |
  1. 73 | 74 | {item.label} 75 | 76 |
  2. 77 | ))} 78 |
79 | ); 80 | }; 81 | 82 | export default Home; 83 | 84 | const notString: string = '1'; 85 | console.log(notString); 86 | -------------------------------------------------------------------------------- /public/api/bad-extra-ingredients.json: -------------------------------------------------------------------------------- 1 | { 2 | "cheese": { 3 | "name": "Cheese", 4 | "price": 0.5 5 | }, 6 | "peppers": { 7 | "name": "Green and red bell pepper", 8 | "price": 0.5 9 | }, 10 | "pepperoni": { 11 | "name": "Pepperoni", 12 | "price": 0.75 13 | }, 14 | "tomatoes": { 15 | "name": "Tomatoes", 16 | "price": 0.5 17 | }, 18 | "olives": { 19 | "name": "Olives", 20 | "price": 0.6 21 | }, 22 | "mushrooms": { 23 | "name": "Mushrooms", 24 | "price": "0.6" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /public/api/good-extra-ingredients.json: -------------------------------------------------------------------------------- 1 | { 2 | "cheese": { 3 | "name": "Cheese", 4 | "price": 0.5 5 | }, 6 | "peppers": { 7 | "name": "Green and red bell pepper", 8 | "price": 0.5 9 | }, 10 | "pepperoni": { 11 | "name": "Pepperoni", 12 | "price": 0.75 13 | }, 14 | "tomatoes": { 15 | "name": "Tomatoes", 16 | "price": 0.5 17 | }, 18 | "olives": { 19 | "name": "Olives", 20 | "price": 0.6 21 | }, 22 | "mushrooms": { 23 | "name": "Mushrooms", 24 | "price": 0.6 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /public/api/pizzas.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Pizza Margherita", 4 | "ingredients": ["tomato sauce", "mozzarella", "basil"], 5 | "price": 7.95, 6 | "extras": ["olives"] 7 | }, 8 | { 9 | "name": "Pizza Quattro formaggi", 10 | "ingredients": ["mozzarella", "gorgonzola", "provolone", "parmesan cheese"], 11 | "price": 9.45, 12 | "extras": ["cheese"] 13 | }, 14 | { 15 | "name": "Pizza Rustica", 16 | "ingredients": [ 17 | "tomato sauce", 18 | "mozzarella", 19 | "seasoned minced beef", 20 | "red peppers" 21 | ], 22 | "price": 9.95, 23 | "extras": ["cheese", "pepperoni", "peppers"] 24 | }, 25 | { 26 | "name": "Pizza Ai funghi", 27 | "ingredients": ["tomato sauce", "mozzarella", "mushrooms", "garlic"], 28 | "price": 10.95, 29 | "extras": ["cheese", "mushrooms"] 30 | }, 31 | { 32 | "name": "Pizza Hawaii", 33 | "ingredients": ["tomato sauce", "mozzarella", "prosciutto", "pineapple"], 34 | "price": 11.75, 35 | "extras": ["cheese", "pepperoni", "tomatoes"] 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mauricedb/advanced-react-typescript-2022/fe535deb86057ff751a0b38b3eb2f09f7ab0feda/public/favicon.ico -------------------------------------------------------------------------------- /public/img/funghi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mauricedb/advanced-react-typescript-2022/fe535deb86057ff751a0b38b3eb2f09f7ab0feda/public/img/funghi.png -------------------------------------------------------------------------------- /public/img/hawaii.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mauricedb/advanced-react-typescript-2022/fe535deb86057ff751a0b38b3eb2f09f7ab0feda/public/img/hawaii.png -------------------------------------------------------------------------------- /public/img/margherita.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mauricedb/advanced-react-typescript-2022/fe535deb86057ff751a0b38b3eb2f09f7ab0feda/public/img/margherita.png -------------------------------------------------------------------------------- /public/img/quattro-formaggi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mauricedb/advanced-react-typescript-2022/fe535deb86057ff751a0b38b3eb2f09f7ab0feda/public/img/quattro-formaggi.png -------------------------------------------------------------------------------- /public/img/rustica.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mauricedb/advanced-react-typescript-2022/fe535deb86057ff751a0b38b3eb2f09f7ab0feda/public/img/rustica.png -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/01-js-to-ts/alert.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { useIntl } from 'react-intl'; 3 | 4 | type Props = { 5 | messageId: string; 6 | variant: 'primary' | 'secondary' | 'success' | 'danger'; 7 | }; 8 | 9 | export const Alert: FC = ({ messageId, variant }) => { 10 | const { formatMessage } = useIntl(); 11 | 12 | if (!messageId) { 13 | throw new Error('The messageId prop is required'); 14 | } 15 | 16 | return ( 17 |
18 | {formatMessage({ id: messageId })} 19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/02-mutually-exclusive/dual-alert.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import { useIntl } from 'react-intl'; 3 | 4 | type Variant = 5 | | 'danger' 6 | | 'dark' 7 | | 'info' 8 | | 'light' 9 | | 'primary' 10 | | 'secondary' 11 | | 'success' 12 | | 'warning'; 13 | 14 | type Props = ( 15 | | { 16 | message: string; 17 | messageId?: never; 18 | } 19 | | { 20 | message?: never; 21 | messageId: string; 22 | } 23 | ) & { 24 | variant?: Variant; 25 | }; 26 | 27 | export const DualAlert: FC = ({ 28 | message, 29 | messageId, 30 | variant = 'primary', 31 | }) => { 32 | const { formatMessage } = useIntl(); 33 | 34 | const actualMessage = message ?? formatMessage({ id: messageId }); 35 | 36 | return ( 37 |
38 | {actualMessage} 39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/03-more-strict/menu.ts: -------------------------------------------------------------------------------- 1 | import { ExtraIngredient, ExtraIngredients, Pizza } from './types'; 2 | 3 | export const pizzas: Pizza[] = [ 4 | { 5 | name: 'Pizza Margherita', 6 | ingredients: ['tomato sauce', 'mozzarella', 'basil'], 7 | image: '/img/margherita.png', 8 | price: 7.95, 9 | extras: ['olives'], 10 | }, 11 | { 12 | name: 'Pizza Quattro formaggi', 13 | ingredients: ['mozzarella', 'gorgonzola', 'provolone', 'parmesan cheese'], 14 | image: '/img/quattro-formaggi.png', 15 | price: 9.45, 16 | extras: ['cheese'], 17 | }, 18 | { 19 | name: 'Pizza Rustica', 20 | ingredients: [ 21 | 'tomato sauce', 22 | 'mozzarella', 23 | 'seasoned minced beef', 24 | 'red peppers', 25 | ], 26 | image: '/img/rustica.png', 27 | price: 9.95, 28 | extras: ['cheese', 'pepperoni', 'peppers'], 29 | }, 30 | { 31 | name: 'Pizza Ai funghi', 32 | ingredients: ['tomato sauce', 'mozzarella', 'mushrooms', 'garlic'], 33 | image: '/img/funghi.png', 34 | price: 10.95, 35 | extras: ['cheese', 'mushrooms'], 36 | }, 37 | { 38 | name: 'Pizza Hawaii', 39 | ingredients: ['tomato sauce', 'mozzarella', 'prosciutto', 'pineapple'], 40 | image: '/img/hawaii.png', 41 | price: 11.75, 42 | extras: ['cheese', 'pepperoni', 'tomatoes'], 43 | }, 44 | ]; 45 | 46 | export const extraIngredients: ExtraIngredients = { 47 | cheese: { name: 'Cheese', price: 0.5 }, 48 | peppers: { name: 'Green and red bell pepper', price: 0.5 }, 49 | pepperoni: { name: 'Pepperoni', price: 0.75 }, 50 | tomatoes: { name: 'Tomatoes', price: 0.5 }, 51 | olives: { name: 'Olives', price: 0.6 }, 52 | mushroms: { name: 'Mushrooms', price: 0.6 }, 53 | }; 54 | 55 | export const getExtraIngredient = (name: string): ExtraIngredient => { 56 | const extraIngredient = extraIngredients[name] ?? { name, price: 0 }; 57 | 58 | return extraIngredient; 59 | }; 60 | -------------------------------------------------------------------------------- /src/03-more-strict/ordered-pizza.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import type { PizzaOnOrder } from './types'; 3 | 4 | import { useIntl } from 'react-intl'; 5 | 6 | type Props = { 7 | pizza: PizzaOnOrder; 8 | }; 9 | 10 | export const OrderedPizza: FC = ({ pizza }) => { 11 | const { formatNumber } = useIntl(); 12 | 13 | return ( 14 |
15 |
16 |

17 |
{pizza.name}
18 |
19 | {formatNumber(pizza.price, { 20 | style: 'currency', 21 | currency: 'EUR', 22 | })} 23 |
24 |

25 |
    26 | {pizza.extraIngredients.map((extra, index) => ( 27 |
  1. 28 |
    {extra.name}
    29 |
    30 | {formatNumber(extra.price, { 31 | style: 'currency', 32 | currency: 'EUR', 33 | })} 34 |
    35 |
  2. 36 | ))} 37 |
38 |
39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/03-more-strict/pizza-on-menu.tsx: -------------------------------------------------------------------------------- 1 | import type { Pizza, PizzaOnOrder } from './types'; 2 | 3 | import { FC, useState } from 'react'; 4 | 5 | import Image from 'next/image'; 6 | import { useIntl } from 'react-intl'; 7 | import { LabeledCheckbox } from '../components'; 8 | import { getExtraIngredient } from './menu'; 9 | 10 | type Props = { 11 | pizza: Pizza; 12 | onAddToOrder: (pizza: PizzaOnOrder) => void; 13 | }; 14 | 15 | export const PizzaOnMenu: FC = ({ pizza, onAddToOrder }) => { 16 | const [extras, setExtras] = useState([]); 17 | const { formatList } = useIntl(); 18 | 19 | return ( 20 |
21 | {pizza.name} 28 |
29 |

{pizza.name}

30 |
31 |
Ingredients
32 |
33 | {formatList( 34 | pizza.ingredients.map((ingredient) => ( 35 | 36 | {ingredient} 37 | 38 | )) 39 | )} 40 |
41 |
42 |
43 |
Extras
44 | {pizza.extras.map((extra) => ( 45 | { 49 | setExtras((extras) => { 50 | if (extras.includes(extra)) { 51 | return extras.filter((x) => x !== extra); 52 | } else { 53 | return [...extras, extra]; 54 | } 55 | }); 56 | }} 57 | > 58 | {extra} 59 | 60 | ))} 61 |
62 | 73 |
74 |
75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /src/03-more-strict/pizza-shop.tsx: -------------------------------------------------------------------------------- 1 | import type { PizzaOnOrder } from './types'; 2 | 3 | import { FC, useMemo, useState } from 'react'; 4 | import { useIntl } from 'react-intl'; 5 | import { pizzas } from './menu'; 6 | import { OrderedPizza } from './ordered-pizza'; 7 | import { PizzaOnMenu } from './pizza-on-menu'; 8 | 9 | export const PizzaShop: FC = () => { 10 | const { formatNumber } = useIntl(); 11 | const [order, setOrder] = useState([]); 12 | const totalPrice = useMemo(() => calculateTotalPrice(order), [order]); 13 | 14 | return ( 15 |
16 |

Pizza Shop

17 |
18 |
19 |

Menu

20 | {pizzas.map((pizza) => ( 21 | setOrder((order) => [...order, p])} 25 | /> 26 | ))} 27 |
28 |
29 |
30 |

Your Order

31 | {order.map((pizza, index) => ( 32 | 33 | ))} 34 |
35 |
Total amount:
36 |
37 | {formatNumber(totalPrice, { 38 | style: 'currency', 39 | currency: 'EUR', 40 | })} 41 |
42 |
43 |
44 |
45 |
46 |
47 | ); 48 | }; 49 | 50 | function calculateTotalPrice(order: PizzaOnOrder[]) { 51 | return order.reduce((sum, pizza) => { 52 | const extraPrice = pizza.extraIngredients.reduce( 53 | (sum, extra) => sum + extra.price, 54 | 0 55 | ); 56 | return sum + pizza.price + extraPrice; 57 | }, 0); 58 | } 59 | -------------------------------------------------------------------------------- /src/03-more-strict/types.ts: -------------------------------------------------------------------------------- 1 | export type Pizza = { 2 | name: string; 3 | ingredients: string[]; 4 | image: string; 5 | price: number; 6 | extras: string[]; 7 | }; 8 | 9 | export type ExtraIngredients = Record; 10 | 11 | export type ExtraIngredient = { 12 | name: string; 13 | price: number; 14 | }; 15 | 16 | export type PizzaOnOrder = { 17 | name: string; 18 | price: number; 19 | extraIngredients: ExtraIngredient[]; 20 | }; 21 | -------------------------------------------------------------------------------- /src/04-validating-ajax-data/ordered-pizza.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import type { PizzaOnOrder } from './types'; 3 | 4 | import { useIntl } from 'react-intl'; 5 | 6 | type Props = { 7 | pizza: PizzaOnOrder; 8 | }; 9 | 10 | export const OrderedPizza: FC = ({ pizza }) => { 11 | const { formatNumber } = useIntl(); 12 | 13 | return ( 14 |
15 |
16 |

17 |
{pizza.name}
18 |
19 | {formatNumber(pizza.price, { 20 | style: 'currency', 21 | currency: 'EUR', 22 | })} 23 |
24 |

25 |
    26 | {pizza.extraIngredients.map((extra, index) => ( 27 |
  1. 28 |
    {extra.name}
    29 |
    30 | {formatNumber(extra.price, { 31 | style: 'currency', 32 | currency: 'EUR', 33 | })} 34 |
    35 |
  2. 36 | ))} 37 |
38 |
39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/04-validating-ajax-data/pizza-on-menu.tsx: -------------------------------------------------------------------------------- 1 | import type { ExtraIngredient, Pizza, PizzaOnOrder } from './types'; 2 | 3 | import { FC, useState } from 'react'; 4 | 5 | import { useIntl } from 'react-intl'; 6 | import { LabeledCheckbox } from '../components'; 7 | 8 | type Props = { 9 | pizza: Pizza; 10 | getExtraIngredient: (name: string) => ExtraIngredient; 11 | onAddToOrder: (pizza: PizzaOnOrder) => void; 12 | }; 13 | 14 | export const PizzaOnMenu: FC = ({ 15 | pizza, 16 | getExtraIngredient, 17 | onAddToOrder, 18 | }) => { 19 | const [extras, setExtras] = useState([]); 20 | const { formatList } = useIntl(); 21 | 22 | return ( 23 |
24 |
25 |

{pizza.name}

26 |
27 |
Ingredients:
28 |
29 | {formatList( 30 | pizza.ingredients.map((ingredient) => ( 31 | 32 | {ingredient} 33 | 34 | )) 35 | )} 36 |
37 |
38 |
39 |
Extras
40 | {pizza.extras.map((extra) => ( 41 | { 45 | setExtras((extras) => { 46 | if (extras.includes(extra)) { 47 | return extras.filter((x) => x !== extra); 48 | } else { 49 | return [...extras, extra]; 50 | } 51 | }); 52 | }} 53 | > 54 | {extra} 55 | 56 | ))} 57 |
58 | 69 |
70 |
71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /src/04-validating-ajax-data/pizza-shop-data-loader.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import useSWR from 'swr'; 3 | 4 | import type { ExtraIngredients, Pizza } from './types'; 5 | 6 | import { PizzaShop } from './pizza-shop'; 7 | import { extraIngredientsSchema, pizzasSchema } from './schemas'; 8 | 9 | const server = 'http://localhost:3000'; 10 | 11 | export const PizzaShopDataLoader: FC = () => { 12 | const { data: pizzas, error: pizzasError } = useSWR( 13 | '/api/pizzas.json', 14 | (resource, init) => 15 | fetch(`${server}${resource}`, init) 16 | .then((res) => res.json()) 17 | .then(pizzasSchema.parse) 18 | ); 19 | 20 | const { data: extraIngredients, error: extraIngredientsError } = 21 | useSWR( 22 | '/api/good-extra-ingredients.json', 23 | (resource, init) => 24 | fetch(`${server}${resource}`, init) 25 | .then((res) => res.json()) 26 | .then(extraIngredientsSchema.parse) 27 | ); 28 | 29 | if (pizzasError || extraIngredientsError) { 30 | return ( 31 |
32 | Something went wrong 33 | {pizzasError?.message ?? extraIngredientsError?.message} 34 |
35 | ); 36 | } 37 | 38 | if (!pizzas || !extraIngredients) { 39 | return ( 40 |
41 | Loading... 42 |
43 | ); 44 | } 45 | 46 | return ; 47 | }; 48 | -------------------------------------------------------------------------------- /src/04-validating-ajax-data/pizza-shop.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useMemo, useState } from 'react'; 2 | 3 | import type { 4 | ExtraIngredient, 5 | ExtraIngredients, 6 | Pizza, 7 | PizzaOnOrder, 8 | } from './types'; 9 | 10 | import { OrderedPizza } from './ordered-pizza'; 11 | import { PizzaOnMenu } from './pizza-on-menu'; 12 | 13 | type Props = { 14 | extraIngredients: ExtraIngredients; 15 | pizzas: Pizza[]; 16 | }; 17 | 18 | export const PizzaShop: FC = ({ extraIngredients, pizzas }) => { 19 | const [order, setOrder] = useState([]); 20 | const totalPrice = useMemo(() => calculateTotalPrice(order), [order]); 21 | 22 | const getExtraIngredient = (name: string): ExtraIngredient => 23 | extraIngredients[name]!; 24 | 25 | return ( 26 |
27 |

Pizza Shop

28 |
29 |
30 |

Menu

31 | {pizzas!.map((pizza) => ( 32 | setOrder((order) => [...order, p])} 37 | /> 38 | ))} 39 |
40 |
41 |
42 |

Your Order

43 | {order.map((pizza, index) => ( 44 | 45 | ))} 46 |
47 |
Total amount:
48 |
€{totalPrice}
49 |
50 |
51 |
52 |
53 |
54 | ); 55 | }; 56 | 57 | function calculateTotalPrice(order: PizzaOnOrder[]) { 58 | return order.reduce( 59 | (sum, pizza) => 60 | sum + 61 | pizza.price + 62 | pizza.extraIngredients.reduce((sum, extra) => sum + extra.price, 0), 63 | 0 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/04-validating-ajax-data/schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const pizzaSchema = z.object({ 4 | name: z.string(), 5 | ingredients: z.string().array(), 6 | price: z.number(), 7 | extras: z.string().array(), 8 | }); 9 | 10 | export const pizzasSchema = z.array(pizzaSchema); 11 | 12 | export const extraIngredientSchema = z.object({ 13 | name: z.string(), 14 | price: z.number(), 15 | }); 16 | 17 | export const extraIngredientsSchema = z.record(extraIngredientSchema); 18 | -------------------------------------------------------------------------------- /src/04-validating-ajax-data/types.ts: -------------------------------------------------------------------------------- 1 | export type Pizza = { 2 | name: string; 3 | ingredients: string[]; 4 | price: number; 5 | extras: string[]; 6 | }; 7 | 8 | export type ExtraIngredients = Record; 9 | 10 | export type ExtraIngredient = { 11 | name: string; 12 | price: number; 13 | }; 14 | 15 | export type PizzaOnOrder = { 16 | name: string; 17 | price: number; 18 | extraIngredients: ExtraIngredient[]; 19 | }; 20 | -------------------------------------------------------------------------------- /src/05-inferring-types/configuration.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { LabeledInput } from '../components'; 3 | 4 | function getConfigItem< 5 | TSection extends keyof typeof config, 6 | TItem extends keyof typeof config[TSection] 7 | >(section: TSection, item: TItem) { 8 | const config = { 9 | user: { 10 | firstName: 'John', 11 | lastName: 'Doe', 12 | birthDate: new Date(1990, 6, 10), 13 | }, 14 | address: { 15 | street: 'Main St', 16 | houseNumber: 123, 17 | city: 'New York', 18 | }, 19 | }; 20 | 21 | return config[section][item]; 22 | } 23 | 24 | export const Configuration: FC = () => { 25 | const firstName = getConfigItem('user', 'firstName'); 26 | const lastName = getConfigItem('user', 'lastName'); 27 | const birthDate = getConfigItem('user', 'birthDate').toLocaleDateString(); 28 | 29 | // const employer = getConfigItem('employer', 'name'); 30 | 31 | const street = getConfigItem('address', 'street'); 32 | const houseNumber = getConfigItem('address', 'houseNumber'); 33 | const city = getConfigItem('address', 'city').toUpperCase(); 34 | 35 | return ( 36 |
37 |

Configuration

38 |
39 | 40 | First name: 41 | 42 | 43 | Last name: 44 | 45 | 46 | Birth date: 47 | 48 | 49 | Address: 50 | 51 |
52 |
53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /src/06-generic-prop-types/generic-form.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState } from 'react'; 2 | import { LabeledInput } from '../components'; 3 | 4 | type Props = { 5 | header: string; 6 | initialValues: TValues; 7 | onSubmit: (values: TValues) => void; 8 | }; 9 | 10 | export function GenericForm>({ 11 | header, 12 | initialValues, 13 | onSubmit, 14 | }: Props) { 15 | const [values, setValues] = useState(initialValues); 16 | 17 | return ( 18 |
onSubmit(values)}> 19 |

{header}

20 | 21 | {Object.keys(values).map((key) => ( 22 | setValues({ ...values, [key]: e.target.value })} 26 | > 27 | {key}: 28 | 29 | ))} 30 | 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/06-generic-prop-types/two-forms.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { GenericForm } from './generic-form'; 3 | 4 | export const TwoForms: FC = () => { 5 | return ( 6 | <> 7 | 14 | alert( 15 | `${values.firstName} ${values.lastName}\n\n${JSON.stringify( 16 | values, 17 | null, 18 | 2 19 | )}` 20 | ) 21 | } 22 | /> 23 | 24 | 32 | alert( 33 | `${values.street} ${values.houseNumber} ${ 34 | values.city 35 | }\n\n${JSON.stringify(values, null, 2)}` 36 | ) 37 | } 38 | /> 39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/07-deriving-prop-types/ordered-pizza.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import type { PizzaOnOrder } from './types'; 3 | 4 | import { useIntl } from 'react-intl'; 5 | 6 | type Props = { 7 | pizza: PizzaOnOrder; 8 | }; 9 | 10 | export const OrderedPizza: FC = ({ pizza }) => { 11 | const { formatNumber } = useIntl(); 12 | 13 | return ( 14 |
15 |
16 |

17 |
{pizza.name}
18 |
19 | {formatNumber(pizza.price, { 20 | style: 'currency', 21 | currency: 'EUR', 22 | })} 23 |
24 |

25 |
    26 | {pizza.extraIngredients.map((extra, index) => ( 27 |
  1. 28 |
    {extra.name}
    29 |
    30 | {formatNumber(extra.price, { 31 | style: 'currency', 32 | currency: 'EUR', 33 | })} 34 |
    35 |
  2. 36 | ))} 37 |
38 |
39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/07-deriving-prop-types/pizza-on-menu.tsx: -------------------------------------------------------------------------------- 1 | import type { ExtraIngredient, Pizza, PizzaOnOrder } from './types'; 2 | 3 | import { FC, useState } from 'react'; 4 | 5 | import { useIntl } from 'react-intl'; 6 | import { LabeledCheckbox } from '../components'; 7 | 8 | type Props = { 9 | pizza: Pizza; 10 | getExtraIngredient: (name: string) => ExtraIngredient; 11 | onAddToOrder: (pizza: PizzaOnOrder) => void; 12 | }; 13 | 14 | export const PizzaOnMenu: FC = ({ 15 | pizza, 16 | getExtraIngredient, 17 | onAddToOrder, 18 | }) => { 19 | const [extras, setExtras] = useState([]); 20 | const { formatList } = useIntl(); 21 | 22 | return ( 23 |
24 |
25 |

{pizza.name}

26 |
27 |
Ingredients:
28 |
29 | {formatList( 30 | pizza.ingredients.map((ingredient) => ( 31 | 32 | {ingredient} 33 | 34 | )) 35 | )} 36 |
37 |
38 |
39 |
Extras
40 | {pizza.extras.map((extra) => ( 41 | { 47 | setExtras((extras) => { 48 | if (extras.includes(extra)) { 49 | return extras.filter((x) => x !== extra); 50 | } else { 51 | return [...extras, extra]; 52 | } 53 | }); 54 | }} 55 | > 56 | {extra} 57 | 58 | ))} 59 |
60 | 71 |
72 |
73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /src/07-deriving-prop-types/pizza-shop-data-loader.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import useSWR from 'swr'; 3 | 4 | import type { ExtraIngredients, Pizza } from './types'; 5 | 6 | import { PizzaShop } from './pizza-shop'; 7 | import { extraIngredientsSchema, pizzasSchema } from './schemas'; 8 | 9 | const server = 'http://localhost:3000'; 10 | 11 | export const PizzaShopDataLoader: FC = () => { 12 | const { data: pizzas, error: pizzasError } = useSWR( 13 | '/api/pizzas.json', 14 | (resource, init) => 15 | fetch(`${server}${resource}`, init) 16 | .then((res) => res.json()) 17 | .then(pizzasSchema.parse) 18 | ); 19 | 20 | const { data: extraIngredients, error: extraIngredientsError } = 21 | useSWR( 22 | '/api/good-extra-ingredients.json', 23 | (resource, init) => 24 | fetch(`${server}${resource}`, init) 25 | .then((res) => res.json()) 26 | .then(extraIngredientsSchema.parse) 27 | ); 28 | 29 | if (pizzasError || extraIngredientsError) { 30 | return ( 31 |
32 | Something went wrong 33 | {pizzasError?.message ?? extraIngredientsError?.message} 34 |
35 | ); 36 | } 37 | 38 | if (!pizzas || !extraIngredients) { 39 | return ( 40 |
41 | Loading... 42 |
43 | ); 44 | } 45 | 46 | return ; 47 | }; 48 | -------------------------------------------------------------------------------- /src/07-deriving-prop-types/pizza-shop.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | ExtraIngredient, 3 | ExtraIngredients, 4 | Pizza, 5 | PizzaOnOrder, 6 | } from './types'; 7 | 8 | import { FC, useMemo, useState } from 'react'; 9 | import { useIntl } from 'react-intl'; 10 | import { OrderedPizza } from './ordered-pizza'; 11 | import { PizzaOnMenu } from './pizza-on-menu'; 12 | 13 | type Props = { 14 | extraIngredients: ExtraIngredients; 15 | pizzas: Pizza[]; 16 | }; 17 | 18 | export const PizzaShop: FC = ({ extraIngredients, pizzas }) => { 19 | const { formatNumber } = useIntl(); 20 | const [order, setOrder] = useState([]); 21 | const totalPrice = useMemo(() => calculateTotalPrice(order), [order]); 22 | 23 | const getExtraIngredient = (name: string): ExtraIngredient => 24 | extraIngredients[name]!; 25 | 26 | return ( 27 |
28 |

Pizza Shop

29 |
30 |
31 |

Menu

32 | {pizzas!.map((pizza) => ( 33 | setOrder((order) => [...order, p])} 38 | /> 39 | ))} 40 |
41 |
42 |
43 |

Your Order

44 | {order.map((pizza, index) => ( 45 | 46 | ))} 47 |
48 |
Total amount:
49 |
50 | {formatNumber(totalPrice, { 51 | style: 'currency', 52 | currency: 'EUR', 53 | })} 54 |
55 |
56 |
57 |
58 |
59 |
60 | ); 61 | }; 62 | 63 | function calculateTotalPrice(order: PizzaOnOrder[]) { 64 | return order.reduce( 65 | (sum, pizza) => 66 | sum + 67 | pizza.price + 68 | pizza.extraIngredients.reduce((sum, extra) => sum + extra.price, 0), 69 | 0 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /src/07-deriving-prop-types/schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const pizzaSchema = z.object({ 4 | name: z.string(), 5 | ingredients: z.string().array(), 6 | price: z.number(), 7 | extras: z.string().array(), 8 | }); 9 | 10 | export const pizzasSchema = z.array(pizzaSchema); 11 | 12 | export const extraIngredientSchema = z.object({ 13 | name: z.string(), 14 | price: z.number(), 15 | }); 16 | 17 | export const extraIngredientsSchema = z.record(extraIngredientSchema); 18 | -------------------------------------------------------------------------------- /src/07-deriving-prop-types/types.ts: -------------------------------------------------------------------------------- 1 | export type Pizza = { 2 | name: string; 3 | ingredients: string[]; 4 | price: number; 5 | extras: string[]; 6 | }; 7 | 8 | export type ExtraIngredients = Record; 9 | 10 | export type ExtraIngredient = { 11 | name: string; 12 | price: number; 13 | }; 14 | 15 | export type PizzaOnOrder = { 16 | name: string; 17 | price: number; 18 | extraIngredients: ExtraIngredient[]; 19 | }; 20 | -------------------------------------------------------------------------------- /src/08-inferring-schema-types/ordered-pizza.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import type { PizzaOnOrder } from './types'; 3 | 4 | import { useIntl } from 'react-intl'; 5 | 6 | type Props = { 7 | pizza: PizzaOnOrder; 8 | }; 9 | 10 | export const OrderedPizza: FC = ({ pizza }) => { 11 | const { formatNumber } = useIntl(); 12 | 13 | return ( 14 |
15 |
16 |

17 |
{pizza.name}
18 |
19 | {formatNumber(pizza.price, { 20 | style: 'currency', 21 | currency: 'EUR', 22 | })} 23 |
24 |

25 |
    26 | {pizza.extraIngredients.map((extra, index) => ( 27 |
  1. 28 |
    {extra.name}
    29 |
    30 | {formatNumber(extra.price, { 31 | style: 'currency', 32 | currency: 'EUR', 33 | })} 34 |
    35 |
  2. 36 | ))} 37 |
38 |
39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/08-inferring-schema-types/pizza-on-menu.tsx: -------------------------------------------------------------------------------- 1 | import type { ExtraIngredient, Pizza, PizzaOnOrder } from './types'; 2 | 3 | import { FC, useState } from 'react'; 4 | 5 | import { useIntl } from 'react-intl'; 6 | import { LabeledCheckbox } from '../components'; 7 | 8 | type Props = { 9 | pizza: Pizza; 10 | getExtraIngredient: (name: string) => ExtraIngredient; 11 | onAddToOrder: (pizza: PizzaOnOrder) => void; 12 | }; 13 | 14 | export const PizzaOnMenu: FC = ({ 15 | pizza, 16 | getExtraIngredient, 17 | onAddToOrder, 18 | }) => { 19 | const [extras, setExtras] = useState([]); 20 | const { formatList } = useIntl(); 21 | 22 | return ( 23 |
24 |
25 |

{pizza.name}

26 |
27 |
Ingredients:
28 |
29 | {formatList( 30 | pizza.ingredients.map((ingredient) => ( 31 | 32 | {ingredient} 33 | 34 | )) 35 | )} 36 |
37 |
38 |
39 |
Extras
40 | {pizza.extras.map((extra) => ( 41 | { 45 | setExtras((extras) => { 46 | if (extras.includes(extra)) { 47 | return extras.filter((x) => x !== extra); 48 | } else { 49 | return [...extras, extra]; 50 | } 51 | }); 52 | }} 53 | > 54 | {extra} 55 | 56 | ))} 57 |
58 | 69 |
70 |
71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /src/08-inferring-schema-types/pizza-shop-data-loader.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import useSWR from 'swr'; 3 | 4 | import type { ExtraIngredients, Pizza } from './types'; 5 | 6 | import { PizzaShop } from './pizza-shop'; 7 | import { extraIngredientsSchema, pizzasSchema } from './schemas'; 8 | 9 | const server = 'http://localhost:3000'; 10 | 11 | export const PizzaShopDataLoader: FC = () => { 12 | const { data: pizzas, error: pizzasError } = useSWR( 13 | '/api/pizzas.json', 14 | (resource, init) => 15 | fetch(`${server}${resource}`, init) 16 | .then((res) => res.json()) 17 | .then(pizzasSchema.parse) 18 | ); 19 | 20 | const { data: extraIngredients, error: extraIngredientsError } = 21 | useSWR( 22 | '/api/good-extra-ingredients.json', 23 | (resource, init) => 24 | fetch(`${server}${resource}`, init) 25 | .then((res) => res.json()) 26 | .then(extraIngredientsSchema.parse) 27 | ); 28 | 29 | if (pizzasError || extraIngredientsError) { 30 | return ( 31 |
32 | Something went wrong 33 | {pizzasError?.message ?? extraIngredientsError?.message} 34 |
35 | ); 36 | } 37 | 38 | if (!pizzas || !extraIngredients) { 39 | return ( 40 |
41 | Loading... 42 |
43 | ); 44 | } 45 | 46 | return ; 47 | }; 48 | -------------------------------------------------------------------------------- /src/08-inferring-schema-types/pizza-shop.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | ExtraIngredient, 3 | ExtraIngredients, 4 | Pizza, 5 | PizzaOnOrder, 6 | } from './types'; 7 | 8 | import { FC, useMemo, useState } from 'react'; 9 | import { useIntl } from 'react-intl'; 10 | import { OrderedPizza } from './ordered-pizza'; 11 | import { PizzaOnMenu } from './pizza-on-menu'; 12 | 13 | type Props = { 14 | extraIngredients: ExtraIngredients; 15 | pizzas: Pizza[]; 16 | }; 17 | 18 | export const PizzaShop: FC = ({ extraIngredients, pizzas }) => { 19 | const { formatNumber } = useIntl(); 20 | const [order, setOrder] = useState([]); 21 | const totalPrice = useMemo(() => calculateTotalPrice(order), [order]); 22 | 23 | const getExtraIngredient = (name: string): ExtraIngredient => 24 | extraIngredients[name]!; 25 | 26 | return ( 27 |
28 |

Pizza Shop

29 |
30 |
31 |

Menu

32 | {pizzas!.map((pizza) => ( 33 | setOrder((order) => [...order, p])} 38 | /> 39 | ))} 40 |
41 |
42 |
43 |

Your Order

44 | {order.map((pizza, index) => ( 45 | 46 | ))} 47 |
48 |
Total amount:
49 |
50 | {formatNumber(totalPrice, { 51 | style: 'currency', 52 | currency: 'EUR', 53 | })} 54 |
55 |
56 |
57 |
58 |
59 |
60 | ); 61 | }; 62 | 63 | function calculateTotalPrice(order: PizzaOnOrder[]) { 64 | return order.reduce( 65 | (sum, pizza) => 66 | sum + 67 | pizza.price + 68 | pizza.extraIngredients.reduce((sum, extra) => sum + extra.price, 0), 69 | 0 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /src/08-inferring-schema-types/schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const pizzaSchema = z.object({ 4 | name: z.string(), 5 | ingredients: z.string().array(), 6 | price: z.number(), 7 | extras: z.string().array(), 8 | }); 9 | 10 | export const pizzasSchema = z.array(pizzaSchema); 11 | 12 | export const extraIngredientSchema = z.object({ 13 | name: z.string(), 14 | price: z.number(), 15 | }); 16 | 17 | export const extraIngredientsSchema = z.record(extraIngredientSchema); 18 | -------------------------------------------------------------------------------- /src/08-inferring-schema-types/types.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { 3 | extraIngredientSchema, 4 | extraIngredientsSchema, 5 | pizzaSchema, 6 | } from './schemas'; 7 | 8 | export type Pizza = z.infer; 9 | 10 | export type ExtraIngredients = z.infer; 11 | 12 | export type ExtraIngredient = z.infer; 13 | 14 | export type PizzaOnOrder = { 15 | name: string; 16 | price: number; 17 | extraIngredients: ExtraIngredient[]; 18 | }; 19 | -------------------------------------------------------------------------------- /src/09-type-mapping/ordered-pizza.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import type { PizzaOnOrder } from './types'; 3 | 4 | import { useIntl } from 'react-intl'; 5 | 6 | type Props = { 7 | pizza: PizzaOnOrder; 8 | }; 9 | 10 | export const OrderedPizza: FC = ({ pizza }) => { 11 | const { formatNumber } = useIntl(); 12 | 13 | return ( 14 |
15 |
16 |

17 |
{pizza.name}
18 |
19 | {formatNumber(pizza.price, { 20 | style: 'currency', 21 | currency: 'EUR', 22 | })} 23 |
24 |

25 |
    26 | {pizza.extraIngredients.map((extra, index) => ( 27 |
  1. 28 |
    {extra.name}
    29 |
    30 | {formatNumber(extra.price, { 31 | style: 'currency', 32 | currency: 'EUR', 33 | })} 34 |
    35 |
  2. 36 | ))} 37 |
38 |
39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/09-type-mapping/pizza-on-menu.tsx: -------------------------------------------------------------------------------- 1 | import type { ExtraIngredient, Pizza, PizzaOnOrder } from './types'; 2 | 3 | import { FC, useState } from 'react'; 4 | 5 | import { useIntl } from 'react-intl'; 6 | import { LabeledCheckbox } from '../components'; 7 | 8 | type Props = { 9 | pizza: Pizza; 10 | getExtraIngredient: (name: string) => ExtraIngredient; 11 | onAddToOrder: (pizza: PizzaOnOrder) => void; 12 | }; 13 | 14 | export const PizzaOnMenu: FC = ({ 15 | pizza, 16 | getExtraIngredient, 17 | onAddToOrder, 18 | }) => { 19 | const [extras, setExtras] = useState([]); 20 | const { formatList } = useIntl(); 21 | 22 | return ( 23 |
24 |
25 |

{pizza.name}

26 |
27 |
Ingredients:
28 |
29 | {formatList( 30 | pizza.ingredients.map((ingredient) => ( 31 | 32 | {ingredient} 33 | 34 | )) 35 | )} 36 |
37 |
38 |
39 |
Extras
40 | {pizza.extras.map((extra) => ( 41 | { 45 | setExtras((extras) => { 46 | if (extras.includes(extra)) { 47 | return extras.filter((x) => x !== extra); 48 | } else { 49 | return [...extras, extra]; 50 | } 51 | }); 52 | }} 53 | > 54 | {extra} 55 | 56 | ))} 57 |
58 | 69 |
70 |
71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /src/09-type-mapping/pizza-shop-data-loader.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import useSWR from 'swr'; 3 | 4 | import type { ExtraIngredients, Pizza } from './types'; 5 | 6 | import { PizzaShop } from './pizza-shop'; 7 | import { extraIngredientsSchema, pizzasSchema } from './schemas'; 8 | 9 | const server = 'http://localhost:3000'; 10 | 11 | export const PizzaShopDataLoader: FC = () => { 12 | const { data: pizzas, error: pizzasError } = useSWR( 13 | '/api/pizzas.json', 14 | (resource, init) => 15 | fetch(`${server}${resource}`, init) 16 | .then((res) => res.json()) 17 | .then(pizzasSchema.parse) 18 | ); 19 | 20 | const { data: extraIngredients, error: extraIngredientsError } = 21 | useSWR( 22 | '/api/good-extra-ingredients.json', 23 | (resource, init) => 24 | fetch(`${server}${resource}`, init) 25 | .then((res) => res.json()) 26 | .then(extraIngredientsSchema.parse) 27 | ); 28 | 29 | if (pizzasError || extraIngredientsError) { 30 | return ( 31 |
32 | Something went wrong 33 | {pizzasError?.message ?? extraIngredientsError?.message} 34 |
35 | ); 36 | } 37 | 38 | if (!pizzas || !extraIngredients) { 39 | return ( 40 |
41 | Loading... 42 |
43 | ); 44 | } 45 | 46 | return ; 47 | }; 48 | -------------------------------------------------------------------------------- /src/09-type-mapping/pizza-shop.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | ExtraIngredient, 3 | ExtraIngredients, 4 | Pizza, 5 | PizzaOnOrder, 6 | } from './types'; 7 | 8 | import { FC, useMemo, useState } from 'react'; 9 | import { useIntl } from 'react-intl'; 10 | import { OrderedPizza } from './ordered-pizza'; 11 | import { PizzaOnMenu } from './pizza-on-menu'; 12 | 13 | type Props = { 14 | extraIngredients: ExtraIngredients; 15 | pizzas: Pizza[]; 16 | }; 17 | 18 | export const PizzaShop: FC = ({ extraIngredients, pizzas }) => { 19 | const { formatNumber } = useIntl(); 20 | const [order, setOrder] = useState([]); 21 | const totalPrice = useMemo(() => calculateTotalPrice(order), [order]); 22 | 23 | const getExtraIngredient = (name: string): ExtraIngredient => 24 | extraIngredients[name]!; 25 | 26 | return ( 27 |
28 |

Pizza Shop

29 |
30 |
31 |

Menu

32 | {pizzas!.map((pizza) => ( 33 | setOrder((order) => [...order, p])} 38 | /> 39 | ))} 40 |
41 |
42 |
43 |

Your Order

44 | {order.map((pizza, index) => ( 45 | 46 | ))} 47 |
48 |
Total amount:
49 |
50 | {formatNumber(totalPrice, { 51 | style: 'currency', 52 | currency: 'EUR', 53 | })} 54 |
55 |
56 |
57 |
58 |
59 |
60 | ); 61 | }; 62 | 63 | function calculateTotalPrice(order: PizzaOnOrder[]) { 64 | return order.reduce( 65 | (sum, pizza) => 66 | sum + 67 | pizza.price + 68 | pizza.extraIngredients.reduce((sum, extra) => sum + extra.price, 0), 69 | 0 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /src/09-type-mapping/schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const pizzaSchema = z.object({ 4 | name: z.string(), 5 | ingredients: z.string().array(), 6 | price: z.number(), 7 | extras: z.string().array(), 8 | }); 9 | 10 | export const pizzasSchema = z.array(pizzaSchema); 11 | 12 | export const extraIngredientSchema = z.object({ 13 | name: z.string(), 14 | price: z.number(), 15 | }); 16 | 17 | export const extraIngredientsSchema = z.record(extraIngredientSchema); 18 | -------------------------------------------------------------------------------- /src/09-type-mapping/types.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { 3 | extraIngredientSchema, 4 | extraIngredientsSchema, 5 | pizzaSchema, 6 | } from './schemas'; 7 | 8 | export type Pizza = z.infer; 9 | export type ExtraIngredients = z.infer; 10 | export type ExtraIngredient = z.infer; 11 | 12 | export type PizzaOnOrder = Pick & { 13 | extraIngredients: ExtraIngredient[]; 14 | }; 15 | -------------------------------------------------------------------------------- /src/10-using-readonly/menu.ts: -------------------------------------------------------------------------------- 1 | import { ExtraIngredient, ExtraIngredients, Pizza } from './types'; 2 | 3 | export const pizzas: Pizza[] = [ 4 | { 5 | name: 'Pizza Margherita', 6 | ingredients: ['tomato sauce', 'mozzarella', 'basil'], 7 | price: 7.95, 8 | extras: ['olives'], 9 | }, 10 | { 11 | name: 'Pizza Quattro formaggi', 12 | ingredients: ['mozzarella', 'gorgonzola', 'provolone', 'parmesan cheese'], 13 | price: 9.45, 14 | extras: ['cheese'], 15 | }, 16 | { 17 | name: 'Pizza Rustica', 18 | ingredients: [ 19 | 'tomato sauce', 20 | 'mozzarella', 21 | 'seasoned minced beef', 22 | 'red peppers', 23 | ], 24 | price: 9.95, 25 | extras: ['cheese', 'pepperoni', 'peppers'], 26 | }, 27 | { 28 | name: 'Pizza Ai funghi', 29 | ingredients: ['tomato sauce', 'mozzarella', 'mushrooms', 'garlic'], 30 | price: 10.95, 31 | extras: ['cheese', 'mushrooms'], 32 | }, 33 | { 34 | name: 'Pizza Hawaii', 35 | ingredients: ['tomato sauce', 'mozzarella', 'prosciutto', 'pineapple'], 36 | price: 11.75, 37 | extras: ['cheese', 'pepperoni', 'tomatoes'], 38 | }, 39 | ]; 40 | 41 | export const extraIngredients: ExtraIngredients = { 42 | cheese: { name: 'Cheese', price: 0.5 }, 43 | peppers: { name: 'Green and red bell pepper', price: 0.5 }, 44 | pepperoni: { name: 'Pepperoni', price: 0.75 }, 45 | tomatoes: { name: 'Tomatoes', price: 0.5 }, 46 | olives: { name: 'Olives', price: 0.6 }, 47 | mushrooms: { name: 'Mushrooms', price: 0.6 }, 48 | }; 49 | 50 | export const getExtraIngredient = (name: string): ExtraIngredient => { 51 | const extraIngredient = extraIngredients[name]; 52 | 53 | if (!extraIngredient) { 54 | throw new Error(`Extra ingredient ${name} does not exist`); 55 | } 56 | 57 | return extraIngredient; 58 | }; 59 | -------------------------------------------------------------------------------- /src/10-using-readonly/ordered-pizza.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import type { PizzaOnOrder } from './types'; 3 | 4 | import { useIntl } from 'react-intl'; 5 | 6 | type Props = { 7 | pizza: PizzaOnOrder; 8 | }; 9 | 10 | export const OrderedPizza: FC = ({ pizza }) => { 11 | const { formatNumber } = useIntl(); 12 | 13 | return ( 14 |
15 |
16 |

17 |
{pizza.name}
18 |
19 | {formatNumber(pizza.price, { 20 | style: 'currency', 21 | currency: 'EUR', 22 | })} 23 |
24 |

25 |
    26 | {pizza.extraIngredients.map((extra, index) => ( 27 |
  1. 28 |
    {extra.name}
    29 |
    30 | {formatNumber(extra.price, { 31 | style: 'currency', 32 | currency: 'EUR', 33 | })} 34 |
    35 |
  2. 36 | ))} 37 |
38 |
39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/10-using-readonly/pizza-on-menu.tsx: -------------------------------------------------------------------------------- 1 | import type { Pizza, PizzaOnOrder } from './types'; 2 | 3 | import { FC, useState } from 'react'; 4 | 5 | import { useIntl } from 'react-intl'; 6 | import { LabeledCheckbox } from '../components'; 7 | import { getExtraIngredient } from './menu'; 8 | 9 | type Props = { 10 | pizza: Pizza; 11 | onAddToOrder: (pizza: PizzaOnOrder) => void; 12 | }; 13 | 14 | export const PizzaOnMenu: FC = ({ pizza, onAddToOrder }) => { 15 | const [extras, setExtras] = useState([]); 16 | const { formatList } = useIntl(); 17 | 18 | return ( 19 |
20 |
21 |

{pizza.name}

22 |
23 |
Ingredients
24 |
25 | {formatList( 26 | pizza.ingredients.map((ingredient) => ( 27 | 28 | {ingredient} 29 | 30 | )) 31 | )} 32 |
33 |
34 |
35 |
Extras
36 | {pizza.extras.map((extra) => ( 37 | { 41 | setExtras((extras) => { 42 | if (extras.includes(extra)) { 43 | return extras.filter((x) => x !== extra); 44 | } else { 45 | return [...extras, extra]; 46 | } 47 | }); 48 | }} 49 | > 50 | {extra} 51 | 52 | ))} 53 |
54 | 65 |
66 |
67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /src/10-using-readonly/pizza-shop.tsx: -------------------------------------------------------------------------------- 1 | import type { PizzaOnOrder } from './types'; 2 | 3 | import { FC, useMemo, useState } from 'react'; 4 | import { useIntl } from 'react-intl'; 5 | import { pizzas } from './menu'; 6 | import { OrderedPizza } from './ordered-pizza'; 7 | import { PizzaOnMenu } from './pizza-on-menu'; 8 | 9 | export const PizzaShop: FC = () => { 10 | const { formatNumber } = useIntl(); 11 | const [order, setOrder] = useState([]); 12 | const totalPrice = useMemo(() => calculateTotalPrice(order), [order]); 13 | 14 | const onPlaceOrder = () => { 15 | const extrasForAEuro = order.flatMap((pizza) => 16 | pizza.extraIngredients.filter((extra) => extra.price === 1) 17 | ); 18 | console.log('Extras for a Euro', extrasForAEuro); 19 | 20 | pizzas.push({ 21 | name: `New Pizza ${new Date().toLocaleTimeString()}`, 22 | price: 10, 23 | extras: ['cheese'], 24 | ingredients: ['tomato sauce'], 25 | }); 26 | 27 | if (pizzas[0]) { 28 | pizzas[0].price *= 10; 29 | } 30 | 31 | setOrder([]); 32 | }; 33 | 34 | return ( 35 |
36 |

Pizza Shop

37 |
38 |
39 |

Menu

40 | {pizzas.map((pizza) => ( 41 | setOrder((order) => [...order, p])} 45 | /> 46 | ))} 47 |
48 |
49 |
50 |

Your Order

51 | {order.map((pizza, index) => ( 52 | 53 | ))} 54 |
55 |
Total amount:
56 |
57 | {formatNumber(totalPrice, { 58 | style: 'currency', 59 | currency: 'EUR', 60 | })} 61 |
62 |
63 |
64 | 71 |
72 |
73 |
74 |
75 |
76 | ); 77 | }; 78 | 79 | function calculateTotalPrice(order: PizzaOnOrder[]) { 80 | return order.reduce( 81 | (sum, pizza) => 82 | sum + 83 | pizza.price + 84 | pizza.extraIngredients.reduce((sum, extra) => sum + extra.price, 0), 85 | 0 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /src/10-using-readonly/types.ts: -------------------------------------------------------------------------------- 1 | export type Pizza = { 2 | name: string; 3 | ingredients: string[]; 4 | price: number; 5 | extras: string[]; 6 | }; 7 | 8 | export type ExtraIngredients = Record; 9 | 10 | export type ExtraIngredient = Readonly<{ 11 | name: string; 12 | price: number; 13 | }>; 14 | 15 | export type PizzaOnOrder = { 16 | name: string; 17 | price: number; 18 | extraIngredients: ExtraIngredient[]; 19 | }; 20 | -------------------------------------------------------------------------------- /src/11-using-deep-readonly/menu.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DeepReadonly, 3 | ExtraIngredient, 4 | ExtraIngredients, 5 | Pizza, 6 | } from './types'; 7 | 8 | export const pizzas: DeepReadonly = [ 9 | { 10 | name: 'Pizza Margherita', 11 | ingredients: ['tomato sauce', 'mozzarella', 'basil'], 12 | price: 7.95, 13 | extras: ['olives'], 14 | }, 15 | { 16 | name: 'Pizza Quattro formaggi', 17 | ingredients: ['mozzarella', 'gorgonzola', 'provolone', 'parmesan cheese'], 18 | price: 9.45, 19 | extras: ['cheese'], 20 | }, 21 | { 22 | name: 'Pizza Rustica', 23 | ingredients: [ 24 | 'tomato sauce', 25 | 'mozzarella', 26 | 'seasoned minced beef', 27 | 'red peppers', 28 | ], 29 | price: 9.95, 30 | extras: ['cheese', 'pepperoni', 'peppers'], 31 | }, 32 | { 33 | name: 'Pizza Ai funghi', 34 | ingredients: ['tomato sauce', 'mozzarella', 'mushrooms', 'garlic'], 35 | price: 10.95, 36 | extras: ['cheese', 'mushrooms'], 37 | }, 38 | { 39 | name: 'Pizza Hawaii', 40 | ingredients: ['tomato sauce', 'mozzarella', 'prosciutto', 'pineapple'], 41 | price: 11.75, 42 | extras: ['cheese', 'pepperoni', 'tomatoes'], 43 | }, 44 | ]; 45 | 46 | export const extraIngredients: ExtraIngredients = { 47 | cheese: { name: 'Cheese', price: 0.5 }, 48 | peppers: { name: 'Green and red bell pepper', price: 0.5 }, 49 | pepperoni: { name: 'Pepperoni', price: 0.75 }, 50 | tomatoes: { name: 'Tomatoes', price: 0.5 }, 51 | olives: { name: 'Olives', price: 0.6 }, 52 | mushrooms: { name: 'Mushrooms', price: 0.6 }, 53 | }; 54 | 55 | export const getExtraIngredient = (name: string): ExtraIngredient => { 56 | const extraIngredient = extraIngredients[name]; 57 | 58 | if (!extraIngredient) { 59 | throw new Error(`Extra ingredient ${name} does not exist`); 60 | } 61 | 62 | return extraIngredient; 63 | }; 64 | -------------------------------------------------------------------------------- /src/11-using-deep-readonly/ordered-pizza.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import type { DeepReadonly, PizzaOnOrder } from './types'; 3 | 4 | import { useIntl } from 'react-intl'; 5 | 6 | type Props = { 7 | pizza: DeepReadonly; 8 | }; 9 | 10 | export const OrderedPizza: FC = ({ pizza }) => { 11 | const { formatNumber } = useIntl(); 12 | 13 | return ( 14 |
15 |
16 |

17 |
{pizza.name}
18 |
19 | {formatNumber(pizza.price, { 20 | style: 'currency', 21 | currency: 'EUR', 22 | })} 23 |
24 |

25 |
    26 | {pizza.extraIngredients.map((extra, index) => ( 27 |
  1. 28 |
    {extra.name}
    29 |
    30 | {formatNumber(extra.price, { 31 | style: 'currency', 32 | currency: 'EUR', 33 | })} 34 |
    35 |
  2. 36 | ))} 37 |
38 |
39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/11-using-deep-readonly/pizza-on-menu.tsx: -------------------------------------------------------------------------------- 1 | import type { DeepReadonly, Pizza, PizzaOnOrder } from './types'; 2 | 3 | import { FC, useState } from 'react'; 4 | 5 | import { useIntl } from 'react-intl'; 6 | import { LabeledCheckbox } from '../components'; 7 | import { getExtraIngredient } from './menu'; 8 | 9 | type Props = { 10 | pizza: DeepReadonly; 11 | onAddToOrder: (pizza: PizzaOnOrder) => void; 12 | }; 13 | 14 | export const PizzaOnMenu: FC = ({ pizza, onAddToOrder }) => { 15 | const [extras, setExtras] = useState([]); 16 | const { formatList } = useIntl(); 17 | 18 | return ( 19 |
20 |
21 |

{pizza.name}

22 |
23 |
Ingredients
24 |
25 | {formatList( 26 | pizza.ingredients.map((ingredient) => ( 27 | 28 | {ingredient} 29 | 30 | )) 31 | )} 32 |
33 |
34 |
35 |
Extras
36 | {pizza.extras.map((extra) => ( 37 | { 41 | setExtras((extras) => { 42 | if (extras.includes(extra)) { 43 | return extras.filter((x) => x !== extra); 44 | } else { 45 | return [...extras, extra]; 46 | } 47 | }); 48 | }} 49 | > 50 | {extra} 51 | 52 | ))} 53 |
54 | 65 |
66 |
67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /src/11-using-deep-readonly/pizza-shop.tsx: -------------------------------------------------------------------------------- 1 | import type { DeepReadonly, PizzaOnOrder } from './types'; 2 | 3 | import { FC, useMemo, useState } from 'react'; 4 | import { useIntl } from 'react-intl'; 5 | import { pizzas } from './menu'; 6 | import { OrderedPizza } from './ordered-pizza'; 7 | import { PizzaOnMenu } from './pizza-on-menu'; 8 | 9 | export const PizzaShop: FC = () => { 10 | const { formatNumber } = useIntl(); 11 | const [order, setOrder] = useState>([]); 12 | const totalPrice = useMemo(() => calculateTotalPrice(order), [order]); 13 | 14 | const onPlaceOrder = () => { 15 | const extrasForAEuro = order.flatMap((pizza) => 16 | pizza.extraIngredients.filter((extra) => extra.price === 1) 17 | ); 18 | console.log('Extras for a Euro', extrasForAEuro); 19 | 20 | // pizzas.push({ 21 | // name: `New Pizza ${new Date().toLocaleTimeString()}`, 22 | // price: 10, 23 | // extras: ['cheese'], 24 | // ingredients: ['tomato sauce'], 25 | // }); 26 | 27 | // if (pizzas[0]) { 28 | // pizzas[0].price *= 10; 29 | // } 30 | 31 | setOrder([]); 32 | }; 33 | 34 | return ( 35 |
36 |

Pizza Shop

37 |
38 |
39 |

Menu

40 | {pizzas.map((pizza) => ( 41 | setOrder((order) => [...order, p])} 45 | /> 46 | ))} 47 |
48 |
49 |
50 |

Your Order

51 | {order.map((pizza, index) => ( 52 | 53 | ))} 54 |
55 |
Total amount:
56 |
57 | {formatNumber(totalPrice, { 58 | style: 'currency', 59 | currency: 'EUR', 60 | })} 61 |
62 |
63 |
64 | 71 |
72 |
73 |
74 |
75 |
76 | ); 77 | }; 78 | 79 | function calculateTotalPrice(order: DeepReadonly) { 80 | return order.reduce( 81 | (sum, pizza) => 82 | sum + 83 | pizza.price + 84 | pizza.extraIngredients.reduce((sum, extra) => sum + extra.price, 0), 85 | 0 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /src/11-using-deep-readonly/types.ts: -------------------------------------------------------------------------------- 1 | export type DeepReadonly = { 2 | readonly [P in keyof T]: DeepReadonly; 3 | }; 4 | 5 | export type Pizza = { 6 | name: string; 7 | ingredients: string[]; 8 | price: number; 9 | extras: string[]; 10 | }; 11 | 12 | export type ExtraIngredients = Record; 13 | 14 | export type ExtraIngredient = { 15 | name: string; 16 | price: number; 17 | }; 18 | 19 | export type PizzaOnOrder = { 20 | name: string; 21 | price: number; 22 | extraIngredients: ExtraIngredient[]; 23 | }; 24 | -------------------------------------------------------------------------------- /src/12-displaying-types/menu.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DeepReadonly, 3 | ExtraIngredient, 4 | ExtraIngredients, 5 | Pizza, 6 | } from './types'; 7 | 8 | export const pizzas: DeepReadonly = [ 9 | { 10 | name: 'Pizza Margherita', 11 | ingredients: ['tomato sauce', 'mozzarella', 'basil'], 12 | price: 7.95, 13 | extras: ['olives'], 14 | }, 15 | { 16 | name: 'Pizza Quattro formaggi', 17 | ingredients: ['mozzarella', 'gorgonzola', 'provolone', 'parmesan cheese'], 18 | price: 9.45, 19 | extras: ['cheese'], 20 | }, 21 | { 22 | name: 'Pizza Rustica', 23 | ingredients: [ 24 | 'tomato sauce', 25 | 'mozzarella', 26 | 'seasoned minced beef', 27 | 'red peppers', 28 | ], 29 | price: 9.95, 30 | extras: ['cheese', 'pepperoni', 'peppers'], 31 | }, 32 | { 33 | name: 'Pizza Ai funghi', 34 | ingredients: ['tomato sauce', 'mozzarella', 'mushrooms', 'garlic'], 35 | price: 10.95, 36 | extras: ['cheese', 'mushrooms'], 37 | }, 38 | { 39 | name: 'Pizza Hawaii', 40 | ingredients: ['tomato sauce', 'mozzarella', 'prosciutto', 'pineapple'], 41 | price: 11.75, 42 | extras: ['cheese', 'pepperoni', 'tomatoes'], 43 | }, 44 | ]; 45 | 46 | export const extraIngredients: ExtraIngredients = { 47 | cheese: { name: 'Cheese', price: 0.5 }, 48 | peppers: { name: 'Green and red bell pepper', price: 0.5 }, 49 | pepperoni: { name: 'Pepperoni', price: 0.75 }, 50 | tomatoes: { name: 'Tomatoes', price: 0.5 }, 51 | olives: { name: 'Olives', price: 0.6 }, 52 | mushrooms: { name: 'Mushrooms', price: 0.6 }, 53 | }; 54 | 55 | export const getExtraIngredient = (name: string): ExtraIngredient => { 56 | const extraIngredient = extraIngredients[name]; 57 | 58 | if (!extraIngredient) { 59 | throw new Error(`Extra ingredient ${name} does not exist`); 60 | } 61 | 62 | return extraIngredient; 63 | }; 64 | -------------------------------------------------------------------------------- /src/12-displaying-types/ordered-pizza.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import type { DeepReadonly, PizzaOnOrder } from './types'; 3 | 4 | import { useIntl } from 'react-intl'; 5 | 6 | type Props = { 7 | pizza: DeepReadonly; 8 | }; 9 | 10 | export const OrderedPizza: FC = ({ pizza }) => { 11 | const { formatNumber } = useIntl(); 12 | 13 | return ( 14 |
15 |
16 |

17 |
{pizza.name}
18 |
19 | {formatNumber(pizza.price, { 20 | style: 'currency', 21 | currency: 'EUR', 22 | })} 23 |
24 |

25 |
    26 | {pizza.extraIngredients.map((extra, index) => ( 27 |
  1. 28 |
    {extra.name}
    29 |
    30 | {formatNumber(extra.price, { 31 | style: 'currency', 32 | currency: 'EUR', 33 | })} 34 |
    35 |
  2. 36 | ))} 37 |
38 |
39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/12-displaying-types/pizza-on-menu.tsx: -------------------------------------------------------------------------------- 1 | import type { DeepReadonly, Pizza, PizzaOnOrder } from './types'; 2 | 3 | import { FC, useState } from 'react'; 4 | 5 | import { useIntl } from 'react-intl'; 6 | import { LabeledCheckbox } from '../components'; 7 | import { getExtraIngredient } from './menu'; 8 | 9 | type Props = { 10 | pizza: DeepReadonly; 11 | onAddToOrder: (pizza: PizzaOnOrder) => void; 12 | }; 13 | 14 | export const PizzaOnMenu: FC = ({ pizza, onAddToOrder }) => { 15 | const [extras, setExtras] = useState([]); 16 | const { formatList } = useIntl(); 17 | 18 | return ( 19 |
20 |
21 |

{pizza.name}

22 |
23 |
Ingredients
24 |
25 | {formatList( 26 | pizza.ingredients.map((ingredient) => ( 27 | 28 | {ingredient} 29 | 30 | )) 31 | )} 32 |
33 |
34 |
35 |
Extras
36 | {pizza.extras.map((extra) => ( 37 | { 41 | setExtras((extras) => { 42 | if (extras.includes(extra)) { 43 | return extras.filter((x) => x !== extra); 44 | } else { 45 | return [...extras, extra]; 46 | } 47 | }); 48 | }} 49 | > 50 | {extra} 51 | 52 | ))} 53 |
54 | 65 |
66 |
67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /src/12-displaying-types/pizza-shop.tsx: -------------------------------------------------------------------------------- 1 | import type { DeepReadonly, PizzaOnOrder } from './types'; 2 | 3 | import { FC, useMemo, useState } from 'react'; 4 | import { useIntl } from 'react-intl'; 5 | import { pizzas } from './menu'; 6 | import { OrderedPizza } from './ordered-pizza'; 7 | import { PizzaOnMenu } from './pizza-on-menu'; 8 | 9 | export const PizzaShop: FC = () => { 10 | const { formatNumber } = useIntl(); 11 | const [order, setOrder] = useState>([]); 12 | const totalPrice = useMemo(() => calculateTotalPrice(order), [order]); 13 | 14 | const onPlaceOrder = () => { 15 | const extrasForAEuro = order.flatMap((pizza) => 16 | pizza.extraIngredients.filter((extra) => extra.price === 1) 17 | ); 18 | console.log('Extras for a Euro', extrasForAEuro); 19 | 20 | // pizzas.push({ 21 | // name: `New Pizza ${new Date().toLocaleTimeString()}`, 22 | // price: 10, 23 | // extras: ['cheese'], 24 | // ingredients: ['tomato sauce'], 25 | // }); 26 | 27 | // if (pizzas[0]) { 28 | // pizzas[0].price *= 10; 29 | // } 30 | 31 | setOrder([]); 32 | }; 33 | 34 | return ( 35 |
36 |

Pizza Shop

37 |
38 |
39 |

Menu

40 | {pizzas.map((pizza) => ( 41 | setOrder((order) => [...order, p])} 45 | /> 46 | ))} 47 |
48 |
49 |
50 |

Your Order

51 | {order.map((pizza, index) => ( 52 | 53 | ))} 54 |
55 |
Total amount:
56 |
57 | {formatNumber(totalPrice, { 58 | style: 'currency', 59 | currency: 'EUR', 60 | })} 61 |
62 |
63 |
64 | 71 |
72 |
73 |
74 |
75 |
76 | ); 77 | }; 78 | 79 | function calculateTotalPrice(order: DeepReadonly) { 80 | return order.reduce( 81 | (sum, pizza) => 82 | sum + 83 | pizza.price + 84 | pizza.extraIngredients.reduce((sum, extra) => sum + extra.price, 0), 85 | 0 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /src/12-displaying-types/types.ts: -------------------------------------------------------------------------------- 1 | export type DeepReadonly = { 2 | readonly [P in keyof T]: DeepReadonly; 3 | }; 4 | 5 | export type Pizza = Readonly<{ 6 | name: string; 7 | ingredients: string[]; 8 | price: number; 9 | extras: string[]; 10 | }>; 11 | 12 | export type ExtraIngredients = Record; 13 | 14 | export type ExtraIngredient = Readonly<{ 15 | name: string; 16 | price: number; 17 | }>; 18 | 19 | // Taken from https://effectivetypescript.com/2022/02/25/gentips-4-display/ 20 | type Resolve = T extends Function ? T : { [K in keyof T]: T[K] }; 21 | 22 | export type PizzaOnOrder = Resolve< 23 | Pick & { 24 | extraIngredients: ExtraIngredient[]; 25 | } 26 | >; 27 | -------------------------------------------------------------------------------- /src/13-predicates-assertions/book-on-sale.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import type { FC } from 'react'; 3 | import bookImage from './images/book.png'; 4 | import { Book } from './types'; 5 | 6 | type Props = { 7 | book: Book; 8 | }; 9 | 10 | export const BookOnSale: FC = ({ book }) => { 11 | return ( 12 |
13 | {book.title} 14 |
15 |

{book.title}

16 |

{book.description}

17 |
18 | 19 |
20 |
21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/13-predicates-assertions/book-shop.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import { ItemOnSale } from './item-on-sale'; 3 | import { ItemsOnSale } from './types'; 4 | 5 | const itemsOnSale: ItemsOnSale[] = [ 6 | { 7 | type: 'book', 8 | title: 'The Hobbit', 9 | description: 10 | 'The Hobbit is a wondrous tale of adventure and heroism set in the fantasy realm of middle-earth. Bilbo Baggins, an unambitious Hobbit is unwillingly recruited as a burglar by a party of dwarves and sent on a most extraordinary adventure. Coming head to head against trolls, goblins, wolves and the mighty dragon Smaug, Bilbo faces his worst fears, makes some unlikely allies, travels further than ever before and is changed, forever.', 11 | }, 12 | { type: 'magazine', title: 'BYTE Magazine' }, 13 | { type: 'pen', color: 'blue' }, 14 | ]; 15 | 16 | export const BookShop: FC = () => ( 17 |
18 |

There are {itemsOnSale.length} items on sale

19 | {itemsOnSale.map((item, index) => ( 20 |
21 | 22 |
23 | ))} 24 |
25 | ); 26 | -------------------------------------------------------------------------------- /src/13-predicates-assertions/images/book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mauricedb/advanced-react-typescript-2022/fe535deb86057ff751a0b38b3eb2f09f7ab0feda/src/13-predicates-assertions/images/book.png -------------------------------------------------------------------------------- /src/13-predicates-assertions/images/magazine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mauricedb/advanced-react-typescript-2022/fe535deb86057ff751a0b38b3eb2f09f7ab0feda/src/13-predicates-assertions/images/magazine.png -------------------------------------------------------------------------------- /src/13-predicates-assertions/images/pen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mauricedb/advanced-react-typescript-2022/fe535deb86057ff751a0b38b3eb2f09f7ab0feda/src/13-predicates-assertions/images/pen.png -------------------------------------------------------------------------------- /src/13-predicates-assertions/item-on-sale.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import { BookOnSale } from './book-on-sale'; 3 | import { MagazineOnSale } from './magazine-on-sale'; 4 | import { PenOnSale } from './pen-on-sale'; 5 | import { assertNever, ItemsOnSale } from './types'; 6 | 7 | type Props = { 8 | item: ItemsOnSale; 9 | }; 10 | 11 | export const ItemOnSale: FC = ({ item }) => { 12 | switch (item.type) { 13 | case 'book': 14 | return ; 15 | case 'magazine': 16 | return ; 17 | case 'pen': 18 | return ; 19 | default: 20 | assertNever(item); 21 | } 22 | 23 | return null; 24 | }; 25 | -------------------------------------------------------------------------------- /src/13-predicates-assertions/magazine-on-sale.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import type { FC } from 'react'; 3 | import magazineImage from './images/magazine.png'; 4 | import { Magazine } from './types'; 5 | 6 | type Props = { 7 | magazine: Magazine; 8 | }; 9 | 10 | export const MagazineOnSale: FC = ({ magazine }) => { 11 | return ( 12 |
13 | {magazine.title} 18 |
19 |

{magazine.title}

20 | 21 |
22 | 23 |
24 |
25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/13-predicates-assertions/pen-on-sale.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import type { FC } from 'react'; 3 | import penImage from './images/pen.png'; 4 | import { Pen } from './types'; 5 | 6 | type Props = { 7 | pen: Pen; 8 | }; 9 | 10 | export const PenOnSale: FC = ({ pen }) => { 11 | return ( 12 |
13 | {`A 18 |
19 |

{`A ${pen.color} pen`}

20 | 21 |
22 | 23 |
24 |
25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/13-predicates-assertions/types.ts: -------------------------------------------------------------------------------- 1 | export type Book = { 2 | type: 'book'; 3 | title: string; 4 | description: string; 5 | }; 6 | 7 | export type Magazine = { 8 | type: 'magazine'; 9 | title: string; 10 | }; 11 | 12 | export type Pen = { 13 | type: 'pen'; 14 | color: string; 15 | }; 16 | 17 | export type ItemsOnSale = Book | Magazine | Pen; 18 | 19 | export function isBook(item: ItemsOnSale): item is Book { 20 | return item.type === 'book'; 21 | } 22 | 23 | export function isMagazine(item: ItemsOnSale): item is Magazine { 24 | return item.type === 'magazine'; 25 | } 26 | 27 | export function isPen(item: ItemsOnSale): item is Pen { 28 | return item.type === 'pen'; 29 | } 30 | 31 | export function assertBook(item: ItemsOnSale): asserts item is Book { 32 | if (!isBook(item)) { 33 | throw new Error('Item is not a book'); 34 | } 35 | } 36 | 37 | export function assertMagazine(item: ItemsOnSale): asserts item is Magazine { 38 | if (!isMagazine(item)) { 39 | throw new Error('Item is not a magazine'); 40 | } 41 | } 42 | 43 | export function assertPen(item: ItemsOnSale): asserts item is Pen { 44 | if (!isPen(item)) { 45 | throw new Error('Item is not a pen'); 46 | } 47 | } 48 | 49 | export function assertNever(value: never): never { 50 | throw new Error('Unexpected value: ' + JSON.stringify(value, null, 2)); 51 | } 52 | -------------------------------------------------------------------------------- /src/components/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { FC, InputHTMLAttributes } from 'react'; 2 | 3 | type Props = InputHTMLAttributes; 4 | 5 | export const Checkbox: FC = ({ children, ...props }) => ( 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /src/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | 3 | export const Footer: FC = () => ( 4 |
5 | © {new Date().getFullYear()} ABL - The Problem Solver 6 |
7 | ); 8 | -------------------------------------------------------------------------------- /src/components/header.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import type { FC } from 'react'; 3 | 4 | export const Header: FC = () => ( 5 |
6 | 15 |
16 | ); 17 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './checkbox'; 2 | export * from './footer'; 3 | export * from './header'; 4 | export * from './labeled-checkbox'; 5 | export * from './labeled-input'; 6 | -------------------------------------------------------------------------------- /src/components/input.tsx: -------------------------------------------------------------------------------- 1 | import { FC, InputHTMLAttributes } from 'react'; 2 | 3 | type Props = InputHTMLAttributes; 4 | 5 | export const Input: FC = ({ ...props }) => ( 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /src/components/label.tsx: -------------------------------------------------------------------------------- 1 | import { FC, LabelHTMLAttributes } from 'react'; 2 | 3 | type Props = LabelHTMLAttributes; 4 | 5 | export const Label: FC = ({ children, ...props }) => ( 6 | 9 | ); 10 | -------------------------------------------------------------------------------- /src/components/labeled-checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps, FC, useId } from 'react'; 2 | import { Checkbox } from './checkbox'; 3 | import { Label } from './label'; 4 | 5 | type Props = ComponentProps; 6 | 7 | export const LabeledCheckbox: FC = ({ children, id, ...props }) => { 8 | const internalId = useId(); 9 | 10 | return ( 11 |
12 | 13 | 14 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/components/labeled-input.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps, FC, ReactNode, useId } from 'react'; 2 | import { Input } from './input'; 3 | import { Label } from './label'; 4 | 5 | type InputProps = ComponentProps; 6 | 7 | type Props = { 8 | children: ReactNode; 9 | } & InputProps; 10 | 11 | export const LabeledInput: FC = ({ children, ...props }) => { 12 | const internalId = useId(); 13 | 14 | return ( 15 |
16 | 17 | 18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/messages.ts: -------------------------------------------------------------------------------- 1 | export const messages = { 2 | 'hello-js': 'Hello JavaScript', 3 | 'hello-tsx': 'Hello TypeScript', 4 | }; 5 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | 118 | @media (prefers-color-scheme: dark) { 119 | .card, 120 | .footer { 121 | border-color: #222; 122 | } 123 | .code { 124 | background: #111; 125 | } 126 | .logo img { 127 | filter: invert(1); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noUncheckedIndexedAccess": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true 18 | }, 19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 20 | "exclude": ["node_modules"] 21 | } 22 | --------------------------------------------------------------------------------