├── epicshop
├── .diffignore
├── .npmrc
├── in-browser-tests.spec.js
├── update-deps.sh
├── package.json
├── post-set-playground.js
├── Dockerfile
├── playwright.config.js
├── setup-custom.js
├── fly.yaml
└── setup.js
├── .npmrc
├── public
├── favicon.ico
├── og
│ └── background.png
├── react.js
├── images
│ └── instructor.png
├── react-dom
│ └── client.js
└── favicon.svg
├── .prettierignore
├── .vscode
└── extensions.json
├── exercises
├── FINISHED.mdx
├── 10.arrays
│ ├── 03.solution.key-reset
│ │ ├── index.test.ts
│ │ ├── README.mdx
│ │ └── index.tsx
│ ├── FINISHED.mdx
│ ├── 02.solution.focus-state
│ │ ├── README.mdx
│ │ └── key.test.ts
│ ├── 03.problem.key-reset
│ │ ├── index.tsx
│ │ └── README.mdx
│ ├── 02.problem.focus-state
│ │ └── README.mdx
│ ├── 01.solution.key-prop
│ │ ├── key.test.ts
│ │ ├── index.tsx
│ │ └── README.mdx
│ └── 01.problem.key-prop
│ │ └── index.tsx
├── 07.forms
│ ├── FINISHED.mdx
│ ├── 04.solution.submit
│ │ ├── api.server.ts
│ │ ├── README.mdx
│ │ ├── action.test.ts
│ │ ├── api.test.ts
│ │ ├── index.tsx
│ │ └── submit.test.ts
│ ├── 05.problem.action
│ │ ├── api.server.ts
│ │ ├── README.mdx
│ │ └── index.tsx
│ ├── 02.problem.action
│ │ ├── api.server.ts
│ │ ├── index.tsx
│ │ └── README.mdx
│ ├── 03.problem.types
│ │ ├── api.server.ts
│ │ ├── README.mdx
│ │ └── index.tsx
│ ├── 03.solution.types
│ │ ├── api.server.ts
│ │ ├── action.test.ts
│ │ ├── README.mdx
│ │ └── index.tsx
│ ├── 05.solution.action
│ │ ├── README.mdx
│ │ └── index.tsx
│ ├── 02.solution.action
│ │ ├── api.server.ts
│ │ ├── README.mdx
│ │ ├── action.test.ts
│ │ ├── index.tsx
│ │ └── form.test.ts
│ ├── 01.solution.form
│ │ ├── README.mdx
│ │ ├── index.tsx
│ │ └── form.test.ts
│ ├── 04.problem.submit
│ │ ├── api.server.ts
│ │ └── index.tsx
│ └── 01.problem.form
│ │ ├── README.mdx
│ │ └── index.tsx
├── 08.inputs
│ ├── 03.solution.radio
│ │ └── README.mdx
│ ├── 02.solution.select
│ │ ├── README.mdx
│ │ └── index.tsx
│ ├── FINISHED.mdx
│ ├── 05.solution.default-value
│ │ └── README.mdx
│ ├── 01.solution.checkbox
│ │ ├── README.mdx
│ │ └── index.tsx
│ ├── 02.problem.select
│ │ ├── README.mdx
│ │ └── index.tsx
│ ├── 04.problem.hidden
│ │ └── README.mdx
│ ├── 01.problem.checkbox
│ │ ├── README.mdx
│ │ └── index.tsx
│ ├── 04.solution.hidden
│ │ └── README.mdx
│ ├── README.mdx
│ └── 03.problem.radio
│ │ ├── README.mdx
│ │ └── index.tsx
├── 06.styling
│ ├── 03.solution.size-prop
│ │ ├── README.mdx
│ │ ├── index.css
│ │ ├── index.tsx
│ │ └── boxes.test.ts
│ ├── 01.solution.style
│ │ ├── README.mdx
│ │ ├── index.css
│ │ ├── index.tsx
│ │ └── boxes.test.ts
│ ├── FINISHED.mdx
│ ├── 02.problem.component
│ │ ├── index.css
│ │ ├── index.tsx
│ │ └── README.mdx
│ ├── 02.solution.component
│ │ ├── index.css
│ │ ├── README.mdx
│ │ ├── index.tsx
│ │ ├── boxes.test.ts
│ │ └── box.test.tsx
│ ├── 03.problem.size-prop
│ │ ├── index.css
│ │ ├── README.mdx
│ │ └── index.tsx
│ └── 01.problem.style
│ │ ├── index.css
│ │ ├── README.mdx
│ │ └── index.tsx
├── 05.typescript
│ ├── 03.solution.derive
│ │ ├── README.mdx
│ │ ├── calculator.test.ts
│ │ └── index.tsx
│ ├── 06.solution.satisfies
│ │ ├── README.mdx
│ │ ├── calculator.test.ts
│ │ └── index.tsx
│ ├── FINISHED.mdx
│ ├── 04.solution.default-props
│ │ ├── README.mdx
│ │ ├── calculator.test.ts
│ │ └── index.tsx
│ ├── 05.solution.function-types
│ │ ├── README.mdx
│ │ ├── calculator.test.ts
│ │ └── index.tsx
│ ├── 01.solution.props
│ │ ├── README.mdx
│ │ ├── calculator.test.ts
│ │ └── index.tsx
│ ├── 02.solution.narrow
│ │ ├── README.mdx
│ │ ├── calculator.test.ts
│ │ └── index.tsx
│ ├── 01.problem.props
│ │ ├── README.mdx
│ │ └── index.tsx
│ ├── 03.problem.derive
│ │ ├── index.tsx
│ │ └── README.mdx
│ ├── 02.problem.narrow
│ │ ├── index.tsx
│ │ └── README.mdx
│ ├── 06.problem.satisfies
│ │ ├── index.tsx
│ │ └── README.mdx
│ ├── 04.problem.default-props
│ │ ├── README.mdx
│ │ └── index.tsx
│ └── 05.problem.function-types
│ │ ├── index.tsx
│ │ └── README.mdx
├── 09.errors
│ ├── FINISHED.mdx
│ ├── 02.solution.show-boundary
│ │ ├── README.mdx
│ │ └── error-boundary.test.ts
│ ├── 01.solution.composition
│ │ ├── README.mdx
│ │ └── error-boundary.test.ts
│ ├── 03.problem.reset
│ │ └── README.mdx
│ ├── 03.solution.reset
│ │ ├── README.mdx
│ │ └── error-boundary.test.ts
│ ├── 02.problem.show-boundary
│ │ └── README.mdx
│ └── 01.problem.composition
│ │ └── README.mdx
├── 03.using-jsx
│ ├── 04.solution.nesting
│ │ ├── README.mdx
│ │ ├── index.html
│ │ └── content.test.ts
│ ├── 05.solution.fragments
│ │ ├── README.mdx
│ │ ├── index.html
│ │ ├── index.test.ts
│ │ └── content.test.ts
│ ├── FINISHED.mdx
│ ├── 02.solution.interpolation
│ │ ├── README.mdx
│ │ ├── content.test.ts
│ │ ├── index.html
│ │ └── index.test.ts
│ ├── 03.solution.spread
│ │ ├── README.mdx
│ │ ├── index.test.ts
│ │ ├── content.test.ts
│ │ └── index.html
│ ├── 04.problem.nesting
│ │ ├── README.mdx
│ │ └── index.html
│ ├── 01.solution.compiler
│ │ ├── index.html
│ │ ├── content.test.ts
│ │ ├── README.mdx
│ │ └── index.test.ts
│ ├── 01.problem.compiler
│ │ ├── README.mdx
│ │ └── index.html
│ ├── 02.problem.interpolation
│ │ ├── index.html
│ │ └── README.mdx
│ ├── 03.problem.spread
│ │ ├── index.html
│ │ └── README.mdx
│ ├── 05.problem.fragments
│ │ └── index.html
│ └── README.mdx
├── 02.raw-react
│ ├── 03.solution.deep-nesting
│ │ ├── README.mdx
│ │ ├── index.html
│ │ └── index.test.ts
│ ├── FINISHED.mdx
│ ├── 02.solution.nesting
│ │ ├── README.mdx
│ │ ├── index.html
│ │ └── index.test.ts
│ ├── 01.solution.elements
│ │ ├── README.mdx
│ │ ├── index.html
│ │ └── index.test.ts
│ ├── 03.problem.deep-nesting
│ │ ├── README.mdx
│ │ └── index.html
│ ├── 02.problem.nesting
│ │ ├── README.mdx
│ │ └── index.html
│ └── 01.problem.elements
│ │ └── index.html
├── 04.components
│ ├── 04.solution.props
│ │ ├── README.mdx
│ │ ├── content.test.ts
│ │ └── index.html
│ ├── FINISHED.mdx
│ ├── 02.solution.raw
│ │ ├── README.mdx
│ │ ├── index.test.ts
│ │ ├── content.test.ts
│ │ └── index.html
│ ├── 01.solution.function
│ │ ├── README.mdx
│ │ ├── index.test.ts
│ │ ├── index.html
│ │ └── content.test.ts
│ ├── 03.solution.jsx
│ │ ├── index.test.ts
│ │ ├── index.html
│ │ ├── content.test.ts
│ │ └── README.mdx
│ ├── 04.problem.props
│ │ ├── README.mdx
│ │ └── index.html
│ ├── 01.problem.function
│ │ ├── index.html
│ │ └── README.mdx
│ ├── 03.problem.jsx
│ │ └── index.html
│ ├── 02.problem.raw
│ │ ├── index.html
│ │ └── README.mdx
│ └── README.mdx
├── 01.js-hello-world
│ ├── FINISHED.mdx
│ ├── 01.solution.hello
│ │ ├── README.mdx
│ │ ├── index.html
│ │ └── index.test.ts
│ ├── 02.problem.root
│ │ ├── README.mdx
│ │ └── index.html
│ ├── 02.solution.root
│ │ ├── README.mdx
│ │ ├── index.html
│ │ └── index.test.ts
│ └── 01.problem.hello
│ │ ├── README.mdx
│ │ └── index.html
└── README.mdx
├── tsconfig.json
├── LICENSE.md
├── .gitignore
├── eslint.config.js
├── .github
└── workflows
│ └── validate.yml
└── shared
└── api-utils.ts
/epicshop/.diffignore:
--------------------------------------------------------------------------------
1 | tsconfig.json
2 | *.test.*
3 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | legacy-peer-deps=true
2 | registry=https://registry.npmjs.org/
3 |
--------------------------------------------------------------------------------
/epicshop/.npmrc:
--------------------------------------------------------------------------------
1 | legacy-peer-deps=true
2 | registry=https://registry.npmjs.org/
3 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/epicweb-dev/react-fundamentals/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/og/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/epicweb-dev/react-fundamentals/HEAD/public/og/background.png
--------------------------------------------------------------------------------
/public/react.js:
--------------------------------------------------------------------------------
1 | export * from 'https://esm.sh/react?dev'
2 | export { default } from 'https://esm.sh/react?dev'
3 |
--------------------------------------------------------------------------------
/public/images/instructor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/epicweb-dev/react-fundamentals/HEAD/public/images/instructor.png
--------------------------------------------------------------------------------
/public/react-dom/client.js:
--------------------------------------------------------------------------------
1 | export * from 'https://esm.sh/react-dom/client?dev'
2 | export { default } from 'https://esm.sh/react-dom/client?dev'
3 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | **/build/**
3 | **/public/build/**
4 | **/public/babel-standalone.js
5 | .env
6 | **/package-lock.json
7 | **/playwright-report/**
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "dbaeumer.vscode-eslint",
4 | "esbenp.prettier-vscode",
5 | "neotan.vscode-auto-restart-typescript-eslint-servers"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/exercises/FINISHED.mdx:
--------------------------------------------------------------------------------
1 | # React Fundamentals ⚛
2 |
3 |
4 |
5 | Hooray! You're all done! 👏👏
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*.ts", "**/*.tsx"],
3 | "extends": ["@epic-web/config/typescript"],
4 | "compilerOptions": {
5 | "paths": {
6 | "#*": ["./*"]
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/exercises/10.arrays/03.solution.key-reset/index.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep } from '@epic-web/workshop-utils/test'
2 |
3 | await testStep('this is just a demo', () => {
4 | expect(true).toBe(true)
5 | })
6 |
--------------------------------------------------------------------------------
/exercises/07.forms/FINISHED.mdx:
--------------------------------------------------------------------------------
1 | # Forms
2 |
3 |
4 |
5 | 👨💼 Hooray! You've gotten a good start on how to use HTML forms with React!
6 |
--------------------------------------------------------------------------------
/exercises/08.inputs/03.solution.radio/README.mdx:
--------------------------------------------------------------------------------
1 | # Radios
2 |
3 |
4 |
5 | 👨💼 Great! Now we have a nice way to select Public and Private.
6 |
--------------------------------------------------------------------------------
/exercises/06.styling/03.solution.size-prop/README.mdx:
--------------------------------------------------------------------------------
1 | # Size Prop
2 |
3 |
4 |
5 | 👨💼 The constraints set people free! Great work on the `size` prop!
6 |
--------------------------------------------------------------------------------
/exercises/05.typescript/03.solution.derive/README.mdx:
--------------------------------------------------------------------------------
1 | # Derive Types
2 |
3 |
4 |
5 | 👨💼 Great! That's definitely a win for our developer productivity!
6 |
--------------------------------------------------------------------------------
/exercises/08.inputs/02.solution.select/README.mdx:
--------------------------------------------------------------------------------
1 | # Select
2 |
3 |
4 |
5 | 👨💼 Great work. Now our users can select an account type with some given options.
6 |
--------------------------------------------------------------------------------
/exercises/08.inputs/FINISHED.mdx:
--------------------------------------------------------------------------------
1 | # Inputs
2 |
3 |
4 |
5 | 👨💼 Great job! You've gotten a good start on how to use many of the input types
6 | in React.
7 |
--------------------------------------------------------------------------------
/exercises/09.errors/FINISHED.mdx:
--------------------------------------------------------------------------------
1 | # Error Boundaries
2 |
3 |
4 |
5 | 👨💼 Great work 👏 You now know how to manage errors in React components!
6 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/04.solution.nesting/README.mdx:
--------------------------------------------------------------------------------
1 | # Nesting JSX
2 |
3 |
4 |
5 | 👨💼 Great work! Isn't that so much better than raw `createElement` calls? 😆
6 |
--------------------------------------------------------------------------------
/exercises/05.typescript/06.solution.satisfies/README.mdx:
--------------------------------------------------------------------------------
1 | # Satisfies
2 |
3 |
4 |
5 | 👨💼 That is...
6 |
7 | ...
8 |
9 | ...
10 |
11 | Satisfactory. 🤡
12 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | This material is available for private, non-commercial use under the
2 | [GPL version 3](http://www.gnu.org/licenses/gpl-3.0-standalone.html). If you
3 | would like to use this material to conduct your own workshop, please contact us
4 | at team@epicweb.dev
5 |
--------------------------------------------------------------------------------
/exercises/02.raw-react/03.solution.deep-nesting/README.mdx:
--------------------------------------------------------------------------------
1 | # Deep Nesting Elements
2 |
3 |
4 |
5 | 👨💼 Great job! We can go as deep as we want with this stuff!
6 |
--------------------------------------------------------------------------------
/exercises/02.raw-react/FINISHED.mdx:
--------------------------------------------------------------------------------
1 | # Raw React APIs
2 |
3 |
4 |
5 | 👨💼 Wahoo! I'll bet you're excited to get into JSX after working with the raw
6 | APIs huh? 😅
7 |
--------------------------------------------------------------------------------
/exercises/07.forms/04.solution.submit/api.server.ts:
--------------------------------------------------------------------------------
1 | import { respondWithDataTable } from '#shared/api-utils'
2 |
3 | export async function action({ request }: { request: Request }) {
4 | const data = await request.formData()
5 | return respondWithDataTable(data)
6 | }
7 |
--------------------------------------------------------------------------------
/exercises/07.forms/05.problem.action/api.server.ts:
--------------------------------------------------------------------------------
1 | import { respondWithDataTable } from '#shared/api-utils'
2 |
3 | export async function action({ request }: { request: Request }) {
4 | const data = await request.formData()
5 | return respondWithDataTable(data)
6 | }
7 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/05.solution.fragments/README.mdx:
--------------------------------------------------------------------------------
1 | # Fragments
2 |
3 |
4 |
5 | 👨💼 Great! Now you can put elements side-by-side without worrying about their
6 | parent element.
7 |
--------------------------------------------------------------------------------
/exercises/07.forms/02.problem.action/api.server.ts:
--------------------------------------------------------------------------------
1 | import { respondWithDataTable } from '#shared/api-utils'
2 |
3 | export async function loader({ request }: { request: Request }) {
4 | const data = new URL(request.url).searchParams
5 | return respondWithDataTable(data)
6 | }
7 |
--------------------------------------------------------------------------------
/exercises/07.forms/03.problem.types/api.server.ts:
--------------------------------------------------------------------------------
1 | import { respondWithDataTable } from '#shared/api-utils'
2 |
3 | export async function loader({ request }: { request: Request }) {
4 | const data = new URL(request.url).searchParams
5 | return respondWithDataTable(data)
6 | }
7 |
--------------------------------------------------------------------------------
/exercises/07.forms/03.solution.types/api.server.ts:
--------------------------------------------------------------------------------
1 | import { respondWithDataTable } from '#shared/api-utils'
2 |
3 | export async function loader({ request }: { request: Request }) {
4 | const data = new URL(request.url).searchParams
5 | return respondWithDataTable(data)
6 | }
7 |
--------------------------------------------------------------------------------
/exercises/07.forms/05.solution.action/README.mdx:
--------------------------------------------------------------------------------
1 | # Form Actions
2 |
3 |
4 |
5 | 👨💼 Nice! This is a handy feature from React for handling form submissions via
6 | the client.
7 |
--------------------------------------------------------------------------------
/exercises/04.components/04.solution.props/README.mdx:
--------------------------------------------------------------------------------
1 | # Props
2 |
3 |
4 |
5 | 👨💼 Great work! Now you have a solid understanding of how React treats JSX and
6 | custom components! Well done.
7 |
--------------------------------------------------------------------------------
/exercises/05.typescript/FINISHED.mdx:
--------------------------------------------------------------------------------
1 | # TypeScript
2 |
3 |
4 |
5 | 👨💼 Awesome work. TypeScript is a must in this industry and using it with React
6 | is fairly straightforward.
7 |
--------------------------------------------------------------------------------
/exercises/07.forms/02.solution.action/api.server.ts:
--------------------------------------------------------------------------------
1 | import { respondWithDataTable } from '#shared/api-utils'
2 |
3 | export async function loader({ request }: { request: Request }) {
4 | const data = new URL(request.url).searchParams
5 | return respondWithDataTable(data)
6 | }
7 |
--------------------------------------------------------------------------------
/exercises/08.inputs/05.solution.default-value/README.mdx:
--------------------------------------------------------------------------------
1 | # Default Value
2 |
3 |
4 |
5 | 👨💼 Great! Now when the user loads up the form, they'll have a good default state
6 | to start from.
7 |
--------------------------------------------------------------------------------
/exercises/07.forms/01.solution.form/README.mdx:
--------------------------------------------------------------------------------
1 | # Form
2 |
3 |
4 |
5 | 👨💼 Great job. We now have a form that we can type the username into and we can
6 | submit it. But we've got more work to do. Let's keep going.
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | workspace/
4 | **/.cache/
5 | **/build/
6 | **/public/build
7 | /playground
8 | **/tsconfig.tsbuildinfo
9 |
10 | # in a real app you'd want to not commit the .env
11 | # file as well, but since this is for a workshop
12 | # we're going to keep them around.
13 | # .env
14 |
--------------------------------------------------------------------------------
/exercises/05.typescript/04.solution.default-props/README.mdx:
--------------------------------------------------------------------------------
1 | # Default Props
2 |
3 |
4 |
5 | 👨💼 In the React world we use default props all the time, so I'm glad that you
6 | know all about how to do that now!
7 |
--------------------------------------------------------------------------------
/exercises/06.styling/01.solution.style/README.mdx:
--------------------------------------------------------------------------------
1 | # Styling
2 |
3 |
4 |
5 | 👨💼 Great! We now know how to add a `className` and `style` prop to our elements.
6 | But we've got some repetition that would be nice to ditch...
7 |
--------------------------------------------------------------------------------
/exercises/05.typescript/05.solution.function-types/README.mdx:
--------------------------------------------------------------------------------
1 | # Reduce Duplication
2 |
3 |
4 |
5 | 👨💼 Getting rid of duplication quite nice here. Well done! But we duplicated
6 | something else which is less nice...
7 |
--------------------------------------------------------------------------------
/exercises/10.arrays/FINISHED.mdx:
--------------------------------------------------------------------------------
1 | # Rendering Arrays
2 |
3 |
4 |
5 | 👨💼 Good work! You've successfully learned the importance of associating react
6 | elements to the data they represent when rendering arrays.
7 |
--------------------------------------------------------------------------------
/exercises/05.typescript/01.solution.props/README.mdx:
--------------------------------------------------------------------------------
1 | # Props
2 |
3 |
4 |
5 | 👨💼 Great work! Now we'll get some type safety when people are using the
6 | `Calculator` component. But I think we can do better for the operator prop...
7 |
--------------------------------------------------------------------------------
/exercises/09.errors/02.solution.show-boundary/README.mdx:
--------------------------------------------------------------------------------
1 | # Other Errors
2 |
3 |
4 |
5 | 👨💼 Great work! Now you can handle errors and give the user a better experience
6 | than nothing happening at all when they submit the form.
7 |
--------------------------------------------------------------------------------
/exercises/01.js-hello-world/FINISHED.mdx:
--------------------------------------------------------------------------------
1 | # Hello World in JS
2 |
3 |
4 |
5 | 👨💼 Great job! You did it! Now's your opportunity to review what you learned.
6 | Writing down what you've learned helps you to remember it better.
7 |
--------------------------------------------------------------------------------
/exercises/02.raw-react/02.solution.nesting/README.mdx:
--------------------------------------------------------------------------------
1 | # Nesting Elements
2 |
3 |
4 |
5 | 👨💼 Great! Now we've got a nice way to nest our React elements so we can build
6 | complex structures in our generated HTML. Let's go **even deeper**!
7 |
--------------------------------------------------------------------------------
/exercises/05.typescript/02.solution.narrow/README.mdx:
--------------------------------------------------------------------------------
1 | # Narrow Types
2 |
3 |
4 |
5 | 👨💼 Great, we've narrowed our types a bit which helps a lot with the experience
6 | of using our component props. But there's more to do! Let's keep going.
7 |
--------------------------------------------------------------------------------
/exercises/07.forms/04.solution.submit/README.mdx:
--------------------------------------------------------------------------------
1 | # Submission
2 |
3 |
4 |
5 | 👨💼 Great work! There's a lot more to form submissions on the server side of
6 | things, but this will get us going to handling the UI side of this with React,
7 | so good job.
8 |
--------------------------------------------------------------------------------
/epicshop/in-browser-tests.spec.js:
--------------------------------------------------------------------------------
1 | import { dirname, resolve } from 'path'
2 | import { fileURLToPath } from 'url'
3 | import { setupInBrowserTests } from '@epic-web/workshop-utils/playwright.server'
4 |
5 | const __dirname = dirname(fileURLToPath(import.meta.url))
6 | process.env.EPICSHOP_CONTEXT_CWD = resolve(__dirname, '..')
7 |
8 | setupInBrowserTests()
9 |
--------------------------------------------------------------------------------
/exercises/06.styling/FINISHED.mdx:
--------------------------------------------------------------------------------
1 | # Styling
2 |
3 |
4 |
5 | 👨💼 Great work! Styling is all about the `style` and `className` props. There's
6 | much more to know about styling on the web, but that should get you going for
7 | 99% of your needs with React! Well done.
8 |
--------------------------------------------------------------------------------
/exercises/01.js-hello-world/01.solution.hello/README.mdx:
--------------------------------------------------------------------------------
1 | # Hello World in JS
2 |
3 |
4 |
5 | 👨💼 Awesome job. Now you know how to create DOM nodes using the regular JS APIs.
6 |
7 | But you know what, we could probably do even more in JS... Let's look at that
8 | next.
9 |
--------------------------------------------------------------------------------
/exercises/02.raw-react/01.solution.elements/README.mdx:
--------------------------------------------------------------------------------
1 | # Create React Elements
2 |
3 |
4 |
5 | 👨💼 Great work! We're well on our way to using React for building UIs on the web!
6 | But most apps are a little more complicated than a single element. Let's go
7 | deeper!
8 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/FINISHED.mdx:
--------------------------------------------------------------------------------
1 | # Using JSX
2 |
3 |
4 |
5 | 👨💼 Hooray! You now know how to use JSX effectively to make really nicely
6 | composable user interfaces with React. You've got the fundamentals down, from
7 | here it's just a matter of practice. Great job!
8 |
--------------------------------------------------------------------------------
/exercises/07.forms/04.problem.submit/api.server.ts:
--------------------------------------------------------------------------------
1 | import { respondWithDataTable } from '#shared/api-utils'
2 |
3 | // 🐨 change this from `loader` to `action`
4 | export async function loader({ request }: { request: Request }) {
5 | // 🐨 change data to be `await request.formData()`
6 | const data = new URL(request.url).searchParams
7 | return respondWithDataTable(data)
8 | }
9 |
--------------------------------------------------------------------------------
/exercises/07.forms/02.solution.action/README.mdx:
--------------------------------------------------------------------------------
1 | # Form Action
2 |
3 |
4 |
5 | 👨💼 Super! Now we're submitting the data to that endpoint. The default behavior
6 | of the form is to make a GET request which sends the data in the URL search
7 | params. We'll change it in the near future.
8 |
--------------------------------------------------------------------------------
/epicshop/update-deps.sh:
--------------------------------------------------------------------------------
1 | set -e
2 |
3 | npx npm-check-updates --dep prod,dev --upgrade --root
4 | cd epicshop && npx npm-check-updates --dep prod,dev --upgrade --root
5 | cd ..
6 | rm -rf node_modules package-lock.json ./epicshop/package-lock.json ./epicshop/node_modules ./exercises/**/node_modules
7 | npm install
8 | npm run setup
9 | npm run typecheck
10 | npm run lint -- --fix
11 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/02.solution.interpolation/README.mdx:
--------------------------------------------------------------------------------
1 | # Interpolation
2 |
3 |
4 |
5 | 👨💼 Great job! Remember, if you can compile the JSX you see into JavaScript
6 | expressions (and vice-versa), you'll be much more efficient with this syntax.
7 | It's all just JavaScript expressions!
8 |
--------------------------------------------------------------------------------
/exercises/06.styling/02.problem.component/index.css:
--------------------------------------------------------------------------------
1 | .box {
2 | border: 1px solid #333;
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: center;
6 | text-align: center;
7 | }
8 | .box--large {
9 | width: 270px;
10 | height: 270px;
11 | }
12 | .box--medium {
13 | width: 180px;
14 | height: 180px;
15 | }
16 | .box--small {
17 | width: 90px;
18 | height: 90px;
19 | }
20 |
--------------------------------------------------------------------------------
/exercises/06.styling/02.solution.component/index.css:
--------------------------------------------------------------------------------
1 | .box {
2 | border: 1px solid #333;
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: center;
6 | text-align: center;
7 | }
8 | .box--large {
9 | width: 270px;
10 | height: 270px;
11 | }
12 | .box--medium {
13 | width: 180px;
14 | height: 180px;
15 | }
16 | .box--small {
17 | width: 90px;
18 | height: 90px;
19 | }
20 |
--------------------------------------------------------------------------------
/exercises/06.styling/03.problem.size-prop/index.css:
--------------------------------------------------------------------------------
1 | .box {
2 | border: 1px solid #333;
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: center;
6 | text-align: center;
7 | }
8 | .box--large {
9 | width: 270px;
10 | height: 270px;
11 | }
12 | .box--medium {
13 | width: 180px;
14 | height: 180px;
15 | }
16 | .box--small {
17 | width: 90px;
18 | height: 90px;
19 | }
20 |
--------------------------------------------------------------------------------
/exercises/06.styling/03.solution.size-prop/index.css:
--------------------------------------------------------------------------------
1 | .box {
2 | border: 1px solid #333;
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: center;
6 | text-align: center;
7 | }
8 | .box--large {
9 | width: 270px;
10 | height: 270px;
11 | }
12 | .box--medium {
13 | width: 180px;
14 | height: 180px;
15 | }
16 | .box--small {
17 | width: 90px;
18 | height: 90px;
19 | }
20 |
--------------------------------------------------------------------------------
/exercises/10.arrays/03.solution.key-reset/README.mdx:
--------------------------------------------------------------------------------
1 | # Key Reset
2 |
3 |
4 |
5 | 🦉 This isn't something you're going to do a lot, but it's handy to know about
6 | for situations where you want to reset the state of a section of your React app.
7 | And hopefully it helps you understand keys a bit more as well.
8 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import defaultConfig from '@epic-web/config/eslint'
2 |
3 | /** @type {import("eslint").Linter.Config} */
4 | export default [
5 | { ignores: ['**/babel-standalone.js'] },
6 | ...defaultConfig,
7 | {
8 | rules: {
9 | // we leave unused vars around for the exercises
10 | 'no-unused-vars': 'off',
11 | '@typescript-eslint/no-unused-vars': 'off',
12 | },
13 | },
14 | ]
15 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/03.solution.spread/README.mdx:
--------------------------------------------------------------------------------
1 | # Spread props
2 |
3 |
4 |
5 | 👨💼 Great! We do this all the time in React. You'll often find yourself wrapping
6 | components to compose things in interesting ways and there's where this pattern
7 | is most useful. We'll get to custom components like that later.
8 |
--------------------------------------------------------------------------------
/exercises/06.styling/01.problem.style/index.css:
--------------------------------------------------------------------------------
1 | .box {
2 | border: 1px solid #333;
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: center;
6 | text-align: center;
7 | }
8 |
9 | .box--large {
10 | width: 270px;
11 | height: 270px;
12 | }
13 |
14 | .box--medium {
15 | width: 180px;
16 | height: 180px;
17 | }
18 |
19 | .box--small {
20 | width: 90px;
21 | height: 90px;
22 | }
23 |
--------------------------------------------------------------------------------
/exercises/06.styling/01.solution.style/index.css:
--------------------------------------------------------------------------------
1 | .box {
2 | border: 1px solid #333;
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: center;
6 | text-align: center;
7 | }
8 |
9 | .box--large {
10 | width: 270px;
11 | height: 270px;
12 | }
13 |
14 | .box--medium {
15 | width: 180px;
16 | height: 180px;
17 | }
18 |
19 | .box--small {
20 | width: 90px;
21 | height: 90px;
22 | }
23 |
--------------------------------------------------------------------------------
/exercises/09.errors/01.solution.composition/README.mdx:
--------------------------------------------------------------------------------
1 | # Composition
2 |
3 |
4 |
5 | 👨💼 Great job! You've successfully wrapped our app logic in an error boundary
6 | that can handle any errors that occur in the app. But what about other kinds of
7 | errors? The kind that React doesn't know about? Let's look at that next.
8 |
--------------------------------------------------------------------------------
/exercises/01.js-hello-world/01.solution.hello/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/exercises/08.inputs/01.solution.checkbox/README.mdx:
--------------------------------------------------------------------------------
1 | # Checkbox
2 |
3 |
4 |
5 | 👨💼 Great job! Interesting (weird) how checkboxes work huh?
6 |
7 | You may have also noticed that the `age` input is a string even though its
8 | `type` attribute is `number`. It's good to understand how values are submitted
9 | for different input types!
10 |
--------------------------------------------------------------------------------
/epicshop/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module",
3 | "scripts": {
4 | "test:setup": "playwright install chromium --with-deps",
5 | "test": "playwright test"
6 | },
7 | "dependencies": {
8 | "@epic-web/config": "^1.21.3",
9 | "@epic-web/workshop-app": "^6.47.1",
10 | "@epic-web/workshop-utils": "^6.47.1",
11 | "epicshop": "^6.47.1",
12 | "execa": "^9.6.0",
13 | "fs-extra": "^11.3.2"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/exercises/07.forms/02.solution.action/action.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 |
3 | import './index.tsx'
4 |
5 | const form = await dtl.waitFor(() => {
6 | const form = document.querySelector('form')
7 | expect(form).toBeInTheDocument()
8 | return form
9 | })
10 |
11 | await testStep('Form has correct action', async () => {
12 | expect(form).toHaveAttribute('action', 'api/onboarding')
13 | })
14 |
--------------------------------------------------------------------------------
/exercises/07.forms/03.solution.types/action.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 |
3 | import './index.tsx'
4 |
5 | const form = await dtl.waitFor(() => {
6 | const form = document.querySelector('form')
7 | expect(form).toBeInTheDocument()
8 | return form
9 | })
10 |
11 | await testStep('Form has correct action', async () => {
12 | expect(form).toHaveAttribute('action', 'api/onboarding')
13 | })
14 |
--------------------------------------------------------------------------------
/exercises/06.styling/02.solution.component/README.mdx:
--------------------------------------------------------------------------------
1 | # Custom Component
2 |
3 |
4 |
5 | 👨💼 Great work with the ` ` component. But we can make this experience even
6 | better by designing our box around the different sizes. Let's keep going!
7 |
8 | 🚨 Also, don't forget to export your Box component (tests will need it!) `export function Box...`
9 |
--------------------------------------------------------------------------------
/exercises/07.forms/04.solution.submit/action.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 |
3 | import './index.tsx'
4 |
5 | const form = await dtl.waitFor(() => {
6 | const form = document.querySelector('form')
7 | expect(form).toBeInTheDocument()
8 | return form
9 | })
10 |
11 | await testStep('Form does not have incorrect method', async () => {
12 | expect(form).not.toHaveAttribute('method', 'GET')
13 | })
14 |
--------------------------------------------------------------------------------
/exercises/10.arrays/02.solution.focus-state/README.mdx:
--------------------------------------------------------------------------------
1 | # Focus State
2 |
3 |
4 |
5 | 🦉 There are some other interesting things you can do with `key`s as well (like
6 | changing them on an element to intentionally reset the state of a component).
7 | Feel free to play around with that if you like, but we'll be using that in a
8 | future workshop so look forward to it!
9 |
--------------------------------------------------------------------------------
/exercises/01.js-hello-world/02.problem.root/README.mdx:
--------------------------------------------------------------------------------
1 | # Generate the Root Node
2 |
3 |
4 |
5 | Rather than having the `root` node in the HTML, see if you can create that one
6 | using JavaScript as well. Remove the `
` from the HTML and
7 | instead of trying to find it with `document.getElementById('root')`, create it
8 | and append it to the `document.body`.
9 |
--------------------------------------------------------------------------------
/exercises/04.components/FINISHED.mdx:
--------------------------------------------------------------------------------
1 | # Custom Components
2 |
3 |
4 |
5 | 👨💼 Great job! Now you've got a good handle on what it takes to create a custom
6 | component with React. And it's not a surface-level understanding either. You
7 | understand how it works under the hood which will be critical to your experience
8 | building custom components going forward. Great job!
9 |
--------------------------------------------------------------------------------
/exercises/08.inputs/02.problem.select/README.mdx:
--------------------------------------------------------------------------------
1 | # Select
2 |
3 |
4 |
5 | 👨💼 We have four different account types:
6 |
7 | - Admin
8 | - Teacher
9 | - Parent
10 | - Student
11 |
12 | Please add a `` with ``s for each account type.
13 |
14 | 📜 Learn more about this input type via
15 | [`` on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select).
16 |
--------------------------------------------------------------------------------
/exercises/10.arrays/03.solution.key-reset/index.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 |
4 | function App() {
5 | const [key, setKey] = useState(0)
6 | return (
7 |
8 |
9 | setKey((key) => key + 1)}>Reset
10 |
11 | )
12 | }
13 |
14 | const rootEl = document.createElement('div')
15 | document.body.append(rootEl)
16 | createRoot(rootEl).render( )
17 |
--------------------------------------------------------------------------------
/exercises/07.forms/01.solution.form/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | function App() {
4 | return (
5 |
12 | )
13 | }
14 |
15 | const rootEl = document.createElement('div')
16 | document.body.append(rootEl)
17 | createRoot(rootEl).render( )
18 |
--------------------------------------------------------------------------------
/exercises/07.forms/04.solution.submit/api.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep } from '@epic-web/workshop-utils/test'
2 |
3 | await testStep('POST api/onboarding', async () => {
4 | const formData = new FormData()
5 | const name = 'Kody Koala'
6 | formData.set('name', name)
7 | const response = await fetch('api/onboarding', {
8 | method: 'POST',
9 | body: formData,
10 | })
11 |
12 | expect(response.status).toBe(200)
13 | expect(await response.text()).toContain(name)
14 | })
15 |
--------------------------------------------------------------------------------
/epicshop/post-set-playground.js:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs'
2 | import path from 'node:path'
3 |
4 | const exclude = [
5 | 'exercises/01.',
6 | 'exercises/02.',
7 | 'exercises/03.',
8 | 'exercises/04.',
9 | ]
10 |
11 | if (
12 | exclude.every((e) => !process.env.EPICSHOP_PLAYGROUND_SRC_DIR.includes(e))
13 | ) {
14 | fs.writeFileSync(
15 | path.join(process.env.EPICSHOP_PLAYGROUND_DEST_DIR, 'tsconfig.json'),
16 | JSON.stringify({ extends: '../tsconfig' }, null, 2),
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/exercises/02.raw-react/03.problem.deep-nesting/README.mdx:
--------------------------------------------------------------------------------
1 | # Deep Nesting Elements
2 |
3 |
4 |
5 | 👨💼 Just to make sure we really understand how to nest this stuff. Try to create
6 | this using React's APIs:
7 |
8 | ```html
9 |
10 |
Here's Sam's favorite food:
11 |
12 | Green eggs
13 | Ham
14 |
15 |
16 | ```
17 |
--------------------------------------------------------------------------------
/exercises/07.forms/01.problem.form/README.mdx:
--------------------------------------------------------------------------------
1 | # Form
2 |
3 |
4 |
5 | 👨💼 We want to have an intake form which accepts a number of bits of information
6 | for a new user. They'll need a username to start with, so let's just render that
7 | and the submit button for now.
8 |
9 |
10 | 🦉 You may notice a full page refresh when you submit the form. We'll talk
11 | about this in a future step.
12 |
13 |
--------------------------------------------------------------------------------
/exercises/07.forms/02.solution.action/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | function App() {
4 | return (
5 |
12 | )
13 | }
14 |
15 | const rootEl = document.createElement('div')
16 | document.body.append(rootEl)
17 | createRoot(rootEl).render( )
18 |
--------------------------------------------------------------------------------
/exercises/07.forms/02.problem.action/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | function App() {
4 | return (
5 | // 🐨 add an action prop pointing to "api/onboarding"
6 |
13 | )
14 | }
15 |
16 | const rootEl = document.createElement('div')
17 | document.body.append(rootEl)
18 | createRoot(rootEl).render( )
19 |
--------------------------------------------------------------------------------
/exercises/10.arrays/03.problem.key-reset/index.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 |
4 | function App() {
5 | const [key, setKey] = useState(0)
6 | return (
7 |
8 | {/* 🐨 add a key prop to this input and set it to the key state */}
9 |
10 | setKey((key) => key + 1)}>Reset
11 |
12 | )
13 | }
14 |
15 | const rootEl = document.createElement('div')
16 | document.body.append(rootEl)
17 | createRoot(rootEl).render( )
18 |
--------------------------------------------------------------------------------
/exercises/07.forms/01.problem.form/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | function App() {
4 | // 🐨 render a form
5 | // 🐨 render a "Username" label
6 | // 🐨 render an input with the name "username"
7 | // 🐨 render a button
8 | // 💯 associate the label to the input using htmlFor and id attributes
9 | // 💯 explicitly set the button type appropriately
10 | return 'TODO'
11 | }
12 |
13 | const rootEl = document.createElement('div')
14 | document.body.append(rootEl)
15 | createRoot(rootEl).render( )
16 |
--------------------------------------------------------------------------------
/exercises/08.inputs/04.problem.hidden/README.mdx:
--------------------------------------------------------------------------------
1 | # Hidden Inputs
2 |
3 |
4 |
5 | 👨💼 Our backend needs to know what organization to associate this new account to.
6 | We don't need to ask the user this information because we know that it's just
7 | `123`. So we can add
8 | [a hidden input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/hidden)
9 | to make sure the submission includes this value, but not require the user to
10 | provide it.
11 |
--------------------------------------------------------------------------------
/exercises/04.components/02.solution.raw/README.mdx:
--------------------------------------------------------------------------------
1 | # Raw API
2 |
3 |
4 |
5 | 👨💼 Great! While that's not really all that much nicer to work with, it does
6 | get us a centimeter closer to our goal of using JSX with custom components.
7 | It's also interesting to throw a couple console logs around and see how using
8 | `createElement` compares to simply calling the function directly in our
9 | JSX.
10 |
11 | Now we're finally ready to use JSX for our custom components! Let's go!
12 |
--------------------------------------------------------------------------------
/exercises/01.js-hello-world/02.solution.root/README.mdx:
--------------------------------------------------------------------------------
1 | # Generate the Root Node
2 |
3 |
4 |
5 | 👨💼 Great! Now we can create DOM nodes dynamically using JavaScript. This is only
6 | the beginning of our journey. Creating DOM nodes ourselves is not typically the
7 | best way to build a full fledged application, but it's important for you to
8 | understand that what libraries like React are doing is not magic. They're just
9 | creating and modifying DOM nodes using JavaScript.
10 |
--------------------------------------------------------------------------------
/exercises/01.js-hello-world/02.solution.root/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/exercises/04.components/01.solution.function/README.mdx:
--------------------------------------------------------------------------------
1 | # Simple Function
2 |
3 |
4 |
5 | 👨💼 Super, but that's definitely not the most fun way to use custom components.
6 | In fact, we're technically not creating a custom component at all. We're just
7 | interpolating a function call into our template. This will have interesting
8 | implications in the future with React features like state and effects.
9 |
10 | Instead, let's create React elements out of this function which is what we're
11 | going for...
12 |
--------------------------------------------------------------------------------
/exercises/04.components/02.solution.raw/index.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep } from '@epic-web/workshop-utils/test'
2 |
3 | const response = await fetch(location.href)
4 | const indexHtml = await response.text()
5 | const node = document.createElement('div')
6 | node.innerHTML = indexHtml
7 |
8 | const inlineScript = node.querySelector('script[type="text/babel"]')
9 | if (!inlineScript) {
10 | throw new Error('inlineScript not found')
11 | }
12 |
13 | await testStep('message function is passed to createElement', async () => {
14 | expect(inlineScript.textContent).to.include('createElement(message')
15 | })
16 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/03.solution.spread/index.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep } from '@epic-web/workshop-utils/test'
2 |
3 | const response = await fetch(location.href)
4 | const indexHtml = await response.text()
5 | const node = document.createElement('div')
6 | node.innerHTML = indexHtml
7 |
8 | const inlineScript = node.querySelector('script[type="text/babel"]')
9 | if (!inlineScript) {
10 | throw new Error('inlineScript not found')
11 | }
12 |
13 | await testStep('props are spread on the div', async () => {
14 | expect(inlineScript.textContent, 'props should be spread').to.include(
15 | '...props',
16 | )
17 | })
18 |
--------------------------------------------------------------------------------
/exercises/04.components/03.solution.jsx/index.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep } from '@epic-web/workshop-utils/test'
2 |
3 | const response = await fetch(location.href)
4 | const indexHtml = await response.text()
5 | const node = document.createElement('div')
6 | node.innerHTML = indexHtml
7 |
8 | const inlineScript = node.querySelector('script[type="text/babel"]')
9 | if (!inlineScript) {
10 | throw new Error('inlineScript not found')
11 | }
12 |
13 | await testStep(
14 | 'Correctly creating a component with JSX',
15 | async () => {
16 | expect(inlineScript.textContent).to.include('
4 |
5 | 👨💼 Our developers are having a hard time using the `Calculator` component
6 | properly, so I want you to add type annotations.
7 |
8 | I'd also like you to meet Lily the Life Jacket! 🦺 She's going to be hanging
9 | around with the rest of the crew to indicate wherever there's something that's
10 | TypeScript-specific you need to do and to give you TypeScript-specific tips.
11 | You'll be working with Lily the Life Jacket a lot in this exercise.
12 |
13 | Follow the emoji!
14 |
--------------------------------------------------------------------------------
/exercises/10.arrays/02.problem.focus-state/README.mdx:
--------------------------------------------------------------------------------
1 | # Focus State
2 |
3 |
4 |
5 | 🦉 You can observe that when we're talking about "state" we're also talking
6 | about keyboard focus as well as what text is selected! As you play around with
7 | this, try selecting text in the inputs and observe how the first two examples
8 | differ from the last one. You'll notice that using the array `index` as a key is
9 | no different from React's default behavior, so it's unlikely to fix issues if
10 | you're having them. Best to use a unique ID. Play around with it!
11 |
--------------------------------------------------------------------------------
/exercises/09.errors/03.problem.reset/README.mdx:
--------------------------------------------------------------------------------
1 | # Reset
2 |
3 |
4 |
5 | 👨💼 Sometimes errors are temporary and will go away if you just "turn it off and
6 | on again." `react-error-boundary` supports this with a `resetErrorBoundary`
7 | function it passes to our `FallbackComponent`. We can use this to reset the
8 | error boundary and try again.
9 |
10 | Once you're done, you can test it out. We've still got the typo in our
11 | `onSubmit` handler, so go ahead and trigger the error by submitting the form,
12 | then click the "Try again" button to see the error go away.
13 |
--------------------------------------------------------------------------------
/exercises/09.errors/01.solution.composition/error-boundary.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 | const { screen } = dtl
3 |
4 | import './index.tsx'
5 |
6 | await testStep(
7 | 'Error boundary is rendered immediately due to render error',
8 | async () => {
9 | // Check if the error message is displayed
10 | const errorMessage = await screen.findByRole('alert')
11 | expect(errorMessage).toBeDefined()
12 | expect(errorMessage.textContent).toContain('There was an error:')
13 |
14 | // Ensure the form is not rendered
15 | const form = screen.queryByRole('form')
16 | expect(form).toBeNull()
17 | },
18 | )
19 |
--------------------------------------------------------------------------------
/exercises/02.raw-react/01.solution.elements/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/04.problem.nesting/README.mdx:
--------------------------------------------------------------------------------
1 | # Nesting JSX
2 |
3 |
4 |
5 | 👨💼 Remember when we created this with `createElement`?
6 |
7 | ```html
8 |
9 |
Here's Sam's favorite food:
10 |
11 | Green eggs
12 | Ham
13 |
14 |
15 | ```
16 |
17 | That was... intellectually stimulating 😅. Well, now try doing the same thing
18 | using JSX. I promise it'll be more fun this time.
19 |
20 |
21 | 💰 Tip: remember `class` in JSX is `className`.
22 |
23 |
--------------------------------------------------------------------------------
/exercises/10.arrays/03.problem.key-reset/README.mdx:
--------------------------------------------------------------------------------
1 | # Key Reset
2 |
3 |
4 |
5 | 🦉 Interestingly, we can use the `key` as a mechanism for removing and re-adding
6 | elements to the DOM. This is useful when we want to reset the state of a
7 | component or elements within a component.
8 |
9 | We haven't covered state yet, but state is necessary for this exercise so that's
10 | done for you. All we need you to do is add the key prop and see how that affects
11 | the state of the app. Enjoy!
12 |
13 | 👨💼 Type in the input, then click "reset" and think about why that works the way
14 | it does.
15 |
--------------------------------------------------------------------------------
/exercises/09.errors/03.solution.reset/README.mdx:
--------------------------------------------------------------------------------
1 | # Reset
2 |
3 |
4 |
5 | 👨💼 Great! It's nice to give our users a path forward. One thing you may have
6 | noticed is that if you enter in values into the form and there's an error, when
7 | you restore your form, the values are gone. This is because we're not
8 | persisting the values in the form. That's beyond the scope of this exercise, but
9 | it's something to keep in mind. If that's something important to your situation,
10 | you'll need to find a way to persist the form values and restore those values
11 | when the form is rendered again.
12 |
--------------------------------------------------------------------------------
/exercises/10.arrays/02.solution.focus-state/key.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 |
3 | const originalError = console.error
4 | const errors: Array = []
5 | console.error = (...args: any[]) => {
6 | errors.push(args[0])
7 | originalError(...args)
8 | }
9 |
10 | import './index.tsx'
11 |
12 | const { screen } = dtl
13 |
14 | await testStep('Wait for things to render', async () => {
15 | await screen.findByText('Without a key')
16 | })
17 |
18 | await testStep('Key prop is provided (no console errors)', async () => {
19 | try {
20 | expect(errors).toHaveLength(0)
21 | } finally {
22 | console.error = originalError
23 | }
24 | })
25 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/01.solution.compiler/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/exercises/07.forms/03.problem.types/README.mdx:
--------------------------------------------------------------------------------
1 | # Input Types
2 |
3 |
4 |
5 | 👨💼 We need quite a few more fields. Luckily the web platform has some great
6 | input types we can use. 📜 Check out all the
7 | [standard ` ` types here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input#input_types).
8 |
9 | For us, we need to add types for the password, photo, favorite color, and start
10 | date. Please add inputs for those fields.
11 |
12 |
13 | 🦉 You may notice a full page refresh when you submit the form. We'll talk
14 | about this in a future step.
15 |
16 |
--------------------------------------------------------------------------------
/exercises/06.styling/01.problem.style/README.mdx:
--------------------------------------------------------------------------------
1 | # Styling
2 |
3 |
4 |
5 | 👨💼 We have a CSS file that's being loaded on the
6 | page using a link tag:
7 |
8 | ```html
9 |
10 | ```
11 |
12 | In our isolated learning environment, the workshop app gets the styles on the
13 | page for us automatically. In a real-world app, you'll need to add the ` `
14 | tag yourself (or use a tool which does this for you).
15 |
16 | Your job is to apply the right `className` and `style` props to the divs so the
17 | styles applied match the text content.
18 |
--------------------------------------------------------------------------------
/exercises/10.arrays/01.solution.key-prop/key.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 |
3 | const originalError = console.error
4 | const errors: Array = []
5 | console.error = (...args: any[]) => {
6 | errors.push(args[0])
7 | originalError(...args)
8 | }
9 |
10 | import './index.tsx'
11 |
12 | const { screen } = dtl
13 |
14 | await testStep('Wait for Add Item button', async () => {
15 | await screen.findByRole('button', { name: /add item/i })
16 | })
17 |
18 | await testStep('Key prop is provided (no console errors)', async () => {
19 | try {
20 | expect(errors).toHaveLength(0)
21 | } finally {
22 | console.error = originalError
23 | }
24 | })
25 |
--------------------------------------------------------------------------------
/exercises/04.components/04.problem.props/README.mdx:
--------------------------------------------------------------------------------
1 | # Props
2 |
3 |
4 |
5 | 👨💼 You are the commander of your component's API. The API for your component is
6 | "props" which is the object your component function accepts as an argument.
7 |
8 | So now, we're going to use a `Calculator` component that can display an
9 | equation and its solution, like so:
10 |
11 | ```tsx
12 | element =
13 | // should render:
14 | //
15 | //
16 | // 1 + 2 = 3
17 | //
18 | //
19 | ```
20 |
21 | Let's get some more practice with custom components.
22 |
--------------------------------------------------------------------------------
/exercises/07.forms/01.solution.form/form.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 | const { screen } = dtl
3 |
4 | import './index.tsx'
5 |
6 | await testStep('Form is rendered', () => {
7 | return dtl.waitFor(() => {
8 | const form = document.querySelector('form')
9 | expect(form).toBeInTheDocument()
10 | return form
11 | })
12 | })
13 |
14 | await testStep('Username input is rendered', async () => {
15 | const usernameInput = await screen.findByLabelText(/username/i)
16 | expect(usernameInput).toHaveAttribute('name', 'username')
17 | })
18 |
19 | await testStep('Submit button is rendered', () =>
20 | screen.findByRole('button', { name: /submit/i }),
21 | )
22 |
--------------------------------------------------------------------------------
/exercises/07.forms/02.solution.action/form.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 | const { screen } = dtl
3 |
4 | import './index.tsx'
5 |
6 | await testStep('Form is rendered', () => {
7 | return dtl.waitFor(() => {
8 | const form = document.querySelector('form')
9 | expect(form).toBeInTheDocument()
10 | return form
11 | })
12 | })
13 |
14 | await testStep('Username input is rendered', async () => {
15 | const usernameInput = await screen.findByLabelText(/username/i)
16 | expect(usernameInput).toHaveAttribute('name', 'username')
17 | })
18 |
19 | await testStep('Submit button is rendered', () =>
20 | screen.findByRole('button', { name: /submit/i }),
21 | )
22 |
--------------------------------------------------------------------------------
/exercises/07.forms/03.problem.types/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | function App() {
4 | return (
5 |
19 | )
20 | }
21 |
22 | const rootEl = document.createElement('div')
23 | document.body.append(rootEl)
24 | createRoot(rootEl).render( )
25 |
--------------------------------------------------------------------------------
/exercises/08.inputs/01.problem.checkbox/README.mdx:
--------------------------------------------------------------------------------
1 | # Checkbox
2 |
3 |
4 |
5 | 👨💼 We need to add a checkbox for whether the user has already signed a waiver.
6 |
7 | For checkboxes, typically they appear on the left side of the label and in this
8 | case we can even have the checkbox be _inside_ the label which means we don't
9 | need to worry about the `for` and `id` attributes which is nice.
10 |
11 | Please add the checkbox to the form.
12 |
13 | Once you've done that, make sure to submit the form with the checkbox checked
14 | and without it checked so you can familiarize yourself with the difference in
15 | form data because it's kinda funny.
16 |
--------------------------------------------------------------------------------
/exercises/02.raw-react/02.solution.nesting/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/03.solution.spread/content.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 |
3 | await testStep('"Hello World" is rendered to the DOM', async () => {
4 | const rootElement = document.getElementById('root')
5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement)
6 |
7 | const element = await dtl.waitFor(
8 | async () => {
9 | const element = rootElement!.querySelector('.container')
10 | expect(element, 'container element not found').to.be.instanceOf(
11 | HTMLElement,
12 | )
13 | return element
14 | },
15 | { timeout: 5000 },
16 | )
17 |
18 | expect(element!.textContent, 'element text is not correct').to.equal(
19 | 'Hello World',
20 | )
21 | })
22 |
--------------------------------------------------------------------------------
/exercises/01.js-hello-world/01.solution.hello/index.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 |
3 | await testStep('"Hello World" is rendered to the DOM', async () => {
4 | const rootElement = document.getElementById('root')
5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement)
6 |
7 | const element = await dtl.waitFor(
8 | async () => {
9 | const element = rootElement!.querySelector('.container')
10 | expect(element, 'container element not found').to.be.instanceOf(
11 | HTMLElement,
12 | )
13 | return element
14 | },
15 | { timeout: 5000 },
16 | )
17 |
18 | expect(element!.textContent, 'element text is not correct').to.equal(
19 | 'Hello World',
20 | )
21 | })
22 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/01.solution.compiler/content.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 |
3 | await testStep('"Hello World" is rendered to the DOM', async () => {
4 | const rootElement = document.getElementById('root')
5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement)
6 |
7 | const element = await dtl.waitFor(
8 | async () => {
9 | const element = rootElement!.querySelector('.container')
10 | expect(element, 'container element not found').to.be.instanceOf(
11 | HTMLElement,
12 | )
13 | return element
14 | },
15 | { timeout: 5000 },
16 | )
17 |
18 | expect(element!.textContent, 'element text is not correct').to.equal(
19 | 'Hello World',
20 | )
21 | })
22 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/02.solution.interpolation/content.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 |
3 | await testStep('"Hello World" is rendered to the DOM', async () => {
4 | const rootElement = document.getElementById('root')
5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement)
6 |
7 | const element = await dtl.waitFor(
8 | async () => {
9 | const element = rootElement!.querySelector('.container')
10 | expect(element, 'container element not found').to.be.instanceOf(
11 | HTMLElement,
12 | )
13 | return element
14 | },
15 | { timeout: 5000 },
16 | )
17 |
18 | expect(element!.textContent, 'element text is not correct').to.equal(
19 | 'Hello World',
20 | )
21 | })
22 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/01.problem.compiler/README.mdx:
--------------------------------------------------------------------------------
1 | # Compiling JSX
2 |
3 |
4 |
5 |
6 | Normally you'll compile all of your code at build-time before you ship your
7 | application to the browser, but because Babel is written in JavaScript we can
8 | actually run it _in_ the browser to compile our code on the fly and that's
9 | what we'll do in this exercise.
10 |
11 |
12 | 👨💼 Let's add a script tag for Babel, then we'll update our own script tag to
13 | tell Babel to compile it for us on the fly. When you're done, you should notice
14 | the compiled version of the code appears in the `` of the DOM (which you
15 | can inspect using DevTools).
16 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/02.solution.interpolation/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/03.solution.spread/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/epicshop/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:24-bookworm-slim as base
2 |
3 | RUN apt-get update && apt-get install -y git
4 |
5 | ENV EPICSHOP_GITHUB_REPO=https://github.com/epicweb-dev/react-fundamentals
6 | ENV EPICSHOP_CONTEXT_CWD="/myapp/workshop-content"
7 | ENV EPICSHOP_HOME_DIR="/myapp/.epicshop"
8 | ENV EPICSHOP_DEPLOYED="true"
9 | ENV EPICSHOP_DISABLE_WATCHER="true"
10 | ENV FLY="true"
11 | ENV PORT="8080"
12 | ENV NODE_ENV="production"
13 |
14 | WORKDIR /myapp
15 |
16 | # Clone the workshop repo during build time, excluding database files
17 | RUN git clone --depth 1 ${EPICSHOP_GITHUB_REPO} ${EPICSHOP_CONTEXT_CWD}
18 |
19 | ADD . .
20 |
21 | RUN npm install --omit=dev
22 |
23 | RUN cd ${EPICSHOP_CONTEXT_CWD} && \
24 | npx epicshop warm
25 |
26 | CMD cd ${EPICSHOP_CONTEXT_CWD} && \
27 | npx epicshop start
28 |
--------------------------------------------------------------------------------
/exercises/07.forms/05.problem.action/README.mdx:
--------------------------------------------------------------------------------
1 | # Form Actions
2 |
3 |
4 |
5 | 👨💼 `onSubmit` handlers that prevent default and handle file uploads like this
6 | are so common that React has a built-in way to do that! The `action` prop on a
7 | form can accept a function! The function accepts a `formData` object.
8 |
9 |
10 | Note that in HTML, attributes cannot accept functions. The action prop
11 | accepting a function is a React feature.
12 |
13 |
14 | In this exercise, create a function for logging the `formData` like we are
15 | currently and delete all the superfluous props.
16 |
17 | 📜 [`` React docs](https://react.dev/reference/react-dom/components/form)
18 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/02.problem.interpolation/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/05.solution.fragments/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/exercises/09.errors/02.solution.show-boundary/error-boundary.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 | const { screen } = dtl
3 | import { userEvent } from '@testing-library/user-event'
4 |
5 | const user = userEvent.setup()
6 |
7 | import './index.tsx'
8 |
9 | await testStep('Error boundary is rendered after form submission', async () => {
10 | // submit the form
11 | await user.click(await screen.findByRole('button', { name: /submit/i }))
12 |
13 | // Check if the error message is displayed
14 | const errorMessage = await screen.findByRole('alert')
15 | expect(errorMessage).toBeDefined()
16 | expect(errorMessage.textContent).toContain('There was an error:')
17 |
18 | // Ensure the form is not rendered after error
19 | const form = screen.queryByRole('form')
20 | expect(form).toBeNull()
21 | })
22 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/03.problem.spread/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/04.solution.nesting/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/exercises/06.styling/03.problem.size-prop/README.mdx:
--------------------------------------------------------------------------------
1 | # Size Prop
2 |
3 |
4 |
5 | 👨💼 It's great that we're composing the `className`s and `style`s properly, but
6 | wouldn't it be better if the users of our components didn't have to worry about
7 | which class name to apply for a given effect? Or that a class name is involved
8 | at all? I think it would be better if users of our component had a `size` prop
9 | and our component took care of making the box that size.
10 |
11 | In this step, try to make this API work:
12 |
13 | ```tsx
14 |
15 | small lightblue box
16 |
17 | ```
18 |
19 | - You'll need 📜
20 | [TypeScript's intersection (&) operator](https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types)
21 |
--------------------------------------------------------------------------------
/exercises/02.raw-react/03.solution.deep-nesting/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/exercises/01.js-hello-world/02.problem.root/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/05.solution.fragments/index.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep } from '@epic-web/workshop-utils/test'
2 |
3 | const response = await fetch(location.href)
4 | const indexHtml = await response.text()
5 | const node = document.createElement('div')
6 | node.innerHTML = indexHtml
7 |
8 | const inlineScript = node.querySelector('script[type="text/babel"]')
9 | if (!inlineScript) {
10 | throw new Error('inlineScript not found')
11 | }
12 |
13 | await testStep('fragments are used', async () => {
14 | const usesShorthand = inlineScript.textContent?.includes('<>')
15 | const usesLonghand = inlineScript.textContent?.includes('')
16 | const usesFragment = inlineScript.textContent?.includes('')
17 | expect(
18 | usesShorthand || usesLonghand || usesFragment,
19 | 'fragments are not used. You must use <>, , or ',
20 | ).to.be.true
21 | })
22 |
--------------------------------------------------------------------------------
/exercises/07.forms/03.solution.types/README.mdx:
--------------------------------------------------------------------------------
1 | # Input Types
2 |
3 |
4 |
5 | 👨💼 Great! Now our users will have more fields available to them as they're
6 | filling out this form. But there's something weird going on with the form
7 | submission so let's look at that next.
8 |
9 | 🦉 While the web platform has a lot of really great input types, not all of them
10 | match the style you're going for in your app. And these can be difficult (or in
11 | some cases even impossible) to style. There are efforts in standards bodies to
12 | solve these problems, but until then, you should look into great UI libraries
13 | like [Radix-UI](https://www.radix-ui.com/) and
14 | [React Aria Components](https://react-spectrum.adobe.com/react-aria) which
15 | handle these issues and allow you to customize the style much more easily.
16 |
--------------------------------------------------------------------------------
/exercises/04.components/01.solution.function/index.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep } from '@epic-web/workshop-utils/test'
2 |
3 | const response = await fetch(location.href)
4 | const indexHtml = await response.text()
5 | const node = document.createElement('div')
6 | node.innerHTML = indexHtml
7 |
8 | const inlineScript = node.querySelector('script[type="text/babel"]')
9 | if (!inlineScript) {
10 | throw new Error('inlineScript not found')
11 | }
12 |
13 | await testStep('message function is created', async () => {
14 | const functionForm = inlineScript.textContent?.includes('function message')
15 | const declarationForm = inlineScript.textContent?.includes('const message = ')
16 | expect(functionForm || declarationForm, 'message function must be declared')
17 | .to.be.true
18 | })
19 |
20 | await testStep('message function is called', async () => {
21 | expect(inlineScript.textContent).to.include('message(')
22 | })
23 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/05.problem.fragments/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/exercises/04.components/03.solution.jsx/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/exercises/02.raw-react/02.problem.nesting/README.mdx:
--------------------------------------------------------------------------------
1 | # Nesting Elements
2 |
3 |
4 |
5 | 👨💼 See if you can figure out how to write the JavaScript + React code to
6 | generate this DOM output:
7 |
8 | ```html
9 |
10 | Hello
11 | World
12 |
13 | ```
14 |
15 | Hint: You can either use the `children` prop or additional arguments after the
16 | props. If you use the `children` prop, you will get a warning in the developer
17 | console about needing a "key" prop. We'll get to that later.
18 |
19 | Tip: You'll use `createElement` for the `span` elements, but to get the
20 | space between them, you'll need to use a string (`' '`) to tell React to create
21 | a [`textNode`](https://developer.mozilla.org/en-US/docs/Web/API/Text).
22 |
23 | - [📜 `createElement`](https://react.dev/reference/react/createElement#createelement)
24 |
--------------------------------------------------------------------------------
/exercises/06.styling/02.solution.component/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | export function Box({
4 | style = {},
5 | className = '',
6 | ...otherProps
7 | }: React.ComponentProps<'div'>) {
8 | return (
9 |
14 | )
15 | }
16 |
17 | function App() {
18 | return (
19 |
20 |
21 | small lightblue box
22 |
23 |
24 | medium pink box
25 |
26 |
27 | large orange box
28 |
29 | sizeless colorless box
30 |
31 | )
32 | }
33 |
34 | const rootEl = document.createElement('div')
35 | document.body.append(rootEl)
36 | createRoot(rootEl).render( )
37 |
--------------------------------------------------------------------------------
/exercises/08.inputs/04.solution.hidden/README.mdx:
--------------------------------------------------------------------------------
1 | # Hidden Inputs
2 |
3 |
4 |
5 | 👨💼 Great! Now when the user submits the form, this extra information will be
6 | available in the submission without the user needing to supply it. Well done!
7 |
8 | 🦉 A common question here is whether you can supply complex data structures as
9 | hidden input values, and you definitely can. You can either provide multiple
10 | hidden inputs, or just stick JSON in your hidden input like so:
11 |
12 | ```tsx
13 |
18 | ```
19 |
20 | Then the backend could parse that JSON from the `order` form field. This can be
21 | very useful if you're using a library that doesn't render form elements, but you
22 | want to send the user's selections to the backend as a part of your form using
23 | your own form elements.
24 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/02.solution.interpolation/index.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep } from '@epic-web/workshop-utils/test'
2 |
3 | const response = await fetch(location.href)
4 | const indexHtml = await response.text()
5 | const node = document.createElement('div')
6 | node.innerHTML = indexHtml
7 |
8 | const inlineScript = node.querySelector('script[type="text/babel"]')
9 | if (!inlineScript) {
10 | throw new Error('inlineScript not found')
11 | }
12 |
13 | await testStep('className is interpolated', async () => {
14 | expect(inlineScript.textContent).to.include('className={')
15 | expect(inlineScript.textContent).not.to.include('className="')
16 | })
17 |
18 | await testStep('children is interpolated', async () => {
19 | expect(
20 | inlineScript.textContent,
21 | 'children should be interpolated',
22 | ).not.to.include('>Hello World<')
23 | expect(
24 | inlineScript.textContent,
25 | 'expected script to include { children }',
26 | ).to.match(/{\s*children\s*}/)
27 | })
28 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/04.problem.nesting/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/03.problem.spread/README.mdx:
--------------------------------------------------------------------------------
1 | # Spread props
2 |
3 |
4 |
5 | What if I have an object of props that I want applied to the `div` like this:
6 |
7 | ```tsx
8 | const children = 'Hello World'
9 | const className = 'container'
10 | const props = { children, className }
11 | const element =
// how do I apply props to this div?
12 | ```
13 |
14 | If we were doing raw React APIs it would be:
15 |
16 | ```tsx
17 | const element = createElement('div', props)
18 | ```
19 |
20 | Or, it could be written like this:
21 |
22 | ```tsx
23 | const element = createElement('div', { ...props })
24 | ```
25 |
26 | 👨💼 See if you can figure out how to make that work with JSX. Take an object of
27 | props, and apply those props to a React element.
28 |
29 | 📜
30 | [Forwarding props with the JSX spread syntax](https://react.dev/learn/passing-props-to-a-component#forwarding-props-with-the-jsx-spread-syntax)
31 |
--------------------------------------------------------------------------------
/epicshop/playwright.config.js:
--------------------------------------------------------------------------------
1 | import os from 'os'
2 | import path from 'path'
3 | import { defineConfig, devices } from '@playwright/test'
4 |
5 | const PORT = process.env.PORT || '5639'
6 | const tmpDir = path.join(
7 | os.tmpdir(),
8 | 'epicshop-playwright',
9 | path.basename(new URL('../', import.meta.url).pathname),
10 | )
11 |
12 | export default defineConfig({
13 | outputDir: path.join(tmpDir, 'playwright-test-output'),
14 | reporter: [
15 | [
16 | 'html',
17 | { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') },
18 | ],
19 | ],
20 | use: {
21 | baseURL: `http://localhost:${PORT}/`,
22 | trace: 'retain-on-failure',
23 | },
24 |
25 | projects: [
26 | {
27 | name: 'chromium',
28 | use: { ...devices['Desktop Chrome'] },
29 | },
30 | ],
31 |
32 | webServer: {
33 | command: 'cd .. && npm start',
34 | port: Number(PORT),
35 | reuseExistingServer: !process.env.CI,
36 | stdout: 'pipe',
37 | stderr: 'pipe',
38 | env: { PORT },
39 | },
40 | })
41 |
--------------------------------------------------------------------------------
/exercises/04.components/01.solution.function/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/epicshop/setup-custom.js:
--------------------------------------------------------------------------------
1 | import path from 'node:path'
2 | import {
3 | getApps,
4 | isProblemApp,
5 | setPlayground,
6 | } from '@epic-web/workshop-utils/apps.server'
7 | import { warm } from 'epicshop/warm'
8 | import fsExtra from 'fs-extra'
9 |
10 | await warm()
11 |
12 | const allApps = await getApps()
13 | const problemApps = allApps.filter(isProblemApp)
14 |
15 | if (!process.env.SKIP_PLAYGROUND) {
16 | const firstProblemApp = problemApps[0]
17 | if (firstProblemApp) {
18 | console.log('🛝 setting up the first problem app...')
19 | const playgroundPath = path.join(process.cwd(), 'playground')
20 | if (await fsExtra.exists(playgroundPath)) {
21 | console.log('🗑 deleting existing playground app')
22 | await fsExtra.remove(playgroundPath)
23 | }
24 | await setPlayground(firstProblemApp.fullPath).then(
25 | () => {
26 | console.log('✅ first problem app set up')
27 | },
28 | (error) => {
29 | console.error(error)
30 | throw new Error('❌ first problem app setup failed')
31 | },
32 | )
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/exercises/04.components/02.solution.raw/content.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 |
3 | await testStep('Proper elements are rendered to the DOM', async () => {
4 | const rootElement = document.getElementById('root')
5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement)
6 | if (!rootElement) return
7 |
8 | const element = await dtl.waitFor(
9 | () => {
10 | const element = rootElement!.querySelector('.container')
11 | expect(element, 'container element not found').to.be.instanceOf(
12 | HTMLElement,
13 | )
14 | return element
15 | },
16 | { timeout: 5000 },
17 | )
18 |
19 | if (!element) return
20 |
21 | const messages = Array.from(element.querySelectorAll('.message'))
22 | expect(messages, 'messages not found').to.have.length(2)
23 | const [helloMessage, goodbyeMessage] = messages
24 | expect(helloMessage?.textContent?.toLowerCase()).to.equal('hello world')
25 | expect(goodbyeMessage?.textContent?.toLowerCase()).to.equal('goodbye world')
26 | })
27 |
--------------------------------------------------------------------------------
/exercises/04.components/03.solution.jsx/content.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 |
3 | await testStep('Proper elements are rendered to the DOM', async () => {
4 | const rootElement = document.getElementById('root')
5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement)
6 | if (!rootElement) return
7 |
8 | const element = await dtl.waitFor(
9 | () => {
10 | const element = rootElement!.querySelector('.container')
11 | expect(element, 'container element not found').to.be.instanceOf(
12 | HTMLElement,
13 | )
14 | return element
15 | },
16 | { timeout: 5000 },
17 | )
18 |
19 | if (!element) return
20 |
21 | const messages = Array.from(element.querySelectorAll('.message'))
22 | expect(messages, 'messages not found').to.have.length(2)
23 | const [helloMessage, goodbyeMessage] = messages
24 | expect(helloMessage?.textContent?.toLowerCase()).to.equal('hello world')
25 | expect(goodbyeMessage?.textContent?.toLowerCase()).to.equal('goodbye world')
26 | })
27 |
--------------------------------------------------------------------------------
/exercises/02.raw-react/02.problem.nesting/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/exercises/04.components/01.solution.function/content.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 |
3 | await testStep('Proper elements are rendered to the DOM', async () => {
4 | const rootElement = document.getElementById('root')
5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement)
6 | if (!rootElement) return
7 |
8 | const element = await dtl.waitFor(
9 | () => {
10 | const element = rootElement!.querySelector('.container')
11 | expect(element, 'container element not found').to.be.instanceOf(
12 | HTMLElement,
13 | )
14 | return element
15 | },
16 | { timeout: 5000 },
17 | )
18 |
19 | if (!element) return
20 |
21 | const messages = Array.from(element.querySelectorAll('.message'))
22 | expect(messages, 'messages not found').to.have.length(2)
23 | const [helloMessage, goodbyeMessage] = messages
24 | expect(helloMessage?.textContent?.toLowerCase()).to.equal('hello world')
25 | expect(goodbyeMessage?.textContent?.toLowerCase()).to.equal('goodbye world')
26 | })
27 |
--------------------------------------------------------------------------------
/exercises/04.components/02.solution.raw/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/exercises/02.raw-react/03.problem.deep-nesting/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/exercises/04.components/01.problem.function/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/exercises/08.inputs/README.mdx:
--------------------------------------------------------------------------------
1 | # Inputs
2 |
3 |
4 |
5 | There are lots of different types of form elements built-into the browser you
6 | can use (of varying degrees of usefulness because of the way they are designed).
7 | [Find a list of standard input types here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input).
8 |
9 | In general, you use these form elements in React the same way you use them in
10 | regular HTML, but there are a few exceptions (especially regarding default
11 | values). So we're going to explore all the types of form elements in this
12 | exercise.
13 |
14 | There are a number of ways you can be notified of changes to form elements.
15 | We'll not be covering them in this workshop (we'll get into it more in a future
16 | workshop when we talk about managing state), but if you just can't wait, play
17 | around with `onChange` (which behaves like
18 | [the browser's `input` event](https://developer.mozilla.org/en-US/docs/Web/API/Element/input_event)).
19 |
--------------------------------------------------------------------------------
/exercises/04.components/03.problem.jsx/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/exercises/04.components/02.problem.raw/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/exercises/06.styling/01.solution.style/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | const smallBox = (
4 |
8 | small lightblue box
9 |
10 | )
11 | const mediumBox = (
12 |
16 | medium pink box
17 |
18 | )
19 | const largeBox = (
20 |
24 | large orange box
25 |
26 | )
27 | const sizelessColorlessBox = (
28 |
29 | sizeless colorless box
30 |
31 | )
32 |
33 | function App() {
34 | return (
35 |
36 | {smallBox}
37 | {mediumBox}
38 | {largeBox}
39 | {sizelessColorlessBox}
40 |
41 | )
42 | }
43 |
44 | const rootEl = document.createElement('div')
45 | document.body.append(rootEl)
46 | createRoot(rootEl).render( )
47 |
--------------------------------------------------------------------------------
/exercises/04.components/04.solution.props/content.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 |
3 | await testStep('Proper elements are rendered to the DOM', async () => {
4 | const rootElement = document.getElementById('root')
5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement)
6 | if (!rootElement) return
7 |
8 | const element = rootElement
9 |
10 | await dtl.waitFor(
11 | () => {
12 | const h1 = element.querySelector('h1')
13 | expect(h1, 'h1 not found').to.be.instanceOf(HTMLElement)
14 | expect(h1!.textContent).to.equal('Calculator')
15 | },
16 | { timeout: 5000 },
17 | )
18 |
19 | const codeElements = Array.from(element.querySelectorAll('code'))
20 | expect(codeElements, 'code elements not found').to.have.length(4)
21 |
22 | const [plus, minus, times, divide] = codeElements
23 |
24 | expect(plus?.textContent).to.equal('1 + 2 = 3')
25 | expect(minus?.textContent).to.equal('1 - 2 = -1')
26 | expect(times?.textContent).to.equal('1 * 2 = 2')
27 | expect(divide?.textContent).to.equal('1 / 2 = 0.5')
28 | })
29 |
--------------------------------------------------------------------------------
/exercises/06.styling/03.solution.size-prop/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | export function Box({
4 | size,
5 | ...otherProps
6 | }: {
7 | size?: 'small' | 'medium' | 'large'
8 | } & React.ComponentProps<'div'>) {
9 | const sizeClassName = size ? `box--${size}` : ''
10 | return (
11 |
18 | )
19 | }
20 |
21 | function App() {
22 | return (
23 |
24 |
25 | small lightblue box
26 |
27 |
28 | medium pink box
29 |
30 |
31 | large orange box
32 |
33 | sizeless colorless box
34 |
35 | )
36 | }
37 |
38 | const rootEl = document.createElement('div')
39 | document.body.append(rootEl)
40 | createRoot(rootEl).render( )
41 |
--------------------------------------------------------------------------------
/exercises/01.js-hello-world/01.problem.hello/README.mdx:
--------------------------------------------------------------------------------
1 | # Hello World in JS
2 |
3 |
4 |
5 | 👨💼 It's important to have a basic understanding of how to generate and interact
6 | with DOM nodes using JavaScript because it will help you understand how React
7 | works under the hood a little better. So in this exercise we're actually not
8 | going to use React at all. Instead we're going to use JavaScript to create a
9 | `div` DOM node with the text "Hello World" and insert that DOM node into the
10 | document.
11 |
12 | 🐨 We'll be in to help guide you through making
13 | this work! See you there!
14 |
15 | 👨💼 Once you're finished, open up the
16 | [browser devtools](https://developer.chrome.com/docs/devtools) so
17 | you can check the DOM is what you expect it to be.
18 |
19 | {/* prettier-ignore */}
20 |
21 | 💡 Tip: you may find it useful to{' '}open the playground in a separate tab {'.'}
22 |
23 |
--------------------------------------------------------------------------------
/epicshop/fly.yaml:
--------------------------------------------------------------------------------
1 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file.
2 | #
3 |
4 | app: 'epicweb-dev-react-fundamentals'
5 | primary_region: sjc
6 | kill_signal: SIGINT
7 | kill_timeout: 5s
8 | swap_size_mb: 512
9 |
10 | experimental:
11 | auto_rollback: true
12 |
13 | attached:
14 | secrets: {}
15 |
16 | services:
17 | - processes:
18 | - app
19 | protocol: tcp
20 | internal_port: 8080
21 |
22 | ports:
23 | - port: 80
24 |
25 | handlers:
26 | - http
27 | force_https: true
28 | - port: 443
29 |
30 | handlers:
31 | - tls
32 | - http
33 |
34 | concurrency:
35 | type: connections
36 | hard_limit: 100
37 | soft_limit: 80
38 |
39 | tcp_checks:
40 | - interval: 15s
41 | timeout: 2s
42 | grace_period: 1s
43 |
44 | http_checks:
45 | - interval: 10s
46 | timeout: 2s
47 | grace_period: 5s
48 | method: get
49 | path: /resources/healthcheck
50 | protocol: http
51 | tls_skip_verify: false
52 |
--------------------------------------------------------------------------------
/exercises/01.js-hello-world/02.solution.root/index.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 |
3 | await testStep('"Hello World" is rendered to the DOM', async () => {
4 | const rootElement = document.getElementById('root')
5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement)
6 |
7 | const element = await dtl.waitFor(
8 | async () => {
9 | const element = rootElement!.querySelector('.container')
10 | expect(element, 'container element not found').to.be.instanceOf(
11 | HTMLElement,
12 | )
13 | return element
14 | },
15 | { timeout: 5000 },
16 | )
17 |
18 | expect(element!.textContent, 'element text is not correct').to.equal(
19 | 'Hello World',
20 | )
21 | })
22 |
23 | await testStep('root element is not in the HTML', async () => {
24 | const response = await fetch(location.href)
25 | const text = await response.text()
26 | const node = document.createElement('div')
27 | node.innerHTML = text
28 | expect(
29 | node.querySelector('#root'),
30 | 'root element found in the HTML when it should not be',
31 | ).to.be.null
32 | })
33 |
--------------------------------------------------------------------------------
/exercises/04.components/02.problem.raw/README.mdx:
--------------------------------------------------------------------------------
1 | # Raw API
2 |
3 |
4 |
5 | 👨💼 So far we've only used `createElement('someString')`, but the first
6 | argument to `createElement` can also be a function which returns something
7 | that's renderable.
8 |
9 | So instead of calling your `message` function, pass it as the first argument to
10 | `createElement` and pass the `{children: 'Hello World'}` object as the
11 | second argument.
12 |
13 | ```ts
14 | createElement(
15 | someFunction,
16 | { prop1: 'value1', prop2: 'value2' },
17 | 'child1',
18 | 'child2',
19 | )
20 | ```
21 |
22 | Then `someFunction` will be called with the props object as the first argument
23 | and the children will appear as an array in the children property of the props
24 | object.
25 |
26 | ```ts
27 | function someFunction(props) {
28 | props.children // ['child1', 'child2']
29 | props.prop1 // 'value1'
30 | props.prop2 // 'value2'
31 | return // some jsx
32 | }
33 | ```
34 |
35 | So let's move from calling `message` directly to calling it through
36 | `createElement`.
37 |
--------------------------------------------------------------------------------
/exercises/05.typescript/01.solution.props/calculator.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 | const { screen } = dtl
3 |
4 | import './index.tsx'
5 |
6 | await testStep('Calculator h1 is rendered', () =>
7 | screen.findByText('Calculator'),
8 | )
9 |
10 | const calculators = await testStep('Code elements are rendered', async () => {
11 | const elements = await screen.findAllByRole('code')
12 | expect(elements).toHaveLength(4)
13 | return elements
14 | })
15 |
16 | const [add, subtract, multiply, divide] = calculators
17 |
18 | await testStep('Addition calculation is rendered correctly', async () => {
19 | expect(add).toHaveTextContent('1 + 2 = 3')
20 | })
21 |
22 | await testStep('Subtraction calculation is rendered correctly', async () => {
23 | expect(subtract).toHaveTextContent('1 - 2 = -1')
24 | })
25 |
26 | await testStep('Multiplication calculation is rendered correctly', async () => {
27 | expect(multiply).toHaveTextContent('1 * 2 = 2')
28 | })
29 |
30 | await testStep('Division calculation is rendered correctly', async () => {
31 | expect(divide).toHaveTextContent('1 / 2 = 0.5')
32 | })
33 |
--------------------------------------------------------------------------------
/exercises/05.typescript/02.solution.narrow/calculator.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 | const { screen } = dtl
3 |
4 | import './index.tsx'
5 |
6 | await testStep('Calculator h1 is rendered', () =>
7 | screen.findByText('Calculator'),
8 | )
9 |
10 | const calculators = await testStep('Code elements are rendered', async () => {
11 | const elements = await screen.findAllByRole('code')
12 | expect(elements).toHaveLength(4)
13 | return elements
14 | })
15 |
16 | const [add, subtract, multiply, divide] = calculators
17 |
18 | await testStep('Addition calculation is rendered correctly', async () => {
19 | expect(add).toHaveTextContent('1 + 2 = 3')
20 | })
21 |
22 | await testStep('Subtraction calculation is rendered correctly', async () => {
23 | expect(subtract).toHaveTextContent('1 - 2 = -1')
24 | })
25 |
26 | await testStep('Multiplication calculation is rendered correctly', async () => {
27 | expect(multiply).toHaveTextContent('1 * 2 = 2')
28 | })
29 |
30 | await testStep('Division calculation is rendered correctly', async () => {
31 | expect(divide).toHaveTextContent('1 / 2 = 0.5')
32 | })
33 |
--------------------------------------------------------------------------------
/exercises/05.typescript/03.solution.derive/calculator.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 | const { screen } = dtl
3 |
4 | import './index.tsx'
5 |
6 | await testStep('Calculator h1 is rendered', () =>
7 | screen.findByText('Calculator'),
8 | )
9 |
10 | const calculators = await testStep('Code elements are rendered', async () => {
11 | const elements = await screen.findAllByRole('code')
12 | expect(elements).toHaveLength(4)
13 | return elements
14 | })
15 |
16 | const [add, subtract, multiply, divide] = calculators
17 |
18 | await testStep('Addition calculation is rendered correctly', async () => {
19 | expect(add).toHaveTextContent('1 + 2 = 3')
20 | })
21 |
22 | await testStep('Subtraction calculation is rendered correctly', async () => {
23 | expect(subtract).toHaveTextContent('1 - 2 = -1')
24 | })
25 |
26 | await testStep('Multiplication calculation is rendered correctly', async () => {
27 | expect(multiply).toHaveTextContent('1 * 2 = 2')
28 | })
29 |
30 | await testStep('Division calculation is rendered correctly', async () => {
31 | expect(divide).toHaveTextContent('1 / 2 = 0.5')
32 | })
33 |
--------------------------------------------------------------------------------
/exercises/05.typescript/06.solution.satisfies/calculator.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 | const { screen } = dtl
3 |
4 | import './index.tsx'
5 |
6 | await testStep('Calculator h1 is rendered', () =>
7 | screen.findByText('Calculator'),
8 | )
9 |
10 | const calculators = await testStep('Code elements are rendered', async () => {
11 | const elements = await screen.findAllByRole('code')
12 | expect(elements).toHaveLength(4)
13 | return elements
14 | })
15 |
16 | const [add, subtract, multiply, divide] = calculators
17 |
18 | await testStep('Addition calculation is rendered correctly', async () => {
19 | expect(add).toHaveTextContent('1 + 2 = 3')
20 | })
21 |
22 | await testStep('Subtraction calculation is rendered correctly', async () => {
23 | expect(subtract).toHaveTextContent('0 - 0 = 0')
24 | })
25 |
26 | await testStep('Multiplication calculation is rendered correctly', async () => {
27 | expect(multiply).toHaveTextContent('1 * 0 = 0')
28 | })
29 |
30 | await testStep('Division calculation is rendered correctly', async () => {
31 | expect(divide).toHaveTextContent('0 / 2 = 0')
32 | })
33 |
--------------------------------------------------------------------------------
/exercises/05.typescript/04.solution.default-props/calculator.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 | const { screen } = dtl
3 |
4 | import './index.tsx'
5 |
6 | await testStep('Calculator h1 is rendered', () =>
7 | screen.findByText('Calculator'),
8 | )
9 |
10 | const calculators = await testStep('Code elements are rendered', async () => {
11 | const elements = await screen.findAllByRole('code')
12 | expect(elements).toHaveLength(4)
13 | return elements
14 | })
15 |
16 | const [add, subtract, multiply, divide] = calculators
17 |
18 | await testStep('Addition calculation is rendered correctly', async () => {
19 | expect(add).toHaveTextContent('1 + 2 = 3')
20 | })
21 |
22 | await testStep('Subtraction calculation is rendered correctly', async () => {
23 | expect(subtract).toHaveTextContent('0 - 0 = 0')
24 | })
25 |
26 | await testStep('Multiplication calculation is rendered correctly', async () => {
27 | expect(multiply).toHaveTextContent('1 * 0 = 0')
28 | })
29 |
30 | await testStep('Division calculation is rendered correctly', async () => {
31 | expect(divide).toHaveTextContent('0 / 2 = 0')
32 | })
33 |
--------------------------------------------------------------------------------
/exercises/05.typescript/05.solution.function-types/calculator.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 | const { screen } = dtl
3 |
4 | import './index.tsx'
5 |
6 | await testStep('Calculator h1 is rendered', () =>
7 | screen.findByText('Calculator'),
8 | )
9 |
10 | const calculators = await testStep('Code elements are rendered', async () => {
11 | const elements = await screen.findAllByRole('code')
12 | expect(elements).toHaveLength(4)
13 | return elements
14 | })
15 |
16 | const [add, subtract, multiply, divide] = calculators
17 |
18 | await testStep('Addition calculation is rendered correctly', async () => {
19 | expect(add).toHaveTextContent('1 + 2 = 3')
20 | })
21 |
22 | await testStep('Subtraction calculation is rendered correctly', async () => {
23 | expect(subtract).toHaveTextContent('0 - 0 = 0')
24 | })
25 |
26 | await testStep('Multiplication calculation is rendered correctly', async () => {
27 | expect(multiply).toHaveTextContent('1 * 0 = 0')
28 | })
29 |
30 | await testStep('Division calculation is rendered correctly', async () => {
31 | expect(divide).toHaveTextContent('0 / 2 = 0')
32 | })
33 |
--------------------------------------------------------------------------------
/exercises/04.components/01.problem.function/README.mdx:
--------------------------------------------------------------------------------
1 | # Simple Function
2 |
3 |
4 |
5 | 👨💼 The DOM we want to generate is like this:
6 |
7 | ```html
8 |
9 |
Hello World
10 |
Goodbye World
11 |
12 | ```
13 |
14 | In this case, it would be cool if we could reduce the duplication for creating
15 | the React elements for this:
16 |
17 | ```tsx
18 | {children}
19 | ```
20 |
21 | So we need to make a function which accepts an object argument with a `children`
22 | property and returns the React element. Then you can interpolate a call to that
23 | function in your JSX.
24 |
25 | ```tsx
26 | {message('Hello World')}
27 | ```
28 |
29 | This is not how we write custom React components, but this is important for you
30 | to understand them. We'll get to custom components in the next steps.
31 |
32 | 📜 Read more
33 |
34 | - [JavaScript in JSX with Curly Braces](https://react.dev/learn/javascript-in-jsx-with-curly-braces)
35 | - [What is JSX?](https://kentcdodds.com/blog/what-is-jsx)
36 |
--------------------------------------------------------------------------------
/exercises/05.typescript/04.solution.default-props/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | const operations = {
4 | '+': (left: number, right: number): number => left + right,
5 | '-': (left: number, right: number): number => left - right,
6 | '*': (left: number, right: number): number => left * right,
7 | '/': (left: number, right: number): number => left / right,
8 | }
9 |
10 | type CalculatorProps = {
11 | left?: number
12 | operator?: keyof typeof operations
13 | right?: number
14 | }
15 | function Calculator({ left = 0, operator = '+', right = 0 }: CalculatorProps) {
16 | const result = operations[operator](left, right)
17 | return (
18 |
19 |
20 | {left} {operator} {right} = {result}
21 |
22 |
23 | )
24 | }
25 |
26 | function App() {
27 | return (
28 |
29 |
Calculator
30 |
31 |
32 |
33 |
34 |
35 | )
36 | }
37 |
38 | const rootEl = document.createElement('div')
39 | document.body.append(rootEl)
40 | createRoot(rootEl).render( )
41 |
--------------------------------------------------------------------------------
/exercises/02.raw-react/01.solution.elements/index.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 |
3 | await testStep('"Hello World" is rendered to the DOM', async () => {
4 | const rootElement = document.getElementById('root')
5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement)
6 |
7 | const element = await dtl.waitFor(
8 | async () => {
9 | const element = rootElement!.querySelector('.container')
10 | expect(element, 'container element not found').to.be.instanceOf(
11 | HTMLElement,
12 | )
13 | return element
14 | },
15 | { timeout: 5000 },
16 | )
17 |
18 | expect(element!.textContent, 'element text is not correct').to.equal(
19 | 'Hello World',
20 | )
21 | })
22 |
23 | await testStep('The DOM element is created by React', () => {
24 | const element = document.querySelector('#root .container')
25 | expect(element, 'element not found').to.be.instanceOf(HTMLElement)
26 | if (!element) return
27 |
28 | const reactKeys = Object.keys(element).filter((key) =>
29 | key.startsWith('__react'),
30 | )
31 | expect(
32 | reactKeys.length,
33 | 'element was not created by React',
34 | ).to.be.greaterThan(0)
35 | })
36 |
--------------------------------------------------------------------------------
/exercises/07.forms/02.problem.action/README.mdx:
--------------------------------------------------------------------------------
1 | # Form Action
2 |
3 |
4 |
5 | 🧝♂️ I've added some backend code that will handle our form submission. It's at
6 | `api/onboarding`. You can find it in .
7 |
8 |
9 | Note: the `api.server.ts` file is some magic from the Workshop app to make it
10 | so we can test things out with a backend server without having to actually set
11 | up a backend server. It's not something you'd use in a real app.
12 |
13 |
14 | 👨💼 Great! So we want the user's form submission to be sent along to
15 | `api/onboarding`. By default, when a form is submitted, the browser will send
16 | the form data to the current page's URL. We can change this by adding an
17 | `action` attribute to the form element:
18 |
19 | ```html
20 |
23 | ```
24 |
25 | Go ahead and add an `action` attribute to the form element.
26 |
27 |
28 | 🦉 You may notice a full page refresh when you submit the form. We'll talk
29 | about this in a future step.
30 |
31 |
--------------------------------------------------------------------------------
/exercises/05.typescript/06.solution.satisfies/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | type OperationFn = (left: number, right: number) => number
4 |
5 | const operations = {
6 | '+': (left, right) => left + right,
7 | '-': (left, right) => left - right,
8 | '*': (left, right) => left * right,
9 | '/': (left, right) => left / right,
10 | } satisfies Record
11 |
12 | type CalculatorProps = {
13 | left?: number
14 | operator?: keyof typeof operations
15 | right?: number
16 | }
17 | function Calculator({ left = 0, operator = '+', right = 0 }: CalculatorProps) {
18 | const result = operations[operator](left, right)
19 | return (
20 |
21 |
22 | {left} {operator} {right} = {result}
23 |
24 |
25 | )
26 | }
27 |
28 | function App() {
29 | return (
30 |
31 |
Calculator
32 |
33 |
34 |
35 |
36 |
37 | )
38 | }
39 |
40 | const rootEl = document.createElement('div')
41 | document.body.append(rootEl)
42 | createRoot(rootEl).render( )
43 |
--------------------------------------------------------------------------------
/exercises/06.styling/01.problem.style/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | // 🐨 add a className prop to each div and apply the correct class names
4 | // based on the text content
5 | // 💰 Here are the available class names: box, box--large, box--medium, box--small
6 | // 💰 each of the elements should have the "box" className applied
7 |
8 | // 🐨 add a style prop to each div so their background color
9 | // matches what the text says it should be
10 | // 🐨 also use the style prop to make the font italic
11 | // 💰 Here are available style attributes: backgroundColor, fontStyle
12 |
13 | const smallBox = small lightblue box
14 | const mediumBox = medium pink box
15 | const largeBox = large orange box
16 |
17 | // 💰 the sizelessColorlessBox should still be a box, just with no size or color
18 | const sizelessColorlessBox = sizeless colorless box
19 |
20 | function App() {
21 | return (
22 |
23 | {smallBox}
24 | {mediumBox}
25 | {largeBox}
26 | {sizelessColorlessBox}
27 |
28 | )
29 | }
30 |
31 | const rootEl = document.createElement('div')
32 | document.body.append(rootEl)
33 | createRoot(rootEl).render( )
34 |
--------------------------------------------------------------------------------
/exercises/09.errors/02.problem.show-boundary/README.mdx:
--------------------------------------------------------------------------------
1 | # Other Errors
2 |
3 |
4 |
5 | 👨💼 Recall previously we learned that React doesn't catch all errors for you.
6 | We've got an error in our `onSubmit` handler like this. If you try to submit the
7 | form, you'll notice in the console that there's an error (someone made a typo
8 | 😅). The error looks something like:
9 |
10 | ```javascript
11 | Uncaught TypeError: Cannot read properties of null (reading 'toUpperCase')
12 | ```
13 |
14 | Before we fix the typo, we want to give the user better feedback for when
15 | something like this happens (right now, they don't know it's not working and
16 | that's terribly annoying). So what I want you to do is wrap that callback in our
17 | own `try/catch` and then surface it to React using the `showBoundary` function
18 | you get when you call the `useErrorBoundary` utility:
19 |
20 | ```tsx
21 | import { useErrorBoundary } from 'react-error-boundary'
22 |
23 | // ... in the component
24 | const { showBoundary } = useErrorBoundary()
25 |
26 | // ... in the `catch` block
27 | showBoundary(error)
28 | ```
29 |
30 | So let's make that happen!
31 |
--------------------------------------------------------------------------------
/exercises/05.typescript/02.solution.narrow/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | const operations = {
4 | '+': (left: number, right: number): number => left + right,
5 | '-': (left: number, right: number): number => left - right,
6 | '*': (left: number, right: number): number => left * right,
7 | '/': (left: number, right: number): number => left / right,
8 | }
9 |
10 | type CalculatorProps = {
11 | left: number
12 | operator: '+' | '-' | '*' | '/'
13 | right: number
14 | }
15 | function Calculator({ left, operator, right }: CalculatorProps) {
16 | const result = operations[operator](left, right)
17 | return (
18 |
19 |
20 | {left} {operator} {right} = {result}
21 |
22 |
23 | )
24 | }
25 |
26 | function App() {
27 | return (
28 |
29 |
Calculator
30 |
31 |
32 |
33 |
34 |
35 | )
36 | }
37 |
38 | const rootEl = document.createElement('div')
39 | document.body.append(rootEl)
40 | createRoot(rootEl).render( )
41 |
--------------------------------------------------------------------------------
/exercises/05.typescript/03.solution.derive/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | const operations = {
4 | '+': (left: number, right: number): number => left + right,
5 | '-': (left: number, right: number): number => left - right,
6 | '*': (left: number, right: number): number => left * right,
7 | '/': (left: number, right: number): number => left / right,
8 | }
9 |
10 | type CalculatorProps = {
11 | left: number
12 | operator: keyof typeof operations
13 | right: number
14 | }
15 | function Calculator({ left, operator, right }: CalculatorProps) {
16 | const result = operations[operator](left, right)
17 | return (
18 |
19 |
20 | {left} {operator} {right} = {result}
21 |
22 |
23 | )
24 | }
25 |
26 | function App() {
27 | return (
28 |
29 |
Calculator
30 |
31 |
32 |
33 |
34 |
35 | )
36 | }
37 |
38 | const rootEl = document.createElement('div')
39 | document.body.append(rootEl)
40 | createRoot(rootEl).render( )
41 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/02.problem.interpolation/README.mdx:
--------------------------------------------------------------------------------
1 | # Interpolation
2 |
3 |
4 |
5 | 🦉 "Interpolation" is defined as "the insertion of something of a different
6 | nature into something else."
7 |
8 | Let's take template literals for example:
9 |
10 | ```typescript
11 | const greeting = 'Sup'
12 | const subject = 'World'
13 | const message = `${greeting} ${subject}`
14 | ```
15 |
16 | 👨💼 See if you can figure out how to extract the `className` (`"container"`) and
17 | `children` (`"Hello World"`) to variables and interpolate them in the JSX.
18 |
19 | ```tsx
20 | const className = 'container'
21 | const children = 'Hello World'
22 | const element = how do I make this work?
23 | ```
24 |
25 | 📜 The React docs for JSX are pretty good:
26 | [Writing Markup with JSX](https://react.dev/learn/writing-markup-with-jsx)
27 |
28 | Here are a few sections of particular interest for this step:
29 |
30 | - [JavaScript in JSX with Curly Braces](https://react.dev/learn/javascript-in-jsx-with-curly-braces)
31 | - [Passing strings with quotes](https://react.dev/learn/javascript-in-jsx-with-curly-braces#passing-strings-with-quotes)
32 |
--------------------------------------------------------------------------------
/exercises/05.typescript/05.solution.function-types/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | type OperationFn = (left: number, right: number) => number
4 | type Operator = '+' | '-' | '/' | '*'
5 | const operations: Record = {
6 | '+': (left, right) => left + right,
7 | '-': (left, right) => left - right,
8 | '*': (left, right) => left * right,
9 | '/': (left, right) => left / right,
10 | }
11 |
12 | type CalculatorProps = {
13 | left?: number
14 | operator?: keyof typeof operations
15 | right?: number
16 | }
17 | function Calculator({ left = 0, operator = '+', right = 0 }: CalculatorProps) {
18 | const result = operations[operator](left, right)
19 | return (
20 |
21 |
22 | {left} {operator} {right} = {result}
23 |
24 |
25 | )
26 | }
27 |
28 | function App() {
29 | return (
30 |
31 |
Calculator
32 |
33 |
34 |
35 |
36 |
37 | )
38 | }
39 |
40 | const rootEl = document.createElement('div')
41 | document.body.append(rootEl)
42 | createRoot(rootEl).render( )
43 |
--------------------------------------------------------------------------------
/exercises/09.errors/01.problem.composition/README.mdx:
--------------------------------------------------------------------------------
1 | # Composition
2 |
3 |
4 |
5 | 👨💼 You'll notice that right now our onboarding form isn't showing up at all.
6 | That's because someone made a typo... But errors like this could happen for any
7 | number of reasons (data issue, API version change, network error, etc.). So we
8 | should definitely handle errors in our app.
9 |
10 | Let's start by wrapping our existing `App` component in an Error Boundary to
11 | handle any errors. We're going to have you use the `FallbackComponent` prop on
12 | the `ErrorBoundary` so we can define our own `ErrorFallback` component.
13 |
14 | ```tsx
15 | {/*...*/}
16 | ```
17 |
18 | Right now we have an `App` component which we render using
19 | `createRoot(rootEl).render( )`, so to get our stuff wrapped
20 | properly, we'll rename our existing `App` component (call it `OnboardingForm`) and
21 | then make a new `App` which renders the `ErrorBoundary` around the `OnboardingForm`
22 | component along with our `FallbackComponent` called `ErrorFallback` (which you
23 | also have to create).
24 |
25 | Get to it!
26 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/README.mdx:
--------------------------------------------------------------------------------
1 | # Using JSX
2 |
3 |
4 |
5 | JSX is more intuitive than the raw React API and is easier to understand when
6 | reading the code. It's fairly simple HTML-like syntactic sugar on top of the raw
7 | React APIs:
8 |
9 | ```tsx
10 | const element = Hey there
11 |
12 | // ↓ ↓ ↓ ↓ compiles to ↓ ↓ ↓ ↓
13 |
14 | const element = createElement('h1', {
15 | id: 'greeting',
16 | children: 'Hey there',
17 | })
18 | ```
19 |
20 | Because JSX is not actually JavaScript, you have to convert it using something
21 | called a code compiler. [Babel](https://babeljs.io) is one such tool.
22 |
23 |
24 | 🦉 Pro tip: If you'd like to see how JSX gets compiled to JavaScript, [check
25 | out the online babel REPL
26 | here](https://babeljs.io/repl#?builtIns=App&code_lz=MYewdgzgLgBArgSxgXhgHgCYIG4D40QAOAhmLgBICmANtSGgPRGm7rNkDqIATtRo-3wMseAFBA&presets=react&prettier=true).
27 |
28 |
29 | If you can train your brain to look at JSX and see the compiled version of that
30 | code, you'll be MUCH more effective at reading and using it! I strongly
31 | recommend you give this some intentional practice.
32 |
--------------------------------------------------------------------------------
/exercises/04.components/README.mdx:
--------------------------------------------------------------------------------
1 | # Custom Components
2 |
3 |
4 |
5 | Just like in regular JavaScript, when you want to reuse code, you write
6 | functions. If you want to share JSX, you can do that as well. In React we call
7 | these functions "components" and they have some special properties.
8 |
9 | Components are functions which accept an object called "props" and return
10 | something that is renderable (more React elements, strings, `null`, numbers,
11 | etc.). To be clear, this is _the_ definition of a React component. That's all it
12 | is. So I'll say it
13 | [again](https://twitter.com/kentcdodds/status/1763606427028136131):
14 |
15 | > Components are functions which accept an object called "props" and return
16 | > something that is renderable
17 |
18 | Here's an example of that:
19 |
20 | ```tsx
21 | function Greeting(props) {
22 | return Hello, {props.name}
23 | }
24 | ```
25 |
26 | Then that component can be used like this:
27 |
28 | ```tsx
29 |
30 | ```
31 |
32 | Just like previous exercises, we're going to ease into this syntax so you have a
33 | solid foundational understanding of how this works.
34 |
--------------------------------------------------------------------------------
/exercises/05.typescript/01.solution.props/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | const operations = {
4 | '+': (left: number, right: number): number => left + right,
5 | '-': (left: number, right: number): number => left - right,
6 | '*': (left: number, right: number): number => left * right,
7 | '/': (left: number, right: number): number => left / right,
8 | }
9 |
10 | type CalculatorProps = {
11 | left: number
12 | operator: string
13 | right: number
14 | }
15 | function Calculator({ left, operator, right }: CalculatorProps) {
16 | // @ts-expect-error we'll fix this one later
17 | const result = operations[operator](left, right)
18 | return (
19 |
20 |
21 | {left} {operator} {right} = {result}
22 |
23 |
24 | )
25 | }
26 |
27 | function App() {
28 | return (
29 |
30 |
Calculator
31 |
32 |
33 |
34 |
35 |
36 | )
37 | }
38 |
39 | const rootEl = document.createElement('div')
40 | document.body.append(rootEl)
41 | createRoot(rootEl).render( )
42 |
--------------------------------------------------------------------------------
/exercises/07.forms/03.solution.types/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | function App() {
4 | return (
5 |
32 | )
33 | }
34 |
35 | const rootEl = document.createElement('div')
36 | document.body.append(rootEl)
37 | createRoot(rootEl).render( )
38 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/05.solution.fragments/content.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 |
3 | await testStep('Proper elements are rendered to the DOM', async () => {
4 | const rootElement = document.getElementById('root')
5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement)
6 | if (!rootElement) return
7 |
8 | const element = rootElement
9 |
10 | const p = await dtl.waitFor(
11 | async () => {
12 | const p = element.querySelector('p')
13 | expect(p, ' not found').to.be.instanceOf(HTMLElement)
14 | return p
15 | },
16 | { timeout: 5000 },
17 | )
18 |
19 | const ul = element.querySelector('ul')
20 | expect(ul, '
not found').to.be.instanceOf(HTMLElement)
21 | expect(ul).to.have.class('sams-food')
22 | const lis = element.querySelectorAll('li')
23 | expect(lis, ' elements not found').to.have.length(2)
24 |
25 | expect(p!.textContent, 'p text is not correct').to.equal(
26 | "Here's Sam's favorite food:",
27 | )
28 |
29 | const [greenEggs, ham] = ul!.querySelectorAll('li')
30 |
31 | expect(greenEggs?.textContent, 'green eggs text is not correct').to.equal(
32 | 'Green eggs',
33 | )
34 | expect(ham?.textContent, 'ham text is not correct').to.equal('Ham')
35 | })
36 |
--------------------------------------------------------------------------------
/exercises/08.inputs/03.problem.radio/README.mdx:
--------------------------------------------------------------------------------
1 | # Radios
2 |
3 |
4 |
5 | 👨💼 We need to allow the user to select the visibility of the account. It can be
6 | either Public or Private. We could use a `` here, but I don't think
7 | that's the best option. Instead, let's use a radio group.
8 |
9 | 📜 You can [learn about radio groups on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/radio)
10 |
11 | What's interesting about radio groups is that each radio group can contain
12 | multiple inputs that share the same name. Only one of them can be selected at
13 | a time. Each radio input needs its own `label`. To label the entire group, you
14 | can use a `` and a ``.
15 |
16 |
17 | Note: Similar to checkboxes, to set the default value, you can set the
18 | `checked` attribute to the input you want to be selected by default... but in
19 | React, that will make your input read-only! We'll learn more about how to set
20 | default values in React in a future lesson.
21 |
22 |
23 | Similar to a select `option`, you need to set the `value` on the input to
24 | determine what value will be sent to the server when the form is submitted.
25 |
--------------------------------------------------------------------------------
/exercises/05.typescript/03.problem.derive/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | const operations = {
4 | '+': (left: number, right: number): number => left + right,
5 | '-': (left: number, right: number): number => left - right,
6 | '*': (left: number, right: number): number => left * right,
7 | '/': (left: number, right: number): number => left / right,
8 | }
9 |
10 | type CalculatorProps = {
11 | left: number
12 | // 🐨 derive these values from the keys of the operations object
13 | operator: '+' | '-' | '*' | '/'
14 | right: number
15 | }
16 | function Calculator({ left, operator, right }: CalculatorProps) {
17 | const result = operations[operator](left, right)
18 | return (
19 |
20 |
21 | {left} {operator} {right} = {result}
22 |
23 |
24 | )
25 | }
26 |
27 | function App() {
28 | return (
29 |
30 |
Calculator
31 |
32 |
33 |
34 |
35 |
36 | )
37 | }
38 |
39 | const rootEl = document.createElement('div')
40 | document.body.append(rootEl)
41 | createRoot(rootEl).render( )
42 |
--------------------------------------------------------------------------------
/exercises/02.raw-react/01.problem.elements/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/exercises/05.typescript/01.problem.props/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | const operations = {
4 | '+': (left: number, right: number): number => left + right,
5 | '-': (left: number, right: number): number => left - right,
6 | '*': (left: number, right: number): number => left * right,
7 | '/': (left: number, right: number): number => left / right,
8 | }
9 |
10 | // 🦺 create a type called CalculatorProps
11 |
12 | // 🦺 set the type for this props argument to CalculatorProps
13 | // @ts-expect-error 💣 when you finish, remove this comment.
14 | function Calculator({ left, operator, right }) {
15 | // @ts-expect-error we'll fix this one later
16 | const result = operations[operator](left, right)
17 | return (
18 |
19 |
20 | {left} {operator} {right} = {result}
21 |
22 |
23 | )
24 | }
25 |
26 | function App() {
27 | return (
28 |
29 |
Calculator
30 |
31 |
32 |
33 |
34 |
35 | )
36 | }
37 |
38 | const rootEl = document.createElement('div')
39 | document.body.append(rootEl)
40 | createRoot(rootEl).render( )
41 |
--------------------------------------------------------------------------------
/exercises/05.typescript/02.problem.narrow/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | const operations = {
4 | '+': (left: number, right: number): number => left + right,
5 | '-': (left: number, right: number): number => left - right,
6 | '*': (left: number, right: number): number => left * right,
7 | '/': (left: number, right: number): number => left / right,
8 | }
9 |
10 | type CalculatorProps = {
11 | left: number
12 | // 🦺 limit the operator to be only +, -, *, or /
13 | operator: string
14 | right: number
15 | }
16 | function Calculator({ left, operator, right }: CalculatorProps) {
17 | // @ts-expect-error 💣 when you finish, remove this comment.
18 | const result = operations[operator](left, right)
19 | return (
20 |
21 |
22 | {left} {operator} {right} = {result}
23 |
24 |
25 | )
26 | }
27 |
28 | function App() {
29 | return (
30 |
31 |
Calculator
32 |
33 |
34 |
35 |
36 |
37 | )
38 | }
39 |
40 | const rootEl = document.createElement('div')
41 | document.body.append(rootEl)
42 | createRoot(rootEl).render( )
43 |
--------------------------------------------------------------------------------
/exercises/02.raw-react/02.solution.nesting/index.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 |
3 | await testStep('"Hello World" is rendered to the DOM', async () => {
4 | const rootElement = document.getElementById('root')
5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement)
6 | if (!rootElement) return
7 |
8 | const element = await dtl.waitFor(
9 | () => {
10 | const element = rootElement!.querySelector('.container')
11 | expect(element, 'container element not found').to.be.instanceOf(
12 | HTMLElement,
13 | )
14 | return element
15 | },
16 | { timeout: 5000 },
17 | )
18 |
19 | if (!element) return
20 |
21 | if (element.textContent === 'HelloWorld') {
22 | throw new Error(
23 | 'Looks like you forgot to include the space between the spans',
24 | )
25 | }
26 |
27 | expect(element.textContent, 'element text is not correct').to.equal(
28 | 'Hello World',
29 | )
30 |
31 | const [hello, world] = element.querySelectorAll('span')
32 |
33 | expect(hello, 'Hello span not found').to.be.instanceOf(HTMLElement)
34 | expect(world, 'World span not found').to.be.instanceOf(HTMLElement)
35 |
36 | expect(hello?.textContent, 'hello text is not correct').to.equal('Hello')
37 | expect(world?.textContent, 'world text is not correct').to.equal('World')
38 | })
39 |
--------------------------------------------------------------------------------
/exercises/10.arrays/01.solution.key-prop/index.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 |
4 | const allItems = [
5 | { id: 'apple', value: '🍎 apple' },
6 | { id: 'orange', value: '🍊 orange' },
7 | { id: 'grape', value: '🍇 grape' },
8 | { id: 'pear', value: '🍐 pear' },
9 | ]
10 |
11 | function App() {
12 | const [items, setItems] = useState(allItems)
13 |
14 | function addItem() {
15 | const itemIds = items.map((i) => i.id)
16 | const itemToAdd = allItems.find((i) => !itemIds.includes(i.id))
17 | if (itemToAdd) setItems([...items, itemToAdd])
18 | }
19 |
20 | function removeItem(id: string) {
21 | setItems(items.filter((i) => i.id !== id))
22 | }
23 |
24 | return (
25 |
26 |
= allItems.length} onClick={addItem}>
27 | add item
28 |
29 |
38 |
39 | )
40 | }
41 |
42 | const rootEl = document.createElement('div')
43 | document.body.append(rootEl)
44 | createRoot(rootEl).render( )
45 |
--------------------------------------------------------------------------------
/exercises/07.forms/05.solution.action/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | function App() {
4 | function logFormData(formData: FormData) {
5 | console.log(Object.fromEntries(formData))
6 | }
7 | return (
8 |
35 | )
36 | }
37 |
38 | const rootEl = document.createElement('div')
39 | document.body.append(rootEl)
40 | createRoot(rootEl).render( )
41 |
--------------------------------------------------------------------------------
/exercises/10.arrays/01.solution.key-prop/README.mdx:
--------------------------------------------------------------------------------
1 | # Key prop
2 |
3 |
4 |
5 | 👨💼 Great work. That was super weird and confusing for our users. They're much
6 | happier now.
7 |
8 | 🦉 The key only needs to be unique within a given array. So this works
9 | fine:
10 |
11 | ```tsx
12 | const element = (
13 |
14 | {list.map((listItem) => (
15 | {listItem.value}
16 | ))}
17 |
18 | )
19 | ```
20 |
21 | In our example, the `value` of the input is managed by the browser, but this
22 | has even bigger implications when we start working with our own state and
23 | side-effects. It's a little too early to demonstrate this for you, but you
24 | should know that when React removes a component from the DOM, it gets
25 | "unmounted" which will trigger side-effect cleanups, and if new elements are
26 | added then those will be "mounted" and will trigger your side-effects. This can
27 | cause some surprising and problematic issues for your users.
28 |
29 | So just remember the rule and always provide a `key` when rendering an array.
30 | Later when you have more React experience, you can come back to this exercise
31 | and expand it a bit with custom components that manage state and side-effects to
32 | observe the problems caused when you ignore the `key`.
33 |
--------------------------------------------------------------------------------
/exercises/02.raw-react/03.solution.deep-nesting/index.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 |
3 | await testStep('Proper elements are rendered to the DOM', async () => {
4 | const rootElement = document.getElementById('root')
5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement)
6 | if (!rootElement) return
7 |
8 | const element = await dtl.waitFor(
9 | () => {
10 | const element = rootElement!.querySelector('.container')
11 | expect(element, 'container element not found').to.be.instanceOf(
12 | HTMLElement,
13 | )
14 | return element
15 | },
16 | { timeout: 5000 },
17 | )
18 |
19 | if (!element) return
20 |
21 | const p = element.querySelector('p')
22 | expect(p, ' not found').to.be.instanceOf(HTMLElement)
23 | const ul = element.querySelector('ul')
24 | expect(ul, '
not found').to.be.instanceOf(HTMLElement)
25 | const lis = element.querySelectorAll('li')
26 | expect(lis, ' elements not found').to.have.length(2)
27 |
28 | expect(p!.textContent, 'p text is not correct').to.equal(
29 | "Here's Sam's favorite food:",
30 | )
31 |
32 | const [greenEggs, ham] = ul!.querySelectorAll('li')
33 |
34 | expect(greenEggs?.textContent, 'green eggs text is not correct').to.equal(
35 | 'Green eggs',
36 | )
37 | expect(ham?.textContent, 'ham text is not correct').to.equal('Ham')
38 | })
39 |
--------------------------------------------------------------------------------
/exercises/05.typescript/06.problem.satisfies/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | type OperationFn = (left: number, right: number) => number
4 | // 💣 delete the Operator type
5 | type Operator = '+' | '-' | '/' | '*'
6 | // 🦺 remove the type cast
7 | const operations: Record = {
8 | '+': (left, right) => left + right,
9 | '-': (left, right) => left - right,
10 | '*': (left, right) => left * right,
11 | '/': (left, right) => left / right,
12 | }
13 | // 🦺 add "satisfies" here to ensure operations satisfies a Record with string keys and OperationFn values
14 |
15 | type CalculatorProps = {
16 | left?: number
17 | operator?: keyof typeof operations
18 | right?: number
19 | }
20 | function Calculator({ left = 0, operator = '+', right = 0 }: CalculatorProps) {
21 | const result = operations[operator](left, right)
22 | return (
23 |
24 |
25 | {left} {operator} {right} = {result}
26 |
27 |
28 | )
29 | }
30 |
31 | function App() {
32 | return (
33 |
34 |
Calculator
35 |
36 |
37 |
38 |
39 |
40 | )
41 | }
42 |
43 | const rootEl = document.createElement('div')
44 | document.body.append(rootEl)
45 | createRoot(rootEl).render( )
46 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/04.solution.nesting/content.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 |
3 | await testStep('Proper elements are rendered to the DOM', async () => {
4 | const rootElement = document.getElementById('root')
5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement)
6 | if (!rootElement) return
7 |
8 | const element = await dtl.waitFor(
9 | () => {
10 | const element = rootElement!.querySelector('.container')
11 | expect(element, 'container element not found').to.be.instanceOf(
12 | HTMLElement,
13 | )
14 | return element
15 | },
16 | { timeout: 5000 },
17 | )
18 |
19 | if (!element) return
20 |
21 | const p = element.querySelector('p')
22 | expect(p, ' not found').to.be.instanceOf(HTMLElement)
23 | const ul = element.querySelector('ul')
24 | expect(ul, '
not found').to.be.instanceOf(HTMLElement)
25 | expect(ul).to.have.class('sams-food')
26 | const lis = element.querySelectorAll('li')
27 | expect(lis, ' elements not found').to.have.length(2)
28 |
29 | expect(p!.textContent, 'p text is not correct').to.equal(
30 | "Here's Sam's favorite food:",
31 | )
32 |
33 | const [greenEggs, ham] = ul!.querySelectorAll('li')
34 |
35 | expect(greenEggs?.textContent, 'green eggs text is not correct').to.equal(
36 | 'Green eggs',
37 | )
38 | expect(ham?.textContent, 'ham text is not correct').to.equal('Ham')
39 | })
40 |
--------------------------------------------------------------------------------
/exercises/09.errors/03.solution.reset/error-boundary.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 | const { screen } = dtl
3 | import { userEvent } from '@testing-library/user-event'
4 |
5 | const user = userEvent.setup()
6 |
7 | import './index.tsx'
8 |
9 | await testStep('Error boundary is rendered after form submission', async () => {
10 | // submit the form
11 | await user.click(await screen.findByRole('button', { name: /submit/i }))
12 |
13 | // Check if the error message is displayed
14 | const errorMessage = await screen.findByRole('alert')
15 | expect(errorMessage).toBeDefined()
16 | expect(errorMessage.textContent).toContain('There was an error:')
17 |
18 | // Ensure the form is not rendered after error
19 | const form = screen.queryByRole('form')
20 | expect(form).toBeNull()
21 | })
22 |
23 | await testStep('Clicking "Try again" resets the error boundary', async () => {
24 | // Click the "Try again" button
25 | const tryAgainButton = await screen.findByRole('button', {
26 | name: /try again/i,
27 | })
28 | await user.click(tryAgainButton)
29 |
30 | // Verify that the submit button appears again
31 | const submitButton = await screen.findByRole('button', { name: /submit/i })
32 | expect(submitButton).toBeDefined()
33 |
34 | // Ensure the error message is no longer present
35 | const errorMessage = screen.queryByRole('alert')
36 | expect(errorMessage).toBeNull()
37 | })
38 |
--------------------------------------------------------------------------------
/exercises/04.components/04.solution.props/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/exercises/08.inputs/01.problem.checkbox/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | function App() {
4 | function logFormData(formData: FormData) {
5 | console.log(Object.fromEntries(formData))
6 | }
7 | return (
8 |
37 | )
38 | }
39 |
40 | const rootEl = document.createElement('div')
41 | document.body.append(rootEl)
42 | createRoot(rootEl).render( )
43 |
--------------------------------------------------------------------------------
/exercises/08.inputs/01.solution.checkbox/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | function App() {
4 | function logFormData(formData: FormData) {
5 | console.log(Object.fromEntries(formData))
6 | }
7 | return (
8 |
41 | )
42 | }
43 |
44 | const rootEl = document.createElement('div')
45 | document.body.append(rootEl)
46 | createRoot(rootEl).render( )
47 |
--------------------------------------------------------------------------------
/exercises/10.arrays/01.problem.key-prop/index.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 |
4 | const allItems = [
5 | { id: 'apple', value: '🍎 apple' },
6 | { id: 'orange', value: '🍊 orange' },
7 | { id: 'grape', value: '🍇 grape' },
8 | { id: 'pear', value: '🍐 pear' },
9 | ]
10 |
11 | function App() {
12 | const [items, setItems] = useState(allItems)
13 |
14 | function addItem() {
15 | const itemIds = items.map((i) => i.id)
16 | const itemToAdd = allItems.find((i) => !itemIds.includes(i.id))
17 | if (itemToAdd) setItems([...items, itemToAdd])
18 | }
19 |
20 | function removeItem(id: string) {
21 | setItems(items.filter((i) => i.id !== id))
22 | }
23 |
24 | return (
25 |
26 |
= allItems.length} onClick={addItem}>
27 | add item
28 |
29 |
41 |
42 | )
43 | }
44 |
45 | const rootEl = document.createElement('div')
46 | document.body.append(rootEl)
47 | createRoot(rootEl).render( )
48 |
--------------------------------------------------------------------------------
/exercises/05.typescript/06.problem.satisfies/README.mdx:
--------------------------------------------------------------------------------
1 | # Satisfies
2 |
3 |
4 |
5 | 👨💼 Ok, so repeating ourselves there is not awesome. The problem is that we want
6 | to enforce the value of our `operations` object, but to do that we either have
7 | to widen the type of our `key` or list it explicitly as we're doing. This is
8 | because when you cast your object to a type, TypeScript widens your type as far
9 | as that cast. Here's a simple example of the problem:
10 |
11 | ```ts
12 | type ValidCandies = 'twix' | 'snickers' | 'm&ms'
13 | const candy: ValidCandies = 'twix'
14 | // candy is 'twix' | 'snickers' | 'm&ms'
15 | ```
16 |
17 | What we need is some way to enforce the values of our object, without having to
18 | annotate our object. We can do this using TypeScript's
19 | [`satisfies`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html#the-satisfies-operator) syntax.
20 | For example:
21 |
22 | ```ts
23 | type ValidCandies = 'twix' | 'snickers' | 'm&ms'
24 | const candy = 'twix' satisfies ValidCandies
25 | // candy is 'twix'
26 | ```
27 |
28 | See? No type widening! Wahoo!
29 |
30 |
31 | Before `satisfies` came out, we had to use a "Constrained Identity Function"
32 | (pattern coined by yours truly). Learn more in **[How to write a Constrained
33 | Identity Function (CIF) in
34 | TypeScript](https://kentcdodds.com/blog/how-to-write-a-constrained-identity-function-in-typescript)**
35 |
36 |
--------------------------------------------------------------------------------
/exercises/04.components/04.problem.props/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/exercises/05.typescript/04.problem.default-props/README.mdx:
--------------------------------------------------------------------------------
1 | # Default Props
2 |
3 |
4 |
5 | 👨💼 Sometimes you want to allow the user of your component to skip providing a
6 | prop and use a default value instead. To do this, we can use
7 | [destructuring default values syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Default_values_2),
8 | but when users of our component try to skip a prop, TypeScript will complain
9 | because our type says all the elements of the `CalculatorProps` type are
10 | required.
11 |
12 | So when you make a prop optional, make sure you provide any relevant default
13 | value as well as mark it as optional using the
14 | [optional properties syntax](https://www.typescriptlang.org/docs/handbook/2/objects.html#optional-properties):
15 |
16 | ```tsx
17 | type User = { name: string; isCute?: boolean }
18 | // name is required, isCute is optional, so these both compile:
19 | const kody = { name: 'Kody', isCute: true }
20 | const peter = { name: 'Peter' }
21 | ```
22 |
23 | For this step, make all props optional. Default `left` and `right` to
24 | `0` and `operator` to `'+'`. Then you can update the App to test it out:
25 |
26 | ```tsx
27 | function App() {
28 | return (
29 |
30 |
Calculator
31 |
32 |
33 |
34 |
35 |
36 | )
37 | }
38 | ```
39 |
--------------------------------------------------------------------------------
/exercises/06.styling/03.problem.size-prop/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | export function Box({
4 | // 💯 you can keep the style and className props here, but you can make this
5 | // still work if you remove them. Give that a shot if you want.
6 | style = {},
7 | className = '',
8 | // 🐨 add a size prop here
9 | ...otherProps // 🦺 intersect (&) this with an object that has a size prop type here which is
10 | // optional and is one of "small", "medium", or "large"
11 | }: React.ComponentProps<'div'>) {
12 | // 🐨 based on the size prop, define a new variable called sizeClassName
13 | return (
14 |
21 | )
22 | }
23 |
24 | function App() {
25 | return (
26 |
27 | {/* 🐨 update all these boxes to use the size prop */}
28 |
29 | small lightblue box
30 |
31 |
32 | medium pink box
33 |
34 |
35 | large orange box
36 |
37 | sizeless colorless box
38 |
39 | )
40 | }
41 |
42 | const rootEl = document.createElement('div')
43 | document.body.append(rootEl)
44 | createRoot(rootEl).render( )
45 |
--------------------------------------------------------------------------------
/exercises/06.styling/01.solution.style/boxes.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 | const { screen } = dtl
3 |
4 | import './index.tsx'
5 |
6 | await testStep('Small lightblue box is rendered correctly', async () => {
7 | const smallBox = await screen.findByText('small lightblue box')
8 | expect(smallBox).toHaveClass('box box--small')
9 | expect(smallBox.style.backgroundColor).toBe('lightblue')
10 | expect(smallBox.style.fontStyle).toBe('italic')
11 | })
12 |
13 | await testStep('Medium pink box is rendered correctly', async () => {
14 | const mediumBox = await screen.findByText('medium pink box')
15 | expect(mediumBox).toHaveClass('box box--medium')
16 | expect(mediumBox.style.backgroundColor).toBe('pink')
17 | expect(mediumBox.style.fontStyle).toBe('italic')
18 | })
19 |
20 | await testStep('Large orange box is rendered correctly', async () => {
21 | const largeBox = await screen.findByText('large orange box')
22 | expect(largeBox).toHaveClass('box box--large')
23 | expect(largeBox.style.backgroundColor).toBe('orange')
24 | expect(largeBox.style.fontStyle).toBe('italic')
25 | })
26 |
27 | await testStep('Sizeless colorless box is rendered correctly', async () => {
28 | const sizelessColorlessBox = await screen.findByText('sizeless colorless box')
29 | expect(sizelessColorlessBox).toHaveClass('box')
30 | expect(sizelessColorlessBox).not.toHaveClass('box--small')
31 | expect(sizelessColorlessBox).not.toHaveClass('box--medium')
32 | expect(sizelessColorlessBox).not.toHaveClass('box--large')
33 | expect(sizelessColorlessBox.style.fontStyle).toBe('italic')
34 | })
35 |
--------------------------------------------------------------------------------
/exercises/05.typescript/03.problem.derive/README.mdx:
--------------------------------------------------------------------------------
1 | # Derive Types
2 |
3 |
4 |
5 | 👨💼 You may have noticed that we're duplicating our operators of `+`, `-`, `*`,
6 | and `/`. Any time we want to add a new operator, we have to add it in two places
7 | and if we miss one then we could either have a runtime error, or users won't be
8 | able to use our new operator at all.
9 |
10 | It would be better if we could have the compiler let us know we missed one
11 | (foreshadowing... look forward to that in an upcoming step) or just
12 | derive the possible operators.
13 |
14 | To do this, you need to know about two TypeScript keywords: `typeof` and
15 | `keyof`. Technically `typeof` is a JavaScript feature, but TypeScript builds on
16 | top of this and will get you the TypeScript type for the given variable. So if
17 | you say:
18 |
19 | ```tsx
20 | const user = { name: 'kody', isCute: true }
21 | type User = typeof user
22 | // type User = { name: string; isCute: boolean; }
23 | ```
24 |
25 | And then you can use `keyof` to get a union-ed type of strings of all the keys
26 | in a given type:
27 |
28 | ```tsx
29 | type UserKeys = keyof User
30 | // type UserKeys = "name" | "isCute"
31 | ```
32 |
33 | 📜 Learn more about TypeScript's
34 | [`typeof` operator](https://www.typescriptlang.org/docs/handbook/2/typeof-types.html)
35 | and
36 | [`keyof` operator](https://www.typescriptlang.org/docs/handbook/2/keyof-types.html).
37 |
38 | With that, try and derive the type of the `CalculatorProps['operator']` so you
39 | don't have to repeat yourself.
40 |
--------------------------------------------------------------------------------
/exercises/06.styling/02.solution.component/boxes.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 | const { screen } = dtl
3 |
4 | import './index.tsx'
5 |
6 | await testStep('Small lightblue box is rendered correctly', async () => {
7 | const smallBox = await screen.findByText('small lightblue box')
8 | expect(smallBox).toHaveClass('box box--small')
9 | expect(smallBox.style.backgroundColor).toBe('lightblue')
10 | expect(smallBox.style.fontStyle).toBe('italic')
11 | })
12 |
13 | await testStep('Medium pink box is rendered correctly', async () => {
14 | const mediumBox = await screen.findByText('medium pink box')
15 | expect(mediumBox).toHaveClass('box box--medium')
16 | expect(mediumBox.style.backgroundColor).toBe('pink')
17 | expect(mediumBox.style.fontStyle).toBe('italic')
18 | })
19 |
20 | await testStep('Large orange box is rendered correctly', async () => {
21 | const largeBox = await screen.findByText('large orange box')
22 | expect(largeBox).toHaveClass('box box--large')
23 | expect(largeBox.style.backgroundColor).toBe('orange')
24 | expect(largeBox.style.fontStyle).toBe('italic')
25 | })
26 |
27 | await testStep('Sizeless colorless box is rendered correctly', async () => {
28 | const sizelessColorlessBox = await screen.findByText('sizeless colorless box')
29 | expect(sizelessColorlessBox).toHaveClass('box')
30 | expect(sizelessColorlessBox).not.toHaveClass('box--small')
31 | expect(sizelessColorlessBox).not.toHaveClass('box--medium')
32 | expect(sizelessColorlessBox).not.toHaveClass('box--large')
33 | expect(sizelessColorlessBox.style.fontStyle).toBe('italic')
34 | })
35 |
--------------------------------------------------------------------------------
/exercises/06.styling/03.solution.size-prop/boxes.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 | const { screen } = dtl
3 |
4 | import './index.tsx'
5 |
6 | await testStep('Small lightblue box is rendered correctly', async () => {
7 | const smallBox = await screen.findByText('small lightblue box')
8 | expect(smallBox).toHaveClass('box box--small')
9 | expect(smallBox.style.backgroundColor).toBe('lightblue')
10 | expect(smallBox.style.fontStyle).toBe('italic')
11 | })
12 |
13 | await testStep('Medium pink box is rendered correctly', async () => {
14 | const mediumBox = await screen.findByText('medium pink box')
15 | expect(mediumBox).toHaveClass('box box--medium')
16 | expect(mediumBox.style.backgroundColor).toBe('pink')
17 | expect(mediumBox.style.fontStyle).toBe('italic')
18 | })
19 |
20 | await testStep('Large orange box is rendered correctly', async () => {
21 | const largeBox = await screen.findByText('large orange box')
22 | expect(largeBox).toHaveClass('box box--large')
23 | expect(largeBox.style.backgroundColor).toBe('orange')
24 | expect(largeBox.style.fontStyle).toBe('italic')
25 | })
26 |
27 | await testStep('Sizeless colorless box is rendered correctly', async () => {
28 | const sizelessColorlessBox = await screen.findByText('sizeless colorless box')
29 | expect(sizelessColorlessBox).toHaveClass('box')
30 | expect(sizelessColorlessBox).not.toHaveClass('box--small')
31 | expect(sizelessColorlessBox).not.toHaveClass('box--medium')
32 | expect(sizelessColorlessBox).not.toHaveClass('box--large')
33 | expect(sizelessColorlessBox.style.fontStyle).toBe('italic')
34 | })
35 |
--------------------------------------------------------------------------------
/exercises/01.js-hello-world/01.problem.hello/index.html:
--------------------------------------------------------------------------------
1 | TODO: Check the instructions, then delete this text.
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/exercises/05.typescript/05.problem.function-types/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | // 🦺 create an OperationFn type that represents the common shape of the
4 | // operation functions below
5 | // 🦺 create an Operator type that's set to the union of +, -, *, and /
6 | // 🦉 Note: you cannot use "keyof typeof operations" anymore because of a
7 | // circular dependency here
8 |
9 | // 🦺 cast the operations variable to a Record of the Operator type and the OperationFn type
10 | const operations = {
11 | // 🦺 remove all the ": number" from these functions
12 | '+': (left: number, right: number): number => left + right,
13 | '-': (left: number, right: number): number => left - right,
14 | '*': (left: number, right: number): number => left * right,
15 | '/': (left: number, right: number): number => left / right,
16 | }
17 |
18 | type CalculatorProps = {
19 | left?: number
20 | operator?: keyof typeof operations
21 | right?: number
22 | }
23 | function Calculator({ left = 0, operator = '+', right = 0 }: CalculatorProps) {
24 | const result = operations[operator](left, right)
25 | return (
26 |
27 |
28 | {left} {operator} {right} = {result}
29 |
30 |
31 | )
32 | }
33 |
34 | function App() {
35 | return (
36 |
37 |
Calculator
38 |
39 |
40 |
41 |
42 |
43 | )
44 | }
45 |
46 | const rootEl = document.createElement('div')
47 | document.body.append(rootEl)
48 | createRoot(rootEl).render( )
49 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/01.problem.compiler/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
31 |
32 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/exercises/05.typescript/04.problem.default-props/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | const operations = {
4 | '+': (left: number, right: number): number => left + right,
5 | '-': (left: number, right: number): number => left - right,
6 | '*': (left: number, right: number): number => left * right,
7 | '/': (left: number, right: number): number => left / right,
8 | }
9 |
10 | type CalculatorProps = {
11 | // 🐨 make each of these optional
12 | left: number
13 | operator: keyof typeof operations
14 | right: number
15 | }
16 | // 🐨 add defaults so if a user just uses they'll get "0 + 0 = 0"
17 | function Calculator({ left, operator, right }: CalculatorProps) {
18 | const result = operations[operator](left, right)
19 | return (
20 |
21 |
22 | {left} {operator} {right} = {result}
23 |
24 |
25 | )
26 | }
27 |
28 | function App() {
29 | return (
30 |
31 |
Calculator
32 | {/* 🐨 remove the values that are not strictly necessary */}
33 |
34 |
35 |
36 |
37 | {/* 🦉 Sometimes passing a value that's the default is ok even if it's not
38 | strictly necessary because it's more explicit and clear. But we're just
39 | playing around with TypeScript and defaults here so play along please! */}
40 |
41 | )
42 | }
43 |
44 | const rootEl = document.createElement('div')
45 | document.body.append(rootEl)
46 | createRoot(rootEl).render( )
47 |
--------------------------------------------------------------------------------
/.github/workflows/validate.yml:
--------------------------------------------------------------------------------
1 | name: deploy
2 |
3 | concurrency:
4 | group: ${{ github.workflow }}-${{ github.ref }}
5 | cancel-in-progress: true
6 |
7 | on:
8 | push:
9 | branches:
10 | - 'main'
11 | pull_request:
12 | branches:
13 | - 'main'
14 | jobs:
15 | setup:
16 | strategy:
17 | matrix:
18 | os: [ubuntu-latest, windows-latest, macos-latest]
19 | runs-on: ${{ matrix.os }}
20 | steps:
21 | - name: ⬇️ Checkout repo
22 | uses: actions/checkout@v4
23 |
24 | - name: ⎔ Setup node
25 | uses: actions/setup-node@v4
26 | with:
27 | node-version: 20
28 |
29 | - name: ▶️ Run setup script
30 | run: npm run setup
31 |
32 | - name: ʦ TypeScript
33 | run: npm run typecheck
34 |
35 | - name: ⬣ ESLint
36 | run: npm run lint
37 |
38 | # TODO: get this working again
39 | # - name: ⬇️ Install Playwright
40 | # run: npm --prefix epicshop run test:setup
41 |
42 | # - name: 🧪 In-browser tests
43 | # run: npm --prefix epicshop test
44 |
45 | deploy:
46 | name: 🚀 Deploy
47 | runs-on: ubuntu-latest
48 | # only deploy main branch on pushes
49 | if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
50 |
51 | steps:
52 | - name: ⬇️ Checkout repo
53 | uses: actions/checkout@v4
54 |
55 | - name: 🎈 Setup Fly
56 | uses: superfly/flyctl-actions/setup-flyctl@1.5
57 |
58 | - name: 🚀 Deploy
59 | run: flyctl deploy --remote-only
60 | working-directory: ./epicshop
61 | env:
62 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
63 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/01.solution.compiler/README.mdx:
--------------------------------------------------------------------------------
1 | # Compiling JSX
2 |
3 |
4 |
5 | 🦉 Let's break down the key changes in our `script` tag:
6 |
7 | 1. `type="text/babel"` and `data-type="module"`:
8 | - `type="text/babel"` tells Babel to transpile this script, allowing us to use JSX and modern JavaScript features.
9 | - `data-type="module"` indicates that this script should be treated as a module after Babel transpilation. This enables us to use `import` statements.
10 |
11 | We use both instead of just `type="module"` because browsers don't natively understand JSX. Babel needs to transform our code first, then it can be treated as a module.
12 |
13 | 2. Importing React:
14 | We've changed from:
15 |
16 | ```javascript
17 | import { createElement } from '/react.js'
18 | ```
19 |
20 | to:
21 |
22 | ```javascript
23 | import * as React from '/react.js'
24 | ```
25 |
26 | This imports all exports from React as a namespace called `React`. It's beneficial because:
27 | - It provides access to all React APIs, not just `createElement`.
28 | - When using JSX, the transpiler will convert JSX elements to `React.createElement` calls, so we need the `React` namespace in scope.
29 |
30 | 🔍 To dive deeper into these concepts, check out these resources:
31 |
32 | - [📜 Babel documentation on browser usage](https://babeljs.io/docs/babel-standalone)
33 | - [📜 React documentation on JSX](https://react.dev/learn/writing-markup-with-jsx)
34 | - [📜 MDN on JavaScript modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)
35 |
36 | 👨💼 Great! Now we're ready to start using JSX in our HTML file!
37 |
--------------------------------------------------------------------------------
/exercises/05.typescript/02.problem.narrow/README.mdx:
--------------------------------------------------------------------------------
1 | # Narrow Types
2 |
3 |
4 |
5 | 👨💼 Our `CalculatorProps['operator']` type being set simply to `string` is not
6 | "narrow" enough to help users of our `Calculator` component. It allows _any_
7 | `string` value to be provided, even one which our Calculator doesn't support.
8 | For example, the exponentiation operator `**` could be passed and TypeScript
9 | won't complain, but this would cause a runtime error because we don't have a
10 | function to handle that operator:
11 |
12 | ```tsx
13 | element = // 💥
14 | ```
15 |
16 | On top of that, the API for our `Calculator` isn't very discoverable. How would
17 | people know which `operations` are possible? Docs? Trial and error?
18 |
19 | Rather than a `string`, your TypeScript type definition can be set to a specific
20 | string. For example:
21 |
22 | ```tsx
23 | type KodyString = 'Kody'
24 | let kody: KodyString // this variable can only ever be set to the string 'Kody'
25 | ```
26 |
27 | Combine that functionality with
28 | [union syntax of `|`](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types)
29 | and you'll be able to specify exactly which operators are allowed. For example:
30 |
31 | ```tsx
32 | type KodyOrHannahString = 'Kody' | 'Hannah'
33 | let assistant: KodyOrHannahString // this variable can only ever be set to the string 'Kody' or 'Hannah'
34 |
35 | // 💰 tip: we could do the same thing without creating a type by inlining instead:
36 | // let assistant: 'Kody' | 'Hannah'
37 | ```
38 |
39 | How about we narrow our `operator` type from a `string` to some specific strings
40 | using a union.
41 |
--------------------------------------------------------------------------------
/exercises/04.components/03.solution.jsx/README.mdx:
--------------------------------------------------------------------------------
1 | # JSX Components
2 |
3 |
4 |
5 | 👨💼 Great job! We can now use JSX to write custom components and reuse bits of
6 | our UI with ease.
7 |
8 | 🦉 The children prop is special because it can appear either as a prop or in
9 | between the opening and closing tags of a component. So these two are
10 | equivalent:
11 |
12 | ```tsx
13 | Something went wrong!
14 |
15 | ```
16 |
17 | Our `Message` component uses the "special" and implicit `children` prop. It's
18 | special because it means we can do this:
19 |
20 | ```tsx
21 | element = Hello World
22 | // is functionally equivalent to:
23 | element =
24 | ```
25 |
26 | And you can put JSX in the children prop with either syntax as well:
27 |
28 | ```tsx
29 | element = (
30 |
31 | Hello World
32 |
33 | )
34 | // is functionally equivalent to:
35 | element = Hello, ' ', World ]} />
36 | ```
37 |
38 | 📜 [Learn more about passing JSX as `children`](https://react.dev/learn/passing-props-to-a-component#passing-jsx-as-children).
39 |
40 | But we don't have to use the `children` prop, we can call it whatever we want.
41 | So you could also do:
42 |
43 | ```tsx
44 | element = Hello, ' ', World ]} />
45 | ```
46 |
47 | The only thing that's special about the `children` prop is that it's implicit
48 | in JSX.
49 |
50 | And sometimes using something other than the `children` prop can be really
51 | useful, but we'll get to that in a future workshop.
52 |
--------------------------------------------------------------------------------
/exercises/08.inputs/02.problem.select/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | function App() {
4 | function logFormData(formData: FormData) {
5 | console.log(Object.fromEntries(formData))
6 | }
7 | return (
8 |
49 | )
50 | }
51 |
52 | const rootEl = document.createElement('div')
53 | document.body.append(rootEl)
54 | createRoot(rootEl).render( )
55 |
--------------------------------------------------------------------------------
/exercises/07.forms/04.solution.submit/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | function App() {
4 | return (
5 |
45 | )
46 | }
47 |
48 | const rootEl = document.createElement('div')
49 | document.body.append(rootEl)
50 | createRoot(rootEl).render( )
51 |
--------------------------------------------------------------------------------
/exercises/07.forms/04.problem.submit/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | function App() {
4 | return (
5 |
41 | )
42 | }
43 |
44 | const rootEl = document.createElement('div')
45 | document.body.append(rootEl)
46 | createRoot(rootEl).render( )
47 |
--------------------------------------------------------------------------------
/epicshop/setup.js:
--------------------------------------------------------------------------------
1 | import { spawnSync } from 'child_process'
2 |
3 | const styles = {
4 | // got these from playing around with what I found from:
5 | // https://github.com/istanbuljs/istanbuljs/blob/0f328fd0896417ccb2085f4b7888dd8e167ba3fa/packages/istanbul-lib-report/lib/file-writer.js#L84-L96
6 | // they're the best I could find that works well for light or dark terminals
7 | success: { open: '\u001b[32;1m', close: '\u001b[0m' },
8 | danger: { open: '\u001b[31;1m', close: '\u001b[0m' },
9 | info: { open: '\u001b[36;1m', close: '\u001b[0m' },
10 | subtitle: { open: '\u001b[2;1m', close: '\u001b[0m' },
11 | }
12 |
13 | function color(modifier, string) {
14 | return styles[modifier].open + string + styles[modifier].close
15 | }
16 |
17 | console.log(color('info', '▶️ Starting workshop setup...'))
18 |
19 | const output = spawnSync('npm --version', { shell: true })
20 | .stdout.toString()
21 | .trim()
22 | const outputParts = output.split('.')
23 | const major = Number(outputParts[0])
24 | const minor = Number(outputParts[1])
25 | if (major < 8 || (major === 8 && minor < 16)) {
26 | console.error(
27 | color(
28 | 'danger',
29 | '🚨 npm version is ' +
30 | output +
31 | ' which is out of date. Please install npm@8.16.0 or greater',
32 | ),
33 | )
34 | throw new Error('npm version is out of date')
35 | }
36 |
37 | const command =
38 | 'npx --yes "https://gist.github.com/kentcdodds/bb452ffe53a5caa3600197e1d8005733" -q'
39 | console.log(
40 | color('subtitle', ' Running the following command: ' + command),
41 | )
42 |
43 | const result = spawnSync(command, { stdio: 'inherit', shell: true })
44 |
45 | if (result.status === 0) {
46 | console.log(color('success', '✅ Workshop setup complete...'))
47 | } else {
48 | process.exit(result.status)
49 | }
50 |
51 | /*
52 | eslint
53 | "no-undef": "off",
54 | "vars-on-top": "off",
55 | */
56 |
--------------------------------------------------------------------------------
/exercises/03.using-jsx/01.solution.compiler/index.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep } from '@epic-web/workshop-utils/test'
2 |
3 | const response = await fetch(location.href)
4 | const indexHtml = await response.text()
5 | const node = document.createElement('div')
6 | node.innerHTML = indexHtml
7 |
8 | const inlineScript = node.querySelector('script[type="text/babel"]')
9 | await testStep('script with type text/babel exists', () => {
10 | expect(
11 | inlineScript,
12 | 'script with type text/babel not found',
13 | ).to.be.instanceOf(HTMLScriptElement)
14 | })
15 |
16 | await testStep('script with type text/babel has data-type="module"', () => {
17 | expect(
18 | inlineScript!.getAttribute('data-type'),
19 | 'script with type text/babel does not have data-type="module"',
20 | ).to.equal('module')
21 | })
22 |
23 | await testStep('babel is loaded', () => {
24 | const script = node.querySelector('script[src="/babel-standalone.js"]')
25 | expect(
26 | script,
27 | 'babel script not found, did you remember to add a script for "/babel-standalone.js" as the src?',
28 | ).to.be.instanceOf(HTMLScriptElement)
29 | })
30 |
31 | await testStep('JSX is in use', async () => {
32 | // Filter out comment lines before checking
33 | const scriptContent = inlineScript!.textContent || ''
34 | const lines = scriptContent.split('\n')
35 | const nonCommentLines = lines
36 | .filter((line) => {
37 | const trimmed = line.trim()
38 | return (
39 | !trimmed.startsWith('//') &&
40 | !trimmed.startsWith('/*') &&
41 | !trimmed.startsWith('*')
42 | )
43 | })
44 | .join('\n')
45 |
46 | expect(
47 | nonCommentLines,
48 | 'createElement( should not appear in your source (excluding comments)',
49 | ).not.to.include('createElement(')
50 | expect(nonCommentLines, 'JSX is not in use (excluding comments)').to.include(
51 | '',
52 | )
53 | })
54 |
--------------------------------------------------------------------------------
/exercises/06.styling/02.problem.component/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | // 🐨 create a component called "Box" which accepts style (defaults to {}), className (defaults to ''), and children props.
4 | // 🐨 Make it render a div with the style, className, and children applied.
5 | // 🐨 Also automatically add the fontStyle: 'italic' style to the style prop so consumers don't have to provide that
6 | // 🐨 And automatically add the "box" className to the className prop so consumers don't have to provide that as well.
7 | // 🚨 make sure to export the Box component so it can be imported in the test file.
8 |
9 | // 💯 as a bonus, have this accept any number of additional props (typed as React.ComponentProps<'div'>)
10 | // and apply those to the rendered div as well.
11 |
12 | // 🐨 update all of these to use the component with the appropriate props.
13 | const smallBox = (
14 |
18 | small lightblue box
19 |
20 | )
21 | const mediumBox = (
22 |
26 | medium pink box
27 |
28 | )
29 | const largeBox = (
30 |
34 | large orange box
35 |
36 | )
37 | const sizelessColorlessBox = (
38 |
39 | sizeless colorless box
40 |
41 | )
42 |
43 | function App() {
44 | return (
45 |
46 | {smallBox}
47 | {mediumBox}
48 | {largeBox}
49 | {sizelessColorlessBox}
50 |
51 | )
52 | }
53 |
54 | const rootEl = document.createElement('div')
55 | document.body.append(rootEl)
56 | createRoot(rootEl).render( )
57 |
--------------------------------------------------------------------------------
/shared/api-utils.ts:
--------------------------------------------------------------------------------
1 | const MAX_FILE_SIZE = 1024 * 1024
2 |
3 | export async function respondWithDataTable(data: URLSearchParams | FormData) {
4 | const cellStyle = 'border: 1px solid black; padding: 6px;'
5 | const headerCellStyle = `${cellStyle} border-bottom-width: 2px;`
6 |
7 | async function stringifyValue(value: string | File) {
8 | // this would be value instanceof File except the global File was not added until Node 20.
9 | // feel free to update this when we drop Node 18 support after April 2025.
10 | if (
11 | typeof value === 'object' &&
12 | 'size' in value &&
13 | 'arrayBuffer' in value &&
14 | 'type' in value &&
15 | 'name' in value
16 | ) {
17 | if (value.size > MAX_FILE_SIZE) {
18 | throw new Response(`File larger than ${MAX_FILE_SIZE} bytes`, {
19 | status: 400,
20 | })
21 | }
22 | const base64 = btoa(
23 | new Uint8Array(await value.arrayBuffer()).reduce(
24 | (data, byte) => data + String.fromCharCode(byte),
25 | '',
26 | ),
27 | )
28 | return ` `
29 | }
30 | return value
31 | }
32 |
33 | let tableOfSearchParams = ``
34 | for (const [key, value] of Array.from(data)) {
35 | tableOfSearchParams += `
36 |
37 | ${key}
38 | ${await stringifyValue(value)}
39 |
40 | `.trim()
41 | }
42 | const headerRow = `
43 |
44 | Key
45 | Value
46 |
47 | `
48 | const htmlResponse = `You submitted the following data:
49 |
50 | ${headerRow}
51 | ${tableOfSearchParams}
52 |
53 | `
54 | return new Response(htmlResponse, {
55 | headers: { 'content-type': 'text/html' },
56 | })
57 | }
58 |
--------------------------------------------------------------------------------
/exercises/08.inputs/02.solution.select/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | function App() {
4 | function logFormData(formData: FormData) {
5 | console.log(Object.fromEntries(formData))
6 | }
7 | return (
8 |
51 | )
52 | }
53 |
54 | const rootEl = document.createElement('div')
55 | document.body.append(rootEl)
56 | createRoot(rootEl).render( )
57 |
--------------------------------------------------------------------------------
/exercises/06.styling/02.solution.component/box.test.tsx:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 | import { createRoot } from 'react-dom/client'
3 | const { screen } = dtl
4 |
5 | import { Box } from './index.tsx'
6 |
7 | await testStep('Box component renders correctly', async () => {
8 | document.body.innerHTML = ''
9 | const root = document.createElement('div')
10 | document.body.appendChild(root)
11 | createRoot(root).render(
12 |
13 | Test Box
14 | ,
15 | )
16 |
17 | const boxElement = await screen.findByText('Test Box')
18 |
19 | expect(boxElement).toHaveClass('box box--small')
20 | expect(boxElement.style.backgroundColor).toBe('lightblue')
21 | expect(boxElement.style.fontStyle).toBe('italic')
22 | expect(boxElement).toHaveTextContent('Test Box')
23 | })
24 |
25 | await testStep('Box component applies default props correctly', async () => {
26 | document.body.innerHTML = ''
27 | const root = document.createElement('div')
28 | document.body.appendChild(root)
29 | createRoot(root).render(Default Box )
30 |
31 | const boxElement = await screen.findByText('Default Box')
32 |
33 | expect(boxElement).toHaveClass('box')
34 | expect(boxElement.style.fontStyle).toBe('italic')
35 | expect(boxElement).toHaveTextContent('Default Box')
36 | })
37 |
38 | await testStep('Box component passes through additional props', async () => {
39 | document.body.innerHTML = ''
40 | const root = document.createElement('div')
41 | document.body.appendChild(root)
42 | createRoot(root).render(
43 |
44 | Custom Props Box
45 | ,
46 | )
47 |
48 | const boxElement = await screen.findByText('Custom Props Box')
49 |
50 | expect(boxElement).toHaveAttribute('data-testid', 'custom-box')
51 | expect(boxElement).toHaveAttribute('aria-label', 'Custom Box')
52 | })
53 |
--------------------------------------------------------------------------------
/exercises/07.forms/04.solution.submit/submit.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2 | const { screen, fireEvent } = dtl
3 |
4 | import './index.tsx'
5 |
6 | // Mock console.log
7 | const originalLog = console.log
8 | const mockLogs: Array = []
9 | console.log = (...args) => {
10 | // Forward all args to the originalLog
11 | originalLog(...args)
12 |
13 | // Check if the log is coming from the onSubmit function
14 | const stack = new Error().stack
15 | if (stack?.includes('onSubmit')) {
16 | mockLogs.push(args)
17 | }
18 | }
19 |
20 | try {
21 | await testStep('Form submission logs correct data', async () => {
22 | // Fill out the form
23 | const usernameInput = await screen.findByLabelText(/username/i)
24 | const passwordInput = await screen.findByLabelText(/password/i)
25 | const ageInput = await screen.findByLabelText(/age/i)
26 | const colorInput = await screen.findByLabelText(/favorite color/i)
27 | const startDateInput = await screen.findByLabelText(/start date/i)
28 | const submitButton = await screen.findByRole('button', { name: /submit/i })
29 |
30 | fireEvent.change(usernameInput, { target: { value: 'testuser' } })
31 | fireEvent.change(passwordInput, { target: { value: 'password123' } })
32 | fireEvent.change(ageInput, { target: { value: '25' } })
33 | fireEvent.change(colorInput, { target: { value: '#ff0000' } })
34 | fireEvent.change(startDateInput, { target: { value: '2023-01-01' } })
35 |
36 | // Submit the form
37 | fireEvent.click(submitButton)
38 |
39 | // Assert that console.log was called with the correct form data
40 | expect(mockLogs[0][0]).toEqual(
41 | expect.objectContaining({
42 | username: 'testuser',
43 | password: 'password123',
44 | age: '25',
45 | color: '#ff0000',
46 | startDate: '2023-01-01',
47 | }),
48 | )
49 | })
50 | } finally {
51 | // Restore original console.log after tests
52 | console.log = originalLog
53 | }
54 |
--------------------------------------------------------------------------------
/exercises/05.typescript/05.problem.function-types/README.mdx:
--------------------------------------------------------------------------------
1 | # Reduce Duplication
2 |
3 |
4 |
5 | 🦉 These last two steps have little to do with React and everything to
6 | do with TypeScript. If you'd rather skip these two, I won't be offended 🥲
7 |
8 | 👨💼 One last thing that bugs me is the repetition in the `operations` type. The
9 | type for every one of those functions is the same. They all accept two numbers
10 | and return a number.
11 |
12 | One thing we could do is extract that function into a type and then tell
13 | TypeScript that the `operations` object is a `Record` where the key is one of
14 | the valid operators and the value is an `OperationFn`.
15 |
16 | I'm going to let you try this one on your own.
17 |
18 | 💰 But I'll give you some hints:
19 |
20 | - You'll need 📜
21 | [TypeScript's `Record` Utility Type](https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type)
22 | - You'll have to manually create a union of all allowed `operations` again for
23 | the Record's `key`
24 | - You'll need to define 📜
25 | [a function type](https://kentcdodds.com/blog/typescript-function-syntaxes)
26 | for the Record's value
27 |
28 | 🦉 At the end of this one, you may prefer the previous version and that's fine.
29 | This is just two ways to do it and they both come with trade-offs. Personally, I
30 | prefer this way to avoid typing all the functions individually.
31 |
32 | 🦉 Also, you may wonder why we went back to repeating ourselves. Unfortunately
33 | there's no way around it if you want to define the object as a Record with a
34 | specific key. However! It's not as bad as before because if we make a mistake
35 | and forget to update both places, the compiler will complain at us rather than
36 | having a runtime error, so it's less of a problem. And there's actually a
37 | workaround for this, which is what the next step is all about!
38 |
--------------------------------------------------------------------------------
/exercises/07.forms/05.problem.action/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | function App() {
4 | // 🐨 create a function called logFormData which accepts a FormData object
5 | // 🐨 console.log the FormData object like we do in the current onSubmit handler
6 | return (
7 |
48 | )
49 | }
50 |
51 | const rootEl = document.createElement('div')
52 | document.body.append(rootEl)
53 | createRoot(rootEl).render( )
54 |
--------------------------------------------------------------------------------
/exercises/06.styling/02.problem.component/README.mdx:
--------------------------------------------------------------------------------
1 | # Custom Component
2 |
3 |
4 |
5 | 👨💼 We want to reduce duplication here by making a custom ` ` component
6 | that renders a div, accepts all the props and merges the given `style` and
7 | `className` props with the shared values.
8 |
9 | I should be able to use it like so:
10 |
11 | ```tsx
12 |
13 | small lightblue box
14 |
15 | ```
16 |
17 | The `box` className and `fontStyle: 'italic'` style should be applied
18 | automatically by the ` ` component which should then merge that default
19 | with the values that come from props.
20 |
21 | How you go about implementing it is part of the fun (aka "learning" 😉). You may
22 | come up with something completely different from how I do it and that's great
23 | for your learning. When you're done, these two ultimately render _exactly_ the
24 | same thing:
25 |
26 | ```tsx
27 | // before:
28 |
32 | small lightblue box
33 |
34 |
35 | // after:
36 |
37 | small lightblue box
38 |
39 | ```
40 |
41 | And I should be able to switch the `div` for a `Box` for the medium and large
42 | boxes as well.
43 |
44 | 💰 Here's a quick tip for the TypeScript part of this. When you want to wrap an
45 | element to basically simulate that element + a little functionality (like in our
46 | case), you'll want to borrow the type definition for that element from React.
47 | Here's how you do that (for a `span` element rather than `div`):
48 |
49 | ```tsx
50 | function MySpan(props: React.ComponentProps<'span'>) {
51 | return
52 | }
53 | ```
54 |
55 | And now you'll get type safety and autocomplete for all the HTML attributes
56 | accepted by a span element.
57 |
--------------------------------------------------------------------------------
/exercises/08.inputs/03.problem.radio/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 |
3 | function App() {
4 | function logFormData(formData: FormData) {
5 | console.log(Object.fromEntries(formData))
6 | }
7 | return (
8 |
53 | )
54 | }
55 |
56 | const rootEl = document.createElement('div')
57 | document.body.append(rootEl)
58 | createRoot(rootEl).render( )
59 |
--------------------------------------------------------------------------------
/exercises/README.mdx:
--------------------------------------------------------------------------------
1 | # React Fundamentals ⚛
2 |
3 |
4 |
5 | 👨💼 Hello! I'm Peter the Project Manager and I'm here to help you know what our
6 | users want you to know so you can build the kind of React application they need!
7 | In this workshop we're going to give you the foundational knowledge you need
8 | upon which you can build your React skills.
9 |
10 | Our users don't need much at the start of this workshop ("Hello World" is about
11 | as far as we'll get for a bit), but they'll need more as we go along, including
12 | custom components, type safety, styling, forms, error handling, and rendering
13 | lists of data. We'll cover all of that and more in this series of exercises.
14 |
15 | It's important that you understand the fundamentals of React so you can not just
16 | use React, but _understand_ it. This will help you build better applications and
17 | be more effective at debugging when things go wrong.
18 |
19 |
20 | NOTE: The first four exercises in this workshop are in `index.html` files to
21 | keep things as simple as possible. The JavaScript you write will be inside
22 | inline `