From e5ba14fc2b4c9130490e2d265a8a902b8d7688c1 Mon Sep 17 00:00:00 2001 From: code-mattclaffey Date: Tue, 20 May 2025 09:44:08 +0100 Subject: [PATCH 01/29] fix: security auditing --- .github/workflows/main.yml | 53 + package-lock.json | 1567 ++++++----------- package.json | 4 +- .../exercise.tsx | 1 + 4 files changed, 584 insertions(+), 1041 deletions(-) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..9593f00 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,53 @@ +name: Quality checks + +on: + push: + branches: + - main + - feature/** + - v2 + pull_request: + branches: + - main + +jobs: + quality_checks: + name: Quality checks + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + shard: [1] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Cache + uses: actions/cache@v4 + with: + path: | + ~/.npm + ${{ github.workspace }}/dist/cache + key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }} + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}- + + - name: Setup Node.js environment + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install + run: npm ci + + - name: Security Audit Checks + run: npm run audit + + - name: Static testing + run: npm run lint + + - name: Build Website + run: npm run build + + - name: Build Storybook + run: npm run build-storybook diff --git a/package-lock.json b/package-lock.json index 2224de6..7337777 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "@typescript-eslint/parser": "^7.13.1", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.19", + "better-npm-audit": "^3.11.0", "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.7", @@ -80,13 +81,15 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -284,19 +287,21 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", - "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -311,38 +316,28 @@ } }, "node_modules/@babel/helpers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", - "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "node_modules/@babel/parser": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@babel/types": "^7.27.1" }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", - "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -558,25 +553,24 @@ } }, "node_modules/@babel/runtime": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", - "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", - "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -604,14 +598,14 @@ } }, "node_modules/@babel/types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -2892,600 +2886,49 @@ } }, "node_modules/@storybook/core": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/core/-/core-8.4.7.tgz", - "integrity": "sha512-7Z8Z0A+1YnhrrSXoKKwFFI4gnsLbWzr8fnDCU6+6HlDukFYh8GHRcZ9zKfqmy6U3hw2h8H5DrHsxWfyaYUUOoA==", - "dev": true, - "dependencies": { - "@storybook/csf": "^0.1.11", - "better-opn": "^3.0.2", - "browser-assert": "^1.2.1", - "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0", - "esbuild-register": "^3.5.0", - "jsdoc-type-pratt-parser": "^4.0.0", - "process": "^0.11.10", - "recast": "^0.23.5", - "semver": "^7.6.2", - "util": "^0.12.5", - "ws": "^8.2.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "prettier": "^2 || ^3" - }, - "peerDependenciesMeta": { - "prettier": { - "optional": true - } - } - }, - "node_modules/@storybook/core-common": { - "version": "8.1.10", - "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-8.1.10.tgz", - "integrity": "sha512-+0GhgDRQwUlXu1lY77NdLnVBVycCEW0DG7eu7rvLYYkTyNRxbdl2RWsQpjr/j4sxqT6u82l9/b+RWpmsl4MgMQ==", - "dev": true, - "dependencies": { - "@storybook/core-events": "8.1.10", - "@storybook/csf-tools": "8.1.10", - "@storybook/node-logger": "8.1.10", - "@storybook/types": "8.1.10", - "@yarnpkg/fslib": "2.10.3", - "@yarnpkg/libzip": "2.3.0", - "chalk": "^4.1.0", - "cross-spawn": "^7.0.3", - "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0", - "esbuild-register": "^3.5.0", - "execa": "^5.0.0", - "file-system-cache": "2.3.0", - "find-cache-dir": "^3.0.0", - "find-up": "^5.0.0", - "fs-extra": "^11.1.0", - "glob": "^10.0.0", - "handlebars": "^4.7.7", - "lazy-universal-dotenv": "^4.0.0", - "node-fetch": "^2.0.0", - "picomatch": "^2.3.0", - "pkg-dir": "^5.0.0", - "prettier-fallback": "npm:prettier@^3", - "pretty-hrtime": "^1.0.3", - "resolve-from": "^5.0.0", - "semver": "^7.3.7", - "tempy": "^3.1.0", - "tiny-invariant": "^1.3.1", - "ts-dedent": "^2.0.0", - "util": "^0.12.4" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "prettier": "^2 || ^3" - }, - "peerDependenciesMeta": { - "prettier": { - "optional": true - } - } - }, - "node_modules/@storybook/core-common/node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/core-common/node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/core-common/node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/core-common/node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/core-common/node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/core-common/node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/core-common/node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/core-common/node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/core-common/node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", - "cpu": [ - "arm" - ], + "version": "8.6.14", + "resolved": "https://registry.npmjs.org/@storybook/core/-/core-8.6.14.tgz", + "integrity": "sha512-1P/w4FSNRqP8j3JQBOi3yGt8PVOgSRbP66Ok520T78eJBeqx9ukCfl912PQZ7SPbW3TIunBwLXMZOjZwBB/JmA==", "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/core-common/node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/core-common/node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/core-common/node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/core-common/node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/core-common/node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/core-common/node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/core-common/node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/core-common/node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/core-common/node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/core-common/node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/core-common/node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/core-common/node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/core-common/node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/core-common/node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/core-common/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@storybook/core-common/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@storybook/core-common/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@storybook/core-common/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/@storybook/core-common/node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" - } - }, - "node_modules/@storybook/core-common/node_modules/glob": { - "version": "10.4.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz", - "integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@storybook/core-common/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/core-common/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/@storybook/core-common/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" + "license": "MIT", + "dependencies": { + "@storybook/theming": "8.6.14", + "better-opn": "^3.0.2", + "browser-assert": "^1.2.1", + "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0", + "esbuild-register": "^3.5.0", + "jsdoc-type-pratt-parser": "^4.0.0", + "process": "^0.11.10", + "recast": "^0.23.5", + "semver": "^7.6.2", + "util": "^0.12.5", + "ws": "^8.2.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "prettier": "^2 || ^3" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + } } }, - "node_modules/@storybook/core-common/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/@storybook/core-common": { + "version": "8.6.14", + "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-8.6.14.tgz", + "integrity": "sha512-Q1rSAFnuZcisoWqE1tmLSsXtPUIC0BC00VPCdpvoeNBOyBbye4JCeARbqr3utQSMrXJJ25D8Qt9rc0f2gwbg2w==", "dev": true, - "dependencies": { - "has-flag": "^4.0.0" + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, "node_modules/@storybook/core-events": { @@ -3502,6 +2945,20 @@ "url": "https://opencollective.com/storybook" } }, + "node_modules/@storybook/core/node_modules/@storybook/theming": { + "version": "8.6.14", + "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.6.14.tgz", + "integrity": "sha512-r4y+LsiB37V5hzpQo+BM10PaCsp7YlZ0YcZzQP1OCkPlYXmUAFy2VvDKaFRpD8IeNPKug2u4iFm/laDEbs03dg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" + } + }, "node_modules/@storybook/csf": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.1.13.tgz", @@ -3612,16 +3069,6 @@ "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, - "node_modules/@storybook/node-logger": { - "version": "8.1.10", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-8.1.10.tgz", - "integrity": "sha512-djgbAROgGAvz/gr49egBxCHn1+rui57e76qa9aOMPzEBcxsGrnnKKp0uNdiNt4M7Xv6S2QHbJ2SfOlHhWmMeaA==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, "node_modules/@storybook/preview-api": { "version": "8.4.7", "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.4.7.tgz", @@ -4317,12 +3764,6 @@ "@types/node": "*" } }, - "node_modules/@types/emscripten": { - "version": "1.39.13", - "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.13.tgz", - "integrity": "sha512-cFq+fO/isvhvmuP/+Sl4K4jtU6E23DoivtbO4r50e3odaxAiVdbfSYRDdJ4gCdxx+3aRjhphS5ZMwIH4hFy/Cw==", - "dev": true - }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -4856,32 +4297,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@yarnpkg/fslib": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@yarnpkg/fslib/-/fslib-2.10.3.tgz", - "integrity": "sha512-41H+Ga78xT9sHvWLlFOZLIhtU6mTGZ20pZ29EiZa97vnxdohJD2AF42rCoAoWfqUz486xY6fhjMH+DYEM9r14A==", - "dev": true, - "dependencies": { - "@yarnpkg/libzip": "^2.3.0", - "tslib": "^1.13.0" - }, - "engines": { - "node": ">=12 <14 || 14.2 - 14.9 || >14.10.0" - } - }, - "node_modules/@yarnpkg/libzip": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/libzip/-/libzip-2.3.0.tgz", - "integrity": "sha512-6xm38yGVIa6mKm/DUCF2zFFJhERh/QWp1ufm4cNUvxsONBmfPg8uZ9pZBdOmF6qFGr/HlT6ABBkCSx/dlEtvWg==", - "dev": true, - "dependencies": { - "@types/emscripten": "^1.39.6", - "tslib": "^1.13.0" - }, - "engines": { - "node": ">=12 <14 || 14.2 - 14.9 || >14.10.0" - } - }, "node_modules/acorn": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", @@ -4999,12 +4414,6 @@ "node": ">= 8" } }, - "node_modules/app-root-dir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/app-root-dir/-/app-root-dir-1.0.2.tgz", - "integrity": "sha512-jlpIfsOoNoafl92Sz//64uQHGSyMrD2vYG5d8o2a4qGvyNCvXur7bzIsWtAC/6flI2RYAp3kv8rsfBtaLm7w0g==", - "dev": true - }, "node_modules/append-transform": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", @@ -5082,6 +4491,16 @@ "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "dev": true }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -5130,6 +4549,7 @@ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, + "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -5141,10 +4561,11 @@ } }, "node_modules/axios": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", - "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", "dev": true, + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -5343,11 +4764,32 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/better-npm-audit": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/better-npm-audit/-/better-npm-audit-3.11.0.tgz", + "integrity": "sha512-/Pt05DK6HQaRjWDc5McsCkJBZYfhgQGneKnxzPJExtRq38NttO1Hm30m0GVQeZogE94LVNBVrhWwVsoCo+at3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^8.0.0", + "dayjs": "^1.10.6", + "lodash.get": "^4.4.2", + "semver": "^7.6.3", + "table": "^6.7.1" + }, + "bin": { + "better-npm-audit": "index.js" + }, + "engines": { + "node": ">= 8.12" + } + }, "node_modules/better-opn": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz", "integrity": "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==", "dev": true, + "license": "MIT", "dependencies": { "open": "^8.0.4" }, @@ -5469,16 +4911,47 @@ } }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -5829,6 +5302,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -5952,33 +5435,6 @@ "node": ">= 8" } }, - "node_modules/crypto-random-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", - "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", - "dev": true, - "dependencies": { - "type-fest": "^1.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/crypto-random-string/node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/css-color-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", @@ -6034,6 +5490,13 @@ "node": ">=0.8" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", @@ -6128,6 +5591,7 @@ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -6145,6 +5609,7 @@ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -6298,25 +5763,19 @@ "domelementtype": "1" } }, - "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, - "engines": { - "node": ">=12" + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dotenv-expand": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", - "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", - "dev": true, "engines": { - "node": ">=12" + "node": ">= 0.4" } }, "node_modules/eastasianwidth": { @@ -6365,13 +5824,11 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -6381,6 +5838,20 @@ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, "engines": { "node": ">= 0.4" } @@ -6430,10 +5901,11 @@ } }, "node_modules/esbuild-register": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.5.0.tgz", - "integrity": "sha512-+4G/XmakeBAsvJuDugJvtyF1x+XJT4FMocynNpxrvEBViirpfUn2PgNpCHedfWhF4WokNsO/OvMKrmJOIJsI5A==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.3.4" }, @@ -7021,6 +6493,23 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -7380,12 +6869,19 @@ } }, "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, + "license": "MIT", "dependencies": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/foreground-child": { @@ -7542,16 +7038,22 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -7569,6 +7071,20 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -7706,12 +7222,13 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7729,27 +7246,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -7764,6 +7260,7 @@ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" }, @@ -7771,23 +7268,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -7800,6 +7286,7 @@ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, + "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" }, @@ -7902,6 +7389,12 @@ "node": "*" } }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", + "license": "CC0-1.0" + }, "node_modules/homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -8115,13 +7608,14 @@ } }, "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -8153,6 +7647,7 @@ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -8189,6 +7684,7 @@ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "dev": true, + "license": "MIT", "bin": { "is-docker": "cli.js" }, @@ -8227,12 +7723,16 @@ } }, "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -8277,7 +7777,26 @@ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, "engines": { - "node": ">=8" + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-stream": { @@ -8293,12 +7812,13 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, + "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -8327,6 +7847,7 @@ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, + "license": "MIT", "dependencies": { "is-docker": "^2.0.0" }, @@ -10652,6 +10173,7 @@ "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz", "integrity": "sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12.0.0" } @@ -10740,20 +10262,6 @@ "node": ">=6" } }, - "node_modules/lazy-universal-dotenv": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/lazy-universal-dotenv/-/lazy-universal-dotenv-4.0.0.tgz", - "integrity": "sha512-aXpZJRnTkpK6gQ/z4nk+ZBLd/Qdp118cvPruLSIQzQNRhKwEcdXCOzXuF55VDqIiuAaY3UGZ10DJtvZzDcvsxg==", - "dev": true, - "dependencies": { - "app-root-dir": "^1.0.2", - "dotenv": "^16.0.0", - "dotenv-expand": "^10.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -10818,12 +10326,27 @@ "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", "dev": true }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -10923,6 +10446,16 @@ "integrity": "sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==", "dev": true }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/memoizerific": { "version": "1.11.3", "resolved": "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz", @@ -11084,32 +10617,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -11454,6 +10961,7 @@ "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", "dev": true, + "license": "MIT", "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", @@ -11723,18 +11231,6 @@ "node": ">= 6" } }, - "node_modules/pkg-dir": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", - "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", - "dev": true, - "dependencies": { - "find-up": "^5.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/playwright": { "version": "1.44.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz", @@ -11792,10 +11288,11 @@ } }, "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -11973,22 +11470,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/prettier-fallback": { - "name": "prettier", - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", - "dev": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -12017,19 +11498,11 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/pretty-hrtime": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/prismjs": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", - "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", "engines": { "node": ">=6" } @@ -12039,6 +11512,7 @@ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6.0" } @@ -12303,12 +11777,14 @@ } }, "node_modules/react-syntax-highlighter": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz", - "integrity": "sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==", + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", + "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.27.0", "refractor": "^3.6.0" @@ -12432,11 +11908,6 @@ "node": ">=6" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, "node_modules/release-zalgo": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", @@ -12458,6 +11929,16 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -12664,6 +12145,24 @@ } ] }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -12673,10 +12172,11 @@ } }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -12700,6 +12200,7 @@ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -12759,6 +12260,60 @@ "node": ">=8" } }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -12855,12 +12410,13 @@ } }, "node_modules/storybook": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-8.4.7.tgz", - "integrity": "sha512-RP/nMJxiWyFc8EVMH5gp20ID032Wvk+Yr3lmKidoegto5Iy+2dVQnUoElZb2zpbVXNHWakGuAkfI0dY1Hfp/vw==", + "version": "8.6.14", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-8.6.14.tgz", + "integrity": "sha512-sVKbCj/OTx67jhmauhxc2dcr1P+yOgz/x3h0krwjyMgdc5Oubvxyg4NYDZmzAw+ym36g/lzH8N0Ccp4dwtdfxw==", "dev": true, + "license": "MIT", "dependencies": { - "@storybook/core": "8.4.7" + "@storybook/core": "8.6.14" }, "bin": { "getstorybook": "bin/index.cjs", @@ -13125,6 +12681,47 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "3.4.5", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.5.tgz", @@ -13171,57 +12768,6 @@ "memoizerific": "^1.11.3" } }, - "node_modules/temp-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", - "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", - "dev": true, - "engines": { - "node": ">=14.16" - } - }, - "node_modules/tempy": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.1.0.tgz", - "integrity": "sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==", - "dev": true, - "dependencies": { - "is-stream": "^3.0.0", - "temp-dir": "^3.0.0", - "type-fest": "^2.12.2", - "unique-string": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tempy/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tempy/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -13317,15 +12863,6 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -13338,12 +12875,6 @@ "node": ">=8.0" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true - }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -13476,40 +13007,12 @@ "node": ">=14.17" } }, - "node_modules/uglify-js": { - "version": "3.18.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.18.0.tgz", - "integrity": "sha512-SyVVbcNBCk0dzr9XL/R/ySrmYf0s372K6/hFklzgcp2lBFyXtw4I7BOdDjlLhE1aVqaI/SHWXWmYdlZxuyF38A==", - "dev": true, - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, - "node_modules/unique-string": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", - "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", - "dev": true, - "dependencies": { - "crypto-random-string": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -13629,6 +13132,7 @@ "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", @@ -13671,10 +13175,11 @@ } }, "node_modules/vite": { - "version": "5.4.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", - "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -13808,12 +13313,6 @@ "makeerror": "1.0.12" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true - }, "node_modules/webpack-sources": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", @@ -13829,16 +13328,6 @@ "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "dev": true }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -13861,15 +13350,18 @@ "dev": true }, "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, "engines": { @@ -13888,12 +13380,6 @@ "node": ">=0.10.0" } }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true - }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -14031,10 +13517,11 @@ "dev": true }, "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index 92f22ff..10bdd62 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "preview": "vite preview", "storybook": "storybook dev -p 6006", "build-storybook": "npm run build && storybook build -o dist/storybook", - "test": "test-storybook" + "test": "test-storybook", + "audit": "better-npm-audit audit --production --exclude 1102459" }, "dependencies": { "@vercel/analytics": "^1.3.1", @@ -40,6 +41,7 @@ "@typescript-eslint/parser": "^7.13.1", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.19", + "better-npm-audit": "^3.11.0", "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.7", diff --git a/src/course/02- lessons/04-PresentationalAndContainer/exercise.tsx b/src/course/02- lessons/04-PresentationalAndContainer/exercise.tsx index cb3d075..215ef12 100644 --- a/src/course/02- lessons/04-PresentationalAndContainer/exercise.tsx +++ b/src/course/02- lessons/04-PresentationalAndContainer/exercise.tsx @@ -22,6 +22,7 @@ interface IPaymentTemplate { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-empty-pattern const PaymentTemplate = ({}: IPaymentTemplate) => null; // 1B ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป - Use the Payment template and pass down the props it needs From e4b6b12586d936ee33cd2e12314f9893bcd4b2c9 Mon Sep 17 00:00:00 2001 From: Matthew Claffey Date: Tue, 20 May 2025 11:52:09 +0100 Subject: [PATCH 02/29] feat: implement level grouping (#13) * feat: implement level grouping * chore: some content changes --- .storybook/main.ts | 8 +- .storybook/preview.tsx | 7 +- package.json | 2 +- src/course/01-introduction/01-Welcome.mdx | 34 +++--- .../01-introduction/02-GettingStarted.mdx | 2 +- .../01-introduction/03-LessonStructure.mdx | 10 +- .../exercise/exercise.stories.tsx | 51 +++++++++ .../exercise}/exercise.tsx | 2 +- .../final}/final.stories.tsx | 33 ++++-- .../ConditionalRendering/final}/final.tsx | 2 +- .../ConditionalRendering}/lesson.mdx | 4 +- .../Hooks}/components.tsx | 6 +- .../Hooks/exercise}/exercise.stories.tsx | 2 +- .../Hooks/exercise}/exercise.tsx | 2 +- .../01-Bronze/Hooks/final}/final.stories.tsx | 2 +- .../01-Bronze/Hooks/final}/final.tsx | 2 +- .../{05-Hooks => 01-Bronze/Hooks}/lesson.mdx | 2 +- .../exercise}/exercise.stories.tsx | 2 +- .../exercise}/exercise.tsx | 8 +- .../final}/final.stories.tsx | 3 +- .../final}/final.tsx | 8 +- .../PresentationalAndContainer}/lesson.mdx | 2 +- .../PresentationalAndContainer}/mocks.ts | 0 .../exercise}/exercise.stories.tsx | 2 +- .../PropsCombination/exercise}/exercise.tsx | 0 .../PropsCombination/final}/final.stories.tsx | 2 +- .../PropsCombination/final}/final.tsx | 0 .../PropsCombination}/lesson.mdx | 2 +- .../Slots/exercise}/exercise.stories.tsx | 2 +- .../Slots/exercise}/exercise.tsx | 0 .../01-Bronze/Slots/final}/final.stories.tsx | 2 +- .../01-Bronze/Slots/final}/final.tsx | 2 +- .../Slots}/icons/index.tsx | 0 .../{11-Slots => 01-Bronze/Slots}/lesson.mdx | 2 +- .../exercise.stories.tsx | 38 ------- .../exercise}/components/Accordion.tsx | 0 .../exercise}/components/Accoridon.module.css | 0 .../exercise}/components/ChevronDown.tsx | 0 .../Compound/exercise}/exercise.stories.tsx | 2 +- .../Compound/exercise}/exercise.tsx | 0 .../Compound/final}/components/Accordion.tsx | 0 .../final}/components/Accoridon.module.css | 0 .../final}/components/ChevronDown.tsx | 0 .../Compound/final}/final.stories.tsx | 2 +- .../02-Silver/Compound/final}/final.tsx | 0 .../Compound}/lesson.mdx | 2 +- .../Controlled/exercise}/exercise.stories.tsx | 3 +- .../Controlled/exercise}/exercise.tsx | 2 +- .../Controlled/final/final.stories.tsx | 20 ++++ .../02-Silver/Controlled/final}/final.tsx | 2 +- .../Controlled}/lesson.mdx | 2 +- .../Portals/exercise}/components/modal.tsx | 2 +- .../Portals/exercise}/exercise.stories.tsx | 2 +- .../Portals/exercise}/exercise.tsx | 2 +- .../Portals/final}/components/modal.tsx | 2 +- .../Portals/final}/final.stories.tsx | 2 +- .../02-Silver/Portals/final}/final.tsx | 2 +- .../Portals}/lesson.mdx | 2 +- .../Provider/exercise}/Provider.tsx | 0 .../Provider/exercise}/exercise.stories.tsx | 2 +- .../Provider/exercise}/exercise.tsx | 0 .../02-Silver/Provider/final}/Provider.tsx | 2 +- .../Provider/final}/final.stories.tsx | 2 +- .../02-Silver/Provider/final}/final.tsx | 0 .../Provider}/lesson.mdx | 2 +- .../exercise}/exercise.stories.tsx | 2 +- .../RenderProps/exercise}/exercise.tsx | 6 +- .../RenderProps/final}/final.stories.tsx | 2 +- .../02-Silver/RenderProps/final}/final.tsx | 6 +- .../RenderProps}/lesson.mdx | 2 +- .../exercise}/exercise.stories.tsx | 2 +- .../StateReducer/exercise}/exercise.tsx | 0 .../StateReducer/final}/final.stories.tsx | 2 +- .../02-Silver/StateReducer/final}/final.tsx | 2 +- .../StateReducer}/lesson.mdx | 2 +- .../exercise}/exercise.stories.tsx | 3 +- .../exercise}/exercise.tsx | 0 .../exercise}/withPokemon.tsx | 0 .../final}/final.stories.tsx | 2 +- .../01-HigherOrderComponents/final}/final.tsx | 2 +- .../final}/withPokemon.tsx | 2 +- .../01-HigherOrderComponents}/lesson.mdx | 2 +- .../04-PresentationalAndContainer/mocks.ts | 104 ------------------ .../02-solutions/05-Hooks/components.tsx | 30 ----- .../10-Compound/final.stories.tsx | 20 ---- .../02-solutions/11-Slots/icons/index.tsx | 31 ------ tsconfig.app.json | 5 +- tsconfig.node.json | 5 +- vite.config.ts | 6 +- 89 files changed, 215 insertions(+), 326 deletions(-) create mode 100644 src/course/02- lessons/01-Bronze/ConditionalRendering/exercise/exercise.stories.tsx rename src/course/02- lessons/{01-ConditionalRendering => 01-Bronze/ConditionalRendering/exercise}/exercise.tsx (92%) rename src/course/{02-solutions/01-ConditionalRendering => 02- lessons/01-Bronze/ConditionalRendering/final}/final.stories.tsx (51%) rename src/course/{02-solutions/01-ConditionalRendering => 02- lessons/01-Bronze/ConditionalRendering/final}/final.tsx (89%) rename src/course/02- lessons/{01-ConditionalRendering => 01-Bronze/ConditionalRendering}/lesson.mdx (90%) rename src/course/02- lessons/{05-Hooks => 01-Bronze/Hooks}/components.tsx (71%) rename src/course/02- lessons/{08-Provider => 01-Bronze/Hooks/exercise}/exercise.stories.tsx (88%) rename src/course/02- lessons/{05-Hooks => 01-Bronze/Hooks/exercise}/exercise.tsx (97%) rename src/course/{02-solutions/03-RenderProps => 02- lessons/01-Bronze/Hooks/final}/final.stories.tsx (88%) rename src/course/{02-solutions/05-Hooks => 02- lessons/01-Bronze/Hooks/final}/final.tsx (96%) rename src/course/02- lessons/{05-Hooks => 01-Bronze/Hooks}/lesson.mdx (98%) rename src/course/02- lessons/{04-PresentationalAndContainer => 01-Bronze/PresentationalAndContainer/exercise}/exercise.stories.tsx (86%) rename src/course/02- lessons/{04-PresentationalAndContainer => 01-Bronze/PresentationalAndContainer/exercise}/exercise.tsx (93%) rename src/course/{02-solutions/04-PresentationalAndContainer => 02- lessons/01-Bronze/PresentationalAndContainer/final}/final.stories.tsx (85%) rename src/course/{02-solutions/04-PresentationalAndContainer => 02- lessons/01-Bronze/PresentationalAndContainer/final}/final.tsx (93%) rename src/course/02- lessons/{04-PresentationalAndContainer => 01-Bronze/PresentationalAndContainer}/lesson.mdx (98%) rename src/course/02- lessons/{04-PresentationalAndContainer => 01-Bronze/PresentationalAndContainer}/mocks.ts (100%) rename src/course/02- lessons/{02-PropsCombination => 01-Bronze/PropsCombination/exercise}/exercise.stories.tsx (94%) rename src/course/02- lessons/{02-PropsCombination => 01-Bronze/PropsCombination/exercise}/exercise.tsx (100%) rename src/course/{02-solutions/02-PropsCombination => 02- lessons/01-Bronze/PropsCombination/final}/final.stories.tsx (94%) rename src/course/{02-solutions/02-PropsCombination => 02- lessons/01-Bronze/PropsCombination/final}/final.tsx (100%) rename src/course/02- lessons/{02-PropsCombination => 01-Bronze/PropsCombination}/lesson.mdx (96%) rename src/course/02- lessons/{11-Slots => 01-Bronze/Slots/exercise}/exercise.stories.tsx (90%) rename src/course/02- lessons/{11-Slots => 01-Bronze/Slots/exercise}/exercise.tsx (100%) rename src/course/{02-solutions/11-Slots => 02- lessons/01-Bronze/Slots/final}/final.stories.tsx (90%) rename src/course/{02-solutions/11-Slots => 02- lessons/01-Bronze/Slots/final}/final.tsx (96%) rename src/course/02- lessons/{11-Slots => 01-Bronze/Slots}/icons/index.tsx (100%) rename src/course/02- lessons/{11-Slots => 01-Bronze/Slots}/lesson.mdx (97%) delete mode 100644 src/course/02- lessons/01-ConditionalRendering/exercise.stories.tsx rename src/course/02- lessons/{10-Compound => 02-Silver/Compound/exercise}/components/Accordion.tsx (100%) rename src/course/02- lessons/{10-Compound => 02-Silver/Compound/exercise}/components/Accoridon.module.css (100%) rename src/course/02- lessons/{10-Compound => 02-Silver/Compound/exercise}/components/ChevronDown.tsx (100%) rename src/course/02- lessons/{06-Controlled => 02-Silver/Compound/exercise}/exercise.stories.tsx (86%) rename src/course/02- lessons/{10-Compound => 02-Silver/Compound/exercise}/exercise.tsx (100%) rename src/course/{02-solutions/10-Compound => 02- lessons/02-Silver/Compound/final}/components/Accordion.tsx (100%) rename src/course/{02-solutions/10-Compound => 02- lessons/02-Silver/Compound/final}/components/Accoridon.module.css (100%) rename src/course/{02-solutions/10-Compound => 02- lessons/02-Silver/Compound/final}/components/ChevronDown.tsx (100%) rename src/course/{02-solutions/06-Controlled => 02- lessons/02-Silver/Compound/final}/final.stories.tsx (86%) rename src/course/{02-solutions/10-Compound => 02- lessons/02-Silver/Compound/final}/final.tsx (100%) rename src/course/02- lessons/{10-Compound => 02-Silver/Compound}/lesson.mdx (96%) rename src/course/02- lessons/{05-Hooks => 02-Silver/Controlled/exercise}/exercise.stories.tsx (85%) rename src/course/02- lessons/{06-Controlled => 02-Silver/Controlled/exercise}/exercise.tsx (98%) create mode 100644 src/course/02- lessons/02-Silver/Controlled/final/final.stories.tsx rename src/course/{02-solutions/06-Controlled => 02- lessons/02-Silver/Controlled/final}/final.tsx (96%) rename src/course/02- lessons/{06-Controlled => 02-Silver/Controlled}/lesson.mdx (96%) rename src/course/02- lessons/{12-Portals => 02-Silver/Portals/exercise}/components/modal.tsx (96%) rename src/course/02- lessons/{12-Portals => 02-Silver/Portals/exercise}/exercise.stories.tsx (89%) rename src/course/02- lessons/{12-Portals => 02-Silver/Portals/exercise}/exercise.tsx (96%) rename src/course/{02-solutions/12-Portals => 02- lessons/02-Silver/Portals/final}/components/modal.tsx (95%) rename src/course/{02-solutions/12-Portals => 02- lessons/02-Silver/Portals/final}/final.stories.tsx (89%) rename src/course/{02-solutions/12-Portals => 02- lessons/02-Silver/Portals/final}/final.tsx (96%) rename src/course/02- lessons/{12-Portals => 02-Silver/Portals}/lesson.mdx (96%) rename src/course/02- lessons/{08-Provider => 02-Silver/Provider/exercise}/Provider.tsx (100%) rename src/course/02- lessons/{09-StateReducer => 02-Silver/Provider/exercise}/exercise.stories.tsx (88%) rename src/course/02- lessons/{08-Provider => 02-Silver/Provider/exercise}/exercise.tsx (100%) rename src/course/{02-solutions/08-Provider => 02- lessons/02-Silver/Provider/final}/Provider.tsx (95%) rename src/course/{02-solutions/09-StateReducer => 02- lessons/02-Silver/Provider/final}/final.stories.tsx (88%) rename src/course/{02-solutions/08-Provider => 02- lessons/02-Silver/Provider/final}/final.tsx (100%) rename src/course/02- lessons/{08-Provider => 02-Silver/Provider}/lesson.mdx (96%) rename src/course/02- lessons/{10-Compound => 02-Silver/RenderProps/exercise}/exercise.stories.tsx (87%) rename src/course/02- lessons/{03-RenderProps => 02-Silver/RenderProps/exercise}/exercise.tsx (91%) rename src/course/{02-solutions/08-Provider => 02- lessons/02-Silver/RenderProps/final}/final.stories.tsx (87%) rename src/course/{02-solutions/03-RenderProps => 02- lessons/02-Silver/RenderProps/final}/final.tsx (90%) rename src/course/02- lessons/{03-RenderProps => 02-Silver/RenderProps}/lesson.mdx (96%) rename src/course/02- lessons/{03-RenderProps => 02-Silver/StateReducer/exercise}/exercise.stories.tsx (87%) rename src/course/02- lessons/{09-StateReducer => 02-Silver/StateReducer/exercise}/exercise.tsx (100%) rename src/course/{02-solutions/05-Hooks => 02- lessons/02-Silver/StateReducer/final}/final.stories.tsx (87%) rename src/course/{02-solutions/09-StateReducer => 02- lessons/02-Silver/StateReducer/final}/final.tsx (97%) rename src/course/02- lessons/{09-StateReducer => 02-Silver/StateReducer}/lesson.mdx (97%) rename src/course/02- lessons/{07-HigherOrderComponents => 03-Gold/01-HigherOrderComponents/exercise}/exercise.stories.tsx (87%) rename src/course/02- lessons/{07-HigherOrderComponents => 03-Gold/01-HigherOrderComponents/exercise}/exercise.tsx (100%) rename src/course/02- lessons/{07-HigherOrderComponents => 03-Gold/01-HigherOrderComponents/exercise}/withPokemon.tsx (100%) rename src/course/{02-solutions/07-HigherOrderComponents => 02- lessons/03-Gold/01-HigherOrderComponents/final}/final.stories.tsx (86%) rename src/course/{02-solutions/07-HigherOrderComponents => 02- lessons/03-Gold/01-HigherOrderComponents/final}/final.tsx (95%) rename src/course/{02-solutions/07-HigherOrderComponents => 02- lessons/03-Gold/01-HigherOrderComponents/final}/withPokemon.tsx (94%) rename src/course/02- lessons/{07-HigherOrderComponents => 03-Gold/01-HigherOrderComponents}/lesson.mdx (96%) delete mode 100644 src/course/02-solutions/04-PresentationalAndContainer/mocks.ts delete mode 100644 src/course/02-solutions/05-Hooks/components.tsx delete mode 100644 src/course/02-solutions/10-Compound/final.stories.tsx delete mode 100644 src/course/02-solutions/11-Slots/icons/index.tsx diff --git a/.storybook/main.ts b/.storybook/main.ts index ed55f6b..3ade951 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,5 +1,6 @@ import type { StorybookConfig } from '@storybook/react-vite'; import { mergeConfig } from 'vite'; +import path from 'path'; const config: StorybookConfig = { stories: [ @@ -27,7 +28,12 @@ const config: StorybookConfig = { }, async viteFinal(baseConfig, { configType }) { return mergeConfig(baseConfig, { - ...(configType === 'PRODUCTION' ? { base: '/storybook/' } : {}) + ...(configType === 'PRODUCTION' ? { base: '/storybook/' } : {}), + resolve: { + alias: { + '@shared': path.resolve(__dirname, '../src', 'shared') + } + } }); } }; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 2e15d5f..9ef9680 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -12,7 +12,12 @@ const preview: Preview = { code: Code } }, - storySort: ['Introduction', 'Lessons', 'Recipes'], + storySort: [ + 'Introduction', + 'Lessons', + ['๐Ÿฅ‰ Bronze', '๐Ÿฅˆ Silver', '๐Ÿฅ‡ Gold'], + 'Recipes' + ], controls: { matchers: { color: /(background|color)$/i, diff --git a/package.json b/package.json index 10bdd62..40ed1d8 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives", "preview": "vite preview", "storybook": "storybook dev -p 6006", "build-storybook": "npm run build && storybook build -o dist/storybook", diff --git a/src/course/01-introduction/01-Welcome.mdx b/src/course/01-introduction/01-Welcome.mdx index fe55e20..87e2a82 100644 --- a/src/course/01-introduction/01-Welcome.mdx +++ b/src/course/01-introduction/01-Welcome.mdx @@ -34,22 +34,30 @@ I will be going in high detail on each of the lessons so if there are some holes ## Contents -Each lesson is broken down in an exercise file and a final file. The exercise file will have instructions in pseudo format to help guide you through the code snippets. Not to worry though, there will be videos to support you along the way. +Each lesson is broken down in an exercise file and a final file. The exercise file will have instructions in pseudo format to help guide you through the code snippets. ### Lessons -- 01 - [Conditionally rendering pattern](?path=/docs/lessons-01-conditional-rendering-pattern-01-lesson--docs) -- 02 - [Props combination pattern](?path=/docs/lessons-02-props-combination-pattern-01-lesson--docs) -- 03 - [Render props pattern](?path=/docs/lessons-03-render-props-pattern-01-lesson--docs) -- 04 - [Presentational and container components pattern](?path=/docs/lessons-04-presentational-container-pattern-01-lesson--docs) -- 05 - [React Hooks pattern](?path=/docs/lessons-05-hooks-pattern-01-lesson--docs) -- 06 - [Controlled component pattern](?path=/docs/lessons-06-controlled-components-pattern-01-lesson--docs) -- 07 - [Higher order component](?path=/docs/lessons-07-higher-order-components-pattern-01-lesson--docs) -- 08 - [The Provider pattern](?path=/docs/lessons-08-provider-pattern-01-lesson--docs) -- 09 - [The State Reducer pattern](?path=/docs/lessons-09-state-reducer-pattern-01-lesson--docs) -- 10 - [Compound components pattern](?path=/docs/lessons-10-compound-components-pattern-01-lesson--docs) -- 11 - [Slots pattern](?path=/docs/lessons-11-slots-01-lesson--docs) -- 12 - [Portals pattern](?path=/docs/lessons-12-portals-01-lesson--docs) +#### ๐Ÿฅ‰ Bronze + +- [Conditionally rendering pattern](?path=/docs/lessons-๐Ÿฅ‰-bronze-conditional-rendering-pattern-01-lesson--docs) +- [Props combination pattern](?path=/docs/lessons-๐Ÿฅ‰-bronze-props-combination-pattern-01-lesson--docs) +- [React Hooks pattern](?path=/docs/lessons-๐Ÿฅ‰-bronze-hooks-pattern-01-lesson--docs) +- [Presentational and container components pattern](?path=/docs/lessons-๐Ÿฅ‰-bronze-presentational-container-pattern-01-lesson--docs) +- [Slots pattern](?path=/docs/lessons-๐Ÿฅ‰-bronze-slots-01-lesson--docs) + +#### ๐Ÿฅˆ Silver + +- [Compound components pattern](?path=/docs/lessons-๐Ÿฅˆ-sliver-compound-components-pattern-01-lesson--docs) +- [Controlled component pattern](?path=/docs/lessons-๐Ÿฅˆ-sliver-controlled-components-pattern-01-lesson--docs) +- [Render props pattern](?path=/docs/lessons-๐Ÿฅˆ-sliver-render-props-pattern-01-lesson--docs) +- [The Provider pattern](?path=/docs/lessons-๐Ÿฅˆ-sliver-provider-pattern-01-lesson--docs) +- [The State Reducer pattern](?path=/docs/lessons-๐Ÿฅˆ-sliver-state-reducer-pattern-01-lesson--docs) +- [Portals pattern](?path=/docs/lessons-๐Ÿฅˆ-sliver-portals-01-lesson--docs) + +#### ๐Ÿฅ‡ Gold + +- [Higher order component](?path=/docs/lessons-๐Ÿฅ‡-gold-higher-order-components-pattern-01-lesson--docs) ## FAQs diff --git a/src/course/01-introduction/02-GettingStarted.mdx b/src/course/01-introduction/02-GettingStarted.mdx index 2e084b0..966de2a 100644 --- a/src/course/01-introduction/02-GettingStarted.mdx +++ b/src/course/01-introduction/02-GettingStarted.mdx @@ -4,7 +4,7 @@ import { Meta } from '@storybook/blocks'; # Getting Started -> Node version 18 required. +> Node version 18+ required. ## Installation diff --git a/src/course/01-introduction/03-LessonStructure.mdx b/src/course/01-introduction/03-LessonStructure.mdx index 8626b76..43fa999 100644 --- a/src/course/01-introduction/03-LessonStructure.mdx +++ b/src/course/01-introduction/03-LessonStructure.mdx @@ -4,7 +4,11 @@ import { Meta } from '@storybook/blocks'; # Lesson Structure -As you will have already noticed in the sidebar that there is a "Lessons" section. Each lesson will get slightly more and more complex as we go on so it eases us into the course. Each lesson will also contain an "exercise.(tsx)" file and a "final.(tsx)". +As you will have already noticed in the sidebar that there is a "Lessons" section. Each lesson sits within a Bronze/Silver/Gold tier folder which mirrors to the complexity of that pattern & we provide more challenging exercises. Each lesson will also contain an "exercise" folder and a "final" folder. + +## Storybook / Folder Structure + +As you can see the storybook sidebar mirrors the way the folder structure is within the repo. This is done so you can easily navigate to the files you are changing within the exercises. ## Exercise files @@ -60,6 +64,4 @@ const Component = () => { ## Final files -If you get stuck do not worry! Each lesson.mdx file will have a video going through it all and there will be a final.tsx file showing the final solution of each exercise. - -[Let's get started](?path=/docs/lessons-01-conditional-rendering-pattern-01-lesson--docs) +If you get stuck do not worry! Each will have a final folder in the lesson showing the final solution of each exercise. Head over to any of the lessons to get started with which patterns you wish to learn. diff --git a/src/course/02- lessons/01-Bronze/ConditionalRendering/exercise/exercise.stories.tsx b/src/course/02- lessons/01-Bronze/ConditionalRendering/exercise/exercise.stories.tsx new file mode 100644 index 0000000..b8777c9 --- /dev/null +++ b/src/course/02- lessons/01-Bronze/ConditionalRendering/exercise/exercise.stories.tsx @@ -0,0 +1,51 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { userEvent, within, expect } from '@storybook/test'; + +import { ComponentOne } from './exercise'; + +const meta: Meta = { + title: + 'Lessons/๐Ÿฅ‰ Bronze/Conditional Rendering Pattern/02-Exercise', + component: ComponentOne +}; + +export default meta; +type Story = StoryObj; + +const username = 'John Doe'; + +/* + * See https://storybook.js.org/docs/writing-stories/play-function#working-with-the-canvas + * to learn more about using the canvasElement to query the DOM + */ +export const Default: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await userEvent.click( + canvas.getByRole('button', { name: 'Login' }) + ); + + await expect( + canvas.getByText(`Welcome ${username}`) + ).toBeInTheDocument(); + await expect( + canvas.queryByRole('button', { name: 'Login' }) + ).toBeNull(); + + await userEvent.click( + canvas.getByRole('button', { name: 'Logout' }) + ); + + await expect( + canvas.queryByText(`Welcome ${username}`) + ).toBeNull(); + await expect( + canvas.queryByRole('button', { name: 'Logout' }) + ).toBeNull(); + }, + args: { + username + } +}; diff --git a/src/course/02- lessons/01-ConditionalRendering/exercise.tsx b/src/course/02- lessons/01-Bronze/ConditionalRendering/exercise/exercise.tsx similarity index 92% rename from src/course/02- lessons/01-ConditionalRendering/exercise.tsx rename to src/course/02- lessons/01-Bronze/ConditionalRendering/exercise/exercise.tsx index 9fdbe6a..5bb0e63 100644 --- a/src/course/02- lessons/01-ConditionalRendering/exercise.tsx +++ b/src/course/02- lessons/01-Bronze/ConditionalRendering/exercise/exercise.tsx @@ -1,4 +1,4 @@ -import { Button } from '../../../shared/components/Button/Button.component'; +import { Button } from '@shared/components/Button/Button.component'; interface IComponentProps { username: string; diff --git a/src/course/02-solutions/01-ConditionalRendering/final.stories.tsx b/src/course/02- lessons/01-Bronze/ConditionalRendering/final/final.stories.tsx similarity index 51% rename from src/course/02-solutions/01-ConditionalRendering/final.stories.tsx rename to src/course/02- lessons/01-Bronze/ConditionalRendering/final/final.stories.tsx index a17bb30..7052c79 100644 --- a/src/course/02-solutions/01-ConditionalRendering/final.stories.tsx +++ b/src/course/02- lessons/01-Bronze/ConditionalRendering/final/final.stories.tsx @@ -1,11 +1,10 @@ import type { Meta, StoryObj } from '@storybook/react'; import { userEvent, within, expect } from '@storybook/test'; - import { ComponentOne } from './final'; const meta: Meta = { - title: 'Lessons/01 - Conditional Rendering Pattern/03-Final', + title: 'Lessons/๐Ÿฅ‰ Bronze/Conditional Rendering Pattern/03-Final', component: ComponentOne }; @@ -22,15 +21,27 @@ export const Default: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await userEvent.click(canvas.getByRole('button', { name: 'Login' })); - - await expect(canvas.getByText(`Welcome ${username}`)).toBeInTheDocument(); - await expect(canvas.queryByRole('button', { name: 'Login' })).toBeNull(); - - await userEvent.click(canvas.getByRole('button', { name: 'Logout' })); - - await expect(canvas.queryByText(`Welcome ${username}`)).toBeNull(); - await expect(canvas.queryByRole('button', { name: 'Logout' })).toBeNull(); + await userEvent.click( + canvas.getByRole('button', { name: 'Login' }) + ); + + await expect( + canvas.getByText(`Welcome ${username}`) + ).toBeInTheDocument(); + await expect( + canvas.queryByRole('button', { name: 'Login' }) + ).toBeNull(); + + await userEvent.click( + canvas.getByRole('button', { name: 'Logout' }) + ); + + await expect( + canvas.queryByText(`Welcome ${username}`) + ).toBeNull(); + await expect( + canvas.queryByRole('button', { name: 'Logout' }) + ).toBeNull(); }, args: { username diff --git a/src/course/02-solutions/01-ConditionalRendering/final.tsx b/src/course/02- lessons/01-Bronze/ConditionalRendering/final/final.tsx similarity index 89% rename from src/course/02-solutions/01-ConditionalRendering/final.tsx rename to src/course/02- lessons/01-Bronze/ConditionalRendering/final/final.tsx index 0a9f76d..d2779ef 100644 --- a/src/course/02-solutions/01-ConditionalRendering/final.tsx +++ b/src/course/02- lessons/01-Bronze/ConditionalRendering/final/final.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { Button } from '../../../shared/components/Button/Button.component'; +import { Button } from '@shared/components/Button/Button.component'; interface IComponentProps { username: string; diff --git a/src/course/02- lessons/01-ConditionalRendering/lesson.mdx b/src/course/02- lessons/01-Bronze/ConditionalRendering/lesson.mdx similarity index 90% rename from src/course/02- lessons/01-ConditionalRendering/lesson.mdx rename to src/course/02- lessons/01-Bronze/ConditionalRendering/lesson.mdx index a27307e..ccecd36 100644 --- a/src/course/02- lessons/01-ConditionalRendering/lesson.mdx +++ b/src/course/02- lessons/01-Bronze/ConditionalRendering/lesson.mdx @@ -1,6 +1,6 @@ import { Meta } from '@storybook/blocks'; - + # Conditional Rendering Pattern @@ -86,7 +86,7 @@ const Component = () => { ## Exercise -In the first exercise we are going to look into building a login and logout toggle which will render a username when they have logged in. Go to the exercise.tsx inside the 01-ConditionalRendering folder and start the exercise. Once completed, the Tests will show as passed in the storybook "Interactions" addon section. +In the first exercise we are going to look into building a login and logout toggle which will render a username when they have logged in. Go to the exercise.tsx inside the ConditionalRendering folder and start the exercise. Once completed, the Tests will show as passed in the storybook "Interactions" addon section. ## Feedback diff --git a/src/course/02- lessons/05-Hooks/components.tsx b/src/course/02- lessons/01-Bronze/Hooks/components.tsx similarity index 71% rename from src/course/02- lessons/05-Hooks/components.tsx rename to src/course/02- lessons/01-Bronze/Hooks/components.tsx index 2adee81..63591b5 100644 --- a/src/course/02- lessons/05-Hooks/components.tsx +++ b/src/course/02- lessons/01-Bronze/Hooks/components.tsx @@ -1,6 +1,6 @@ -import { Input } from '../../../shared/components/Input/Input.component'; -import { Label } from '../../../shared/components/Label/Label.component'; -import { ErrorMessage } from '../../../shared/components/ErrorMessage/ErrorMessage.component'; +import { Input } from '@shared/components/Input/Input.component'; +import { Label } from '@shared/components/Label/Label.component'; +import { ErrorMessage } from '@shared/components/ErrorMessage/ErrorMessage.component'; import { HTMLAttributes } from 'react'; export interface ITextFieldProps { diff --git a/src/course/02- lessons/08-Provider/exercise.stories.tsx b/src/course/02- lessons/01-Bronze/Hooks/exercise/exercise.stories.tsx similarity index 88% rename from src/course/02- lessons/08-Provider/exercise.stories.tsx rename to src/course/02- lessons/01-Bronze/Hooks/exercise/exercise.stories.tsx index 6feb89c..2407fb0 100644 --- a/src/course/02- lessons/08-Provider/exercise.stories.tsx +++ b/src/course/02- lessons/01-Bronze/Hooks/exercise/exercise.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Exercise } from './exercise'; const meta: Meta = { - title: 'Lessons/08 - Provider Pattern/02-Exercise', + title: 'Lessons/๐Ÿฅ‰ Bronze/Hooks Pattern/02-Exercise', component: Exercise }; diff --git a/src/course/02- lessons/05-Hooks/exercise.tsx b/src/course/02- lessons/01-Bronze/Hooks/exercise/exercise.tsx similarity index 97% rename from src/course/02- lessons/05-Hooks/exercise.tsx rename to src/course/02- lessons/01-Bronze/Hooks/exercise/exercise.tsx index 7191929..875f1bb 100644 --- a/src/course/02- lessons/05-Hooks/exercise.tsx +++ b/src/course/02- lessons/01-Bronze/Hooks/exercise/exercise.tsx @@ -1,5 +1,5 @@ import { ChangeEvent, useState } from 'react'; -import { ITextFieldProps, TextFieldComponent } from './components'; +import { ITextFieldProps, TextFieldComponent } from '../components'; /* * Observations diff --git a/src/course/02-solutions/03-RenderProps/final.stories.tsx b/src/course/02- lessons/01-Bronze/Hooks/final/final.stories.tsx similarity index 88% rename from src/course/02-solutions/03-RenderProps/final.stories.tsx rename to src/course/02- lessons/01-Bronze/Hooks/final/final.stories.tsx index 38c16c2..bc3c325 100644 --- a/src/course/02-solutions/03-RenderProps/final.stories.tsx +++ b/src/course/02- lessons/01-Bronze/Hooks/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/03 - Render Props Pattern/03-Final', + title: 'Lessons/๐Ÿฅ‰ Bronze/Hooks Pattern/03-Final', component: Final }; diff --git a/src/course/02-solutions/05-Hooks/final.tsx b/src/course/02- lessons/01-Bronze/Hooks/final/final.tsx similarity index 96% rename from src/course/02-solutions/05-Hooks/final.tsx rename to src/course/02- lessons/01-Bronze/Hooks/final/final.tsx index 95f7cad..ec340b2 100644 --- a/src/course/02-solutions/05-Hooks/final.tsx +++ b/src/course/02- lessons/01-Bronze/Hooks/final/final.tsx @@ -1,5 +1,5 @@ import { ChangeEvent, useState } from 'react'; -import { TextFieldComponent } from './components'; +import { TextFieldComponent } from '../components'; interface IFieldProps { name: string; diff --git a/src/course/02- lessons/05-Hooks/lesson.mdx b/src/course/02- lessons/01-Bronze/Hooks/lesson.mdx similarity index 98% rename from src/course/02- lessons/05-Hooks/lesson.mdx rename to src/course/02- lessons/01-Bronze/Hooks/lesson.mdx index a55afb9..6ab9443 100644 --- a/src/course/02- lessons/05-Hooks/lesson.mdx +++ b/src/course/02- lessons/01-Bronze/Hooks/lesson.mdx @@ -1,6 +1,6 @@ import { Meta } from '@storybook/blocks'; - + # Hooks Pattern diff --git a/src/course/02- lessons/04-PresentationalAndContainer/exercise.stories.tsx b/src/course/02- lessons/01-Bronze/PresentationalAndContainer/exercise/exercise.stories.tsx similarity index 86% rename from src/course/02- lessons/04-PresentationalAndContainer/exercise.stories.tsx rename to src/course/02- lessons/01-Bronze/PresentationalAndContainer/exercise/exercise.stories.tsx index d3892a6..0f004ef 100644 --- a/src/course/02- lessons/04-PresentationalAndContainer/exercise.stories.tsx +++ b/src/course/02- lessons/01-Bronze/PresentationalAndContainer/exercise/exercise.stories.tsx @@ -4,7 +4,7 @@ import { BrandPageOne, BrandPageTwo } from './exercise'; const meta: Meta = { title: - 'Lessons/04 - Presentational & Container Pattern/02-Exercise', + 'Lessons/๐Ÿฅ‰ Bronze/Presentational & Container Pattern/02-Exercise', component: BrandPageOne }; diff --git a/src/course/02- lessons/04-PresentationalAndContainer/exercise.tsx b/src/course/02- lessons/01-Bronze/PresentationalAndContainer/exercise/exercise.tsx similarity index 93% rename from src/course/02- lessons/04-PresentationalAndContainer/exercise.tsx rename to src/course/02- lessons/01-Bronze/PresentationalAndContainer/exercise/exercise.tsx index 215ef12..9b199da 100644 --- a/src/course/02- lessons/04-PresentationalAndContainer/exercise.tsx +++ b/src/course/02- lessons/01-Bronze/PresentationalAndContainer/exercise/exercise.tsx @@ -1,12 +1,12 @@ import { useEffect, useState } from 'react'; -import { ErrorMessage } from '../../../shared/components/ErrorMessage/ErrorMessage.component'; +import { ErrorMessage } from '@shared/components/ErrorMessage/ErrorMessage.component'; import { ICheckoutData, useBrandOnePayment, useCheckout -} from './mocks'; -import { Skeleton } from '../../../shared/components/Skeleton/Skeleton.component'; -import { Button } from '../../../shared/components/Button/Button.component'; +} from '../mocks'; +import { Skeleton } from '@shared/components/Skeleton/Skeleton.component'; +import { Button } from '@shared/components/Button/Button.component'; interface IPaymentTemplate { hasPaymentFailed: boolean; diff --git a/src/course/02-solutions/04-PresentationalAndContainer/final.stories.tsx b/src/course/02- lessons/01-Bronze/PresentationalAndContainer/final/final.stories.tsx similarity index 85% rename from src/course/02-solutions/04-PresentationalAndContainer/final.stories.tsx rename to src/course/02- lessons/01-Bronze/PresentationalAndContainer/final/final.stories.tsx index a6bd4a5..448e8fe 100644 --- a/src/course/02-solutions/04-PresentationalAndContainer/final.stories.tsx +++ b/src/course/02- lessons/01-Bronze/PresentationalAndContainer/final/final.stories.tsx @@ -3,7 +3,8 @@ import type { Meta, StoryObj } from '@storybook/react'; import { BrandPageOne, BrandPageTwo } from './final'; const meta: Meta = { - title: 'Lessons/04 - Presentational & Container Pattern/03-Final', + title: + 'Lessons/๐Ÿฅ‰ Bronze/Presentational & Container Pattern/03-Final', component: BrandPageOne }; diff --git a/src/course/02-solutions/04-PresentationalAndContainer/final.tsx b/src/course/02- lessons/01-Bronze/PresentationalAndContainer/final/final.tsx similarity index 93% rename from src/course/02-solutions/04-PresentationalAndContainer/final.tsx rename to src/course/02- lessons/01-Bronze/PresentationalAndContainer/final/final.tsx index 028dc5e..e539017 100644 --- a/src/course/02-solutions/04-PresentationalAndContainer/final.tsx +++ b/src/course/02- lessons/01-Bronze/PresentationalAndContainer/final/final.tsx @@ -1,13 +1,13 @@ import { useEffect, useState } from 'react'; -import { ErrorMessage } from '../../../shared/components/ErrorMessage/ErrorMessage.component'; +import { ErrorMessage } from '@shared/components/ErrorMessage/ErrorMessage.component'; import { useBrandOnePayment, useCheckout, useBrandTwoPayment, ICheckoutData -} from './mocks'; -import { Button } from '../../../shared/components/Button/Button.component'; -import { Skeleton } from '../../../shared/components/Skeleton/Skeleton.component'; +} from '../mocks'; +import { Button } from '@shared/components/Button/Button.component'; +import { Skeleton } from '@shared/components/Skeleton/Skeleton.component'; interface IPaymentTemplate { hasPaymentFailed: boolean; diff --git a/src/course/02- lessons/04-PresentationalAndContainer/lesson.mdx b/src/course/02- lessons/01-Bronze/PresentationalAndContainer/lesson.mdx similarity index 98% rename from src/course/02- lessons/04-PresentationalAndContainer/lesson.mdx rename to src/course/02- lessons/01-Bronze/PresentationalAndContainer/lesson.mdx index b784a02..080b3ae 100644 --- a/src/course/02- lessons/04-PresentationalAndContainer/lesson.mdx +++ b/src/course/02- lessons/01-Bronze/PresentationalAndContainer/lesson.mdx @@ -1,6 +1,6 @@ import { Meta } from '@storybook/blocks'; - + # Presentational & Container Pattern diff --git a/src/course/02- lessons/04-PresentationalAndContainer/mocks.ts b/src/course/02- lessons/01-Bronze/PresentationalAndContainer/mocks.ts similarity index 100% rename from src/course/02- lessons/04-PresentationalAndContainer/mocks.ts rename to src/course/02- lessons/01-Bronze/PresentationalAndContainer/mocks.ts diff --git a/src/course/02- lessons/02-PropsCombination/exercise.stories.tsx b/src/course/02- lessons/01-Bronze/PropsCombination/exercise/exercise.stories.tsx similarity index 94% rename from src/course/02- lessons/02-PropsCombination/exercise.stories.tsx rename to src/course/02- lessons/01-Bronze/PropsCombination/exercise/exercise.stories.tsx index 0b6a2f8..3a85de5 100644 --- a/src/course/02- lessons/02-PropsCombination/exercise.stories.tsx +++ b/src/course/02- lessons/01-Bronze/PropsCombination/exercise/exercise.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Exercise } from './exercise'; const meta: Meta = { - title: 'Lessons/02 - Props Combination Pattern/02-Exercise', + title: 'Lessons/๐Ÿฅ‰ Bronze/Props Combination Pattern/02-Exercise', component: Exercise }; diff --git a/src/course/02- lessons/02-PropsCombination/exercise.tsx b/src/course/02- lessons/01-Bronze/PropsCombination/exercise/exercise.tsx similarity index 100% rename from src/course/02- lessons/02-PropsCombination/exercise.tsx rename to src/course/02- lessons/01-Bronze/PropsCombination/exercise/exercise.tsx diff --git a/src/course/02-solutions/02-PropsCombination/final.stories.tsx b/src/course/02- lessons/01-Bronze/PropsCombination/final/final.stories.tsx similarity index 94% rename from src/course/02-solutions/02-PropsCombination/final.stories.tsx rename to src/course/02- lessons/01-Bronze/PropsCombination/final/final.stories.tsx index 6b5a5fe..fe71bb7 100644 --- a/src/course/02-solutions/02-PropsCombination/final.stories.tsx +++ b/src/course/02- lessons/01-Bronze/PropsCombination/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/02 - Props Combination Pattern/03-Final', + title: 'Lessons/๐Ÿฅ‰ Bronze/Props Combination Pattern/03-Final', component: Final }; diff --git a/src/course/02-solutions/02-PropsCombination/final.tsx b/src/course/02- lessons/01-Bronze/PropsCombination/final/final.tsx similarity index 100% rename from src/course/02-solutions/02-PropsCombination/final.tsx rename to src/course/02- lessons/01-Bronze/PropsCombination/final/final.tsx diff --git a/src/course/02- lessons/02-PropsCombination/lesson.mdx b/src/course/02- lessons/01-Bronze/PropsCombination/lesson.mdx similarity index 96% rename from src/course/02- lessons/02-PropsCombination/lesson.mdx rename to src/course/02- lessons/01-Bronze/PropsCombination/lesson.mdx index 475e56a..2115d3c 100644 --- a/src/course/02- lessons/02-PropsCombination/lesson.mdx +++ b/src/course/02- lessons/01-Bronze/PropsCombination/lesson.mdx @@ -1,6 +1,6 @@ import { Meta } from '@storybook/blocks'; - + # Props Combination Pattern diff --git a/src/course/02- lessons/11-Slots/exercise.stories.tsx b/src/course/02- lessons/01-Bronze/Slots/exercise/exercise.stories.tsx similarity index 90% rename from src/course/02- lessons/11-Slots/exercise.stories.tsx rename to src/course/02- lessons/01-Bronze/Slots/exercise/exercise.stories.tsx index c3521e5..a40b57b 100644 --- a/src/course/02- lessons/11-Slots/exercise.stories.tsx +++ b/src/course/02- lessons/01-Bronze/Slots/exercise/exercise.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Exercise } from './exercise'; const meta: Meta = { - title: 'Lessons/11 - Slots/02-Exercise', + title: 'Lessons/๐Ÿฅ‰ Bronze/Slots/02-Exercise', component: Exercise }; diff --git a/src/course/02- lessons/11-Slots/exercise.tsx b/src/course/02- lessons/01-Bronze/Slots/exercise/exercise.tsx similarity index 100% rename from src/course/02- lessons/11-Slots/exercise.tsx rename to src/course/02- lessons/01-Bronze/Slots/exercise/exercise.tsx diff --git a/src/course/02-solutions/11-Slots/final.stories.tsx b/src/course/02- lessons/01-Bronze/Slots/final/final.stories.tsx similarity index 90% rename from src/course/02-solutions/11-Slots/final.stories.tsx rename to src/course/02- lessons/01-Bronze/Slots/final/final.stories.tsx index dc71714..8fb0fda 100644 --- a/src/course/02-solutions/11-Slots/final.stories.tsx +++ b/src/course/02- lessons/01-Bronze/Slots/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/11 - Slots/03-Final', + title: 'Lessons/๐Ÿฅ‰ Bronze/Slots/03-Final', component: Final }; diff --git a/src/course/02-solutions/11-Slots/final.tsx b/src/course/02- lessons/01-Bronze/Slots/final/final.tsx similarity index 96% rename from src/course/02-solutions/11-Slots/final.tsx rename to src/course/02- lessons/01-Bronze/Slots/final/final.tsx index fb91eb8..b34f17c 100644 --- a/src/course/02-solutions/11-Slots/final.tsx +++ b/src/course/02- lessons/01-Bronze/Slots/final/final.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import { HTMLAttributes } from 'react'; -import { IconOne, IconTwo } from './icons'; +import { IconOne, IconTwo } from '../icons'; interface IButton extends HTMLAttributes { className?: string; diff --git a/src/course/02- lessons/11-Slots/icons/index.tsx b/src/course/02- lessons/01-Bronze/Slots/icons/index.tsx similarity index 100% rename from src/course/02- lessons/11-Slots/icons/index.tsx rename to src/course/02- lessons/01-Bronze/Slots/icons/index.tsx diff --git a/src/course/02- lessons/11-Slots/lesson.mdx b/src/course/02- lessons/01-Bronze/Slots/lesson.mdx similarity index 97% rename from src/course/02- lessons/11-Slots/lesson.mdx rename to src/course/02- lessons/01-Bronze/Slots/lesson.mdx index 2093db8..3538912 100644 --- a/src/course/02- lessons/11-Slots/lesson.mdx +++ b/src/course/02- lessons/01-Bronze/Slots/lesson.mdx @@ -1,6 +1,6 @@ import { Meta } from '@storybook/blocks'; - + # Slots Pattern diff --git a/src/course/02- lessons/01-ConditionalRendering/exercise.stories.tsx b/src/course/02- lessons/01-ConditionalRendering/exercise.stories.tsx deleted file mode 100644 index 5c66860..0000000 --- a/src/course/02- lessons/01-ConditionalRendering/exercise.stories.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; - -import { userEvent, within, expect } from "@storybook/test"; - -import { ComponentOne } from "./exercise"; - -const meta: Meta = { - title: "Lessons/01 - Conditional Rendering Pattern/02-Exercise", - component: ComponentOne, -}; - -export default meta; -type Story = StoryObj; - -const username = "John Doe"; - -/* - * See https://storybook.js.org/docs/writing-stories/play-function#working-with-the-canvas - * to learn more about using the canvasElement to query the DOM - */ -export const Default: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - await userEvent.click(canvas.getByRole("button", { name: "Login" })); - - await expect(canvas.getByText(`Welcome ${username}`)).toBeInTheDocument(); - await expect(canvas.queryByRole("button", { name: "Login" })).toBeNull(); - - await userEvent.click(canvas.getByRole("button", { name: "Logout" })); - - await expect(canvas.queryByText(`Welcome ${username}`)).toBeNull(); - await expect(canvas.queryByRole("button", { name: "Logout" })).toBeNull(); - }, - args: { - username, - }, -}; diff --git a/src/course/02- lessons/10-Compound/components/Accordion.tsx b/src/course/02- lessons/02-Silver/Compound/exercise/components/Accordion.tsx similarity index 100% rename from src/course/02- lessons/10-Compound/components/Accordion.tsx rename to src/course/02- lessons/02-Silver/Compound/exercise/components/Accordion.tsx diff --git a/src/course/02- lessons/10-Compound/components/Accoridon.module.css b/src/course/02- lessons/02-Silver/Compound/exercise/components/Accoridon.module.css similarity index 100% rename from src/course/02- lessons/10-Compound/components/Accoridon.module.css rename to src/course/02- lessons/02-Silver/Compound/exercise/components/Accoridon.module.css diff --git a/src/course/02- lessons/10-Compound/components/ChevronDown.tsx b/src/course/02- lessons/02-Silver/Compound/exercise/components/ChevronDown.tsx similarity index 100% rename from src/course/02- lessons/10-Compound/components/ChevronDown.tsx rename to src/course/02- lessons/02-Silver/Compound/exercise/components/ChevronDown.tsx diff --git a/src/course/02- lessons/06-Controlled/exercise.stories.tsx b/src/course/02- lessons/02-Silver/Compound/exercise/exercise.stories.tsx similarity index 86% rename from src/course/02- lessons/06-Controlled/exercise.stories.tsx rename to src/course/02- lessons/02-Silver/Compound/exercise/exercise.stories.tsx index 57b9197..b35e34d 100644 --- a/src/course/02- lessons/06-Controlled/exercise.stories.tsx +++ b/src/course/02- lessons/02-Silver/Compound/exercise/exercise.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Exercise } from './exercise'; const meta: Meta = { - title: 'Lessons/06 - Controlled Components Pattern/02-Exercise', + title: 'Lessons/๐Ÿฅˆ Sliver/Compound Components Pattern/02-Exercise', component: Exercise }; diff --git a/src/course/02- lessons/10-Compound/exercise.tsx b/src/course/02- lessons/02-Silver/Compound/exercise/exercise.tsx similarity index 100% rename from src/course/02- lessons/10-Compound/exercise.tsx rename to src/course/02- lessons/02-Silver/Compound/exercise/exercise.tsx diff --git a/src/course/02-solutions/10-Compound/components/Accordion.tsx b/src/course/02- lessons/02-Silver/Compound/final/components/Accordion.tsx similarity index 100% rename from src/course/02-solutions/10-Compound/components/Accordion.tsx rename to src/course/02- lessons/02-Silver/Compound/final/components/Accordion.tsx diff --git a/src/course/02-solutions/10-Compound/components/Accoridon.module.css b/src/course/02- lessons/02-Silver/Compound/final/components/Accoridon.module.css similarity index 100% rename from src/course/02-solutions/10-Compound/components/Accoridon.module.css rename to src/course/02- lessons/02-Silver/Compound/final/components/Accoridon.module.css diff --git a/src/course/02-solutions/10-Compound/components/ChevronDown.tsx b/src/course/02- lessons/02-Silver/Compound/final/components/ChevronDown.tsx similarity index 100% rename from src/course/02-solutions/10-Compound/components/ChevronDown.tsx rename to src/course/02- lessons/02-Silver/Compound/final/components/ChevronDown.tsx diff --git a/src/course/02-solutions/06-Controlled/final.stories.tsx b/src/course/02- lessons/02-Silver/Compound/final/final.stories.tsx similarity index 86% rename from src/course/02-solutions/06-Controlled/final.stories.tsx rename to src/course/02- lessons/02-Silver/Compound/final/final.stories.tsx index 830039c..d09a9cb 100644 --- a/src/course/02-solutions/06-Controlled/final.stories.tsx +++ b/src/course/02- lessons/02-Silver/Compound/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/06 - Controlled Components Pattern/03-Final', + title: 'Lessons/๐Ÿฅˆ Sliver/Compound Components Pattern/03-Final', component: Final }; diff --git a/src/course/02-solutions/10-Compound/final.tsx b/src/course/02- lessons/02-Silver/Compound/final/final.tsx similarity index 100% rename from src/course/02-solutions/10-Compound/final.tsx rename to src/course/02- lessons/02-Silver/Compound/final/final.tsx diff --git a/src/course/02- lessons/10-Compound/lesson.mdx b/src/course/02- lessons/02-Silver/Compound/lesson.mdx similarity index 96% rename from src/course/02- lessons/10-Compound/lesson.mdx rename to src/course/02- lessons/02-Silver/Compound/lesson.mdx index 66b3236..9751a69 100644 --- a/src/course/02- lessons/10-Compound/lesson.mdx +++ b/src/course/02- lessons/02-Silver/Compound/lesson.mdx @@ -1,6 +1,6 @@ import { Meta } from '@storybook/blocks'; - + # Compound Components Pattern diff --git a/src/course/02- lessons/05-Hooks/exercise.stories.tsx b/src/course/02- lessons/02-Silver/Controlled/exercise/exercise.stories.tsx similarity index 85% rename from src/course/02- lessons/05-Hooks/exercise.stories.tsx rename to src/course/02- lessons/02-Silver/Controlled/exercise/exercise.stories.tsx index 28c98ec..1bda094 100644 --- a/src/course/02- lessons/05-Hooks/exercise.stories.tsx +++ b/src/course/02- lessons/02-Silver/Controlled/exercise/exercise.stories.tsx @@ -3,7 +3,8 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Exercise } from './exercise'; const meta: Meta = { - title: 'Lessons/05 - Hooks Pattern/02-Exercise', + title: + 'Lessons/๐Ÿฅˆ Sliver/Controlled Components Pattern/02-Exercise', component: Exercise }; diff --git a/src/course/02- lessons/06-Controlled/exercise.tsx b/src/course/02- lessons/02-Silver/Controlled/exercise/exercise.tsx similarity index 98% rename from src/course/02- lessons/06-Controlled/exercise.tsx rename to src/course/02- lessons/02-Silver/Controlled/exercise/exercise.tsx index 5d723eb..d3878c7 100644 --- a/src/course/02- lessons/06-Controlled/exercise.tsx +++ b/src/course/02- lessons/02-Silver/Controlled/exercise/exercise.tsx @@ -5,7 +5,7 @@ import classNames from 'classnames'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { useEffect, useRef, useState } from 'react'; import FocusLock from 'react-focus-lock'; -import { Button } from '../../../shared/components/Button/Button.component'; +import { Button } from '@shared/components/Button/Button.component'; interface IModal { isVisible: boolean; diff --git a/src/course/02- lessons/02-Silver/Controlled/final/final.stories.tsx b/src/course/02- lessons/02-Silver/Controlled/final/final.stories.tsx new file mode 100644 index 0000000..61763d8 --- /dev/null +++ b/src/course/02- lessons/02-Silver/Controlled/final/final.stories.tsx @@ -0,0 +1,20 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Final } from './final'; + +const meta: Meta = { + title: 'Lessons/๐Ÿฅˆ Sliver/Controlled Components Pattern/03-Final', + component: Final +}; + +export default meta; +type Story = StoryObj; + +/* + * See https://storybook.js.org/docs/writing-stories/play-function#working-with-the-canvas + * to learn more about using the canvasElement to query the DOM + */ +export const Default: Story = { + play: async () => {}, + args: {} +}; diff --git a/src/course/02-solutions/06-Controlled/final.tsx b/src/course/02- lessons/02-Silver/Controlled/final/final.tsx similarity index 96% rename from src/course/02-solutions/06-Controlled/final.tsx rename to src/course/02- lessons/02-Silver/Controlled/final/final.tsx index b4bbb8e..0976052 100644 --- a/src/course/02-solutions/06-Controlled/final.tsx +++ b/src/course/02- lessons/02-Silver/Controlled/final/final.tsx @@ -1,7 +1,7 @@ import classNames from 'classnames'; import { useEffect, useRef, useState } from 'react'; import FocusLock from 'react-focus-lock'; -import { Button } from '../../../shared/components/Button/Button.component'; +import { Button } from '@shared/components/Button/Button.component'; interface IModal { isVisible: boolean; diff --git a/src/course/02- lessons/06-Controlled/lesson.mdx b/src/course/02- lessons/02-Silver/Controlled/lesson.mdx similarity index 96% rename from src/course/02- lessons/06-Controlled/lesson.mdx rename to src/course/02- lessons/02-Silver/Controlled/lesson.mdx index 1b2b099..f238cf0 100644 --- a/src/course/02- lessons/06-Controlled/lesson.mdx +++ b/src/course/02- lessons/02-Silver/Controlled/lesson.mdx @@ -1,6 +1,6 @@ import { Meta } from '@storybook/blocks'; - + # Controlled Components Pattern diff --git a/src/course/02- lessons/12-Portals/components/modal.tsx b/src/course/02- lessons/02-Silver/Portals/exercise/components/modal.tsx similarity index 96% rename from src/course/02- lessons/12-Portals/components/modal.tsx rename to src/course/02- lessons/02-Silver/Portals/exercise/components/modal.tsx index 749bbd0..e209c37 100644 --- a/src/course/02- lessons/12-Portals/components/modal.tsx +++ b/src/course/02- lessons/02-Silver/Portals/exercise/components/modal.tsx @@ -2,7 +2,7 @@ import classNames from 'classnames'; import { useEffect, useRef } from 'react'; // ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 1B - import { createPortal } from 'react-dom'; import FocusLock from 'react-focus-lock'; -import { Button } from '../../../../shared/components/Button/Button.component'; +import { Button } from '@shared/components/Button/Button.component'; interface IModal { isVisible: boolean; diff --git a/src/course/02- lessons/12-Portals/exercise.stories.tsx b/src/course/02- lessons/02-Silver/Portals/exercise/exercise.stories.tsx similarity index 89% rename from src/course/02- lessons/12-Portals/exercise.stories.tsx rename to src/course/02- lessons/02-Silver/Portals/exercise/exercise.stories.tsx index 63d4c26..f032f5d 100644 --- a/src/course/02- lessons/12-Portals/exercise.stories.tsx +++ b/src/course/02- lessons/02-Silver/Portals/exercise/exercise.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Exercise } from './exercise'; const meta: Meta = { - title: 'Lessons/12 - Portals/02-Exercise', + title: 'Lessons/๐Ÿฅˆ Sliver/Portals/02-Exercise', component: Exercise }; diff --git a/src/course/02- lessons/12-Portals/exercise.tsx b/src/course/02- lessons/02-Silver/Portals/exercise/exercise.tsx similarity index 96% rename from src/course/02- lessons/12-Portals/exercise.tsx rename to src/course/02- lessons/02-Silver/Portals/exercise/exercise.tsx index 7158c39..9e87a08 100644 --- a/src/course/02- lessons/12-Portals/exercise.tsx +++ b/src/course/02- lessons/02-Silver/Portals/exercise/exercise.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { Modal } from './components/modal'; -import { Button } from '../../../shared/components/Button/Button.component'; +import { Button } from '@shared/components/Button/Button.component'; // ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 1A - have a look at the current implementation of the modal and then go to components/modal.tsx diff --git a/src/course/02-solutions/12-Portals/components/modal.tsx b/src/course/02- lessons/02-Silver/Portals/final/components/modal.tsx similarity index 95% rename from src/course/02-solutions/12-Portals/components/modal.tsx rename to src/course/02- lessons/02-Silver/Portals/final/components/modal.tsx index e25f422..bb42cff 100644 --- a/src/course/02-solutions/12-Portals/components/modal.tsx +++ b/src/course/02- lessons/02-Silver/Portals/final/components/modal.tsx @@ -2,7 +2,7 @@ import classNames from 'classnames'; import { useEffect, useRef } from 'react'; import { createPortal } from 'react-dom'; import FocusLock from 'react-focus-lock'; -import { Button } from '../../../../shared/components/Button/Button.component'; +import { Button } from '@shared/components/Button/Button.component'; interface IModal { isVisible: boolean; diff --git a/src/course/02-solutions/12-Portals/final.stories.tsx b/src/course/02- lessons/02-Silver/Portals/final/final.stories.tsx similarity index 89% rename from src/course/02-solutions/12-Portals/final.stories.tsx rename to src/course/02- lessons/02-Silver/Portals/final/final.stories.tsx index 55f1e75..7bf7e01 100644 --- a/src/course/02-solutions/12-Portals/final.stories.tsx +++ b/src/course/02- lessons/02-Silver/Portals/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/12 - Portals/03-Final', + title: 'Lessons/๐Ÿฅˆ Sliver/Portals/03-Final', component: Final }; diff --git a/src/course/02-solutions/12-Portals/final.tsx b/src/course/02- lessons/02-Silver/Portals/final/final.tsx similarity index 96% rename from src/course/02-solutions/12-Portals/final.tsx rename to src/course/02- lessons/02-Silver/Portals/final/final.tsx index 3504ff6..04c5305 100644 --- a/src/course/02-solutions/12-Portals/final.tsx +++ b/src/course/02- lessons/02-Silver/Portals/final/final.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { Modal } from './components/modal'; -import { Button } from '../../../shared/components/Button/Button.component'; +import { Button } from '@shared/components/Button/Button.component'; export const Final = () => { const [isVisible, setIsVisible] = useState(false); diff --git a/src/course/02- lessons/12-Portals/lesson.mdx b/src/course/02- lessons/02-Silver/Portals/lesson.mdx similarity index 96% rename from src/course/02- lessons/12-Portals/lesson.mdx rename to src/course/02- lessons/02-Silver/Portals/lesson.mdx index 8b7a9f4..517a6d2 100644 --- a/src/course/02- lessons/12-Portals/lesson.mdx +++ b/src/course/02- lessons/02-Silver/Portals/lesson.mdx @@ -1,6 +1,6 @@ import { Meta } from '@storybook/blocks'; - + # Portals Pattern diff --git a/src/course/02- lessons/08-Provider/Provider.tsx b/src/course/02- lessons/02-Silver/Provider/exercise/Provider.tsx similarity index 100% rename from src/course/02- lessons/08-Provider/Provider.tsx rename to src/course/02- lessons/02-Silver/Provider/exercise/Provider.tsx diff --git a/src/course/02- lessons/09-StateReducer/exercise.stories.tsx b/src/course/02- lessons/02-Silver/Provider/exercise/exercise.stories.tsx similarity index 88% rename from src/course/02- lessons/09-StateReducer/exercise.stories.tsx rename to src/course/02- lessons/02-Silver/Provider/exercise/exercise.stories.tsx index 17ae5bb..2775324 100644 --- a/src/course/02- lessons/09-StateReducer/exercise.stories.tsx +++ b/src/course/02- lessons/02-Silver/Provider/exercise/exercise.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Exercise } from './exercise'; const meta: Meta = { - title: 'Lessons/09 - State Reducer Pattern/02-Exercise', + title: 'Lessons/๐Ÿฅˆ Sliver/Provider Pattern/02-Exercise', component: Exercise }; diff --git a/src/course/02- lessons/08-Provider/exercise.tsx b/src/course/02- lessons/02-Silver/Provider/exercise/exercise.tsx similarity index 100% rename from src/course/02- lessons/08-Provider/exercise.tsx rename to src/course/02- lessons/02-Silver/Provider/exercise/exercise.tsx diff --git a/src/course/02-solutions/08-Provider/Provider.tsx b/src/course/02- lessons/02-Silver/Provider/final/Provider.tsx similarity index 95% rename from src/course/02-solutions/08-Provider/Provider.tsx rename to src/course/02- lessons/02-Silver/Provider/final/Provider.tsx index 6f0bdb8..82dff67 100644 --- a/src/course/02-solutions/08-Provider/Provider.tsx +++ b/src/course/02- lessons/02-Silver/Provider/final/Provider.tsx @@ -2,7 +2,7 @@ import { createContext, useContext, useMemo, useState } from 'react'; import { IPokemonManagerState, PokemonManager -} from '../../../shared/modules/PokemonManager/PokemonManager'; +} from '@shared/modules/PokemonManager/PokemonManager'; export interface IPokemonProviderState extends IPokemonManagerState { fetchPokemons: (total: number) => Promise; diff --git a/src/course/02-solutions/09-StateReducer/final.stories.tsx b/src/course/02- lessons/02-Silver/Provider/final/final.stories.tsx similarity index 88% rename from src/course/02-solutions/09-StateReducer/final.stories.tsx rename to src/course/02- lessons/02-Silver/Provider/final/final.stories.tsx index 493f022..dfc29b9 100644 --- a/src/course/02-solutions/09-StateReducer/final.stories.tsx +++ b/src/course/02- lessons/02-Silver/Provider/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/09 - State Reducer Pattern/03-Final', + title: 'Lessons/๐Ÿฅˆ Sliver/Provider Pattern/03-Final', component: Final }; diff --git a/src/course/02-solutions/08-Provider/final.tsx b/src/course/02- lessons/02-Silver/Provider/final/final.tsx similarity index 100% rename from src/course/02-solutions/08-Provider/final.tsx rename to src/course/02- lessons/02-Silver/Provider/final/final.tsx diff --git a/src/course/02- lessons/08-Provider/lesson.mdx b/src/course/02- lessons/02-Silver/Provider/lesson.mdx similarity index 96% rename from src/course/02- lessons/08-Provider/lesson.mdx rename to src/course/02- lessons/02-Silver/Provider/lesson.mdx index ffa9838..032ca68 100644 --- a/src/course/02- lessons/08-Provider/lesson.mdx +++ b/src/course/02- lessons/02-Silver/Provider/lesson.mdx @@ -1,6 +1,6 @@ import { Meta } from '@storybook/blocks'; - + # Provider Pattern diff --git a/src/course/02- lessons/10-Compound/exercise.stories.tsx b/src/course/02- lessons/02-Silver/RenderProps/exercise/exercise.stories.tsx similarity index 87% rename from src/course/02- lessons/10-Compound/exercise.stories.tsx rename to src/course/02- lessons/02-Silver/RenderProps/exercise/exercise.stories.tsx index ac0daa2..4ae95da 100644 --- a/src/course/02- lessons/10-Compound/exercise.stories.tsx +++ b/src/course/02- lessons/02-Silver/RenderProps/exercise/exercise.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Exercise } from './exercise'; const meta: Meta = { - title: 'Lessons/10 - Compound Components Pattern/02-Exercise', + title: 'Lessons/๐Ÿฅˆ Sliver/Render Props Pattern/02-Exercise', component: Exercise }; diff --git a/src/course/02- lessons/03-RenderProps/exercise.tsx b/src/course/02- lessons/02-Silver/RenderProps/exercise/exercise.tsx similarity index 91% rename from src/course/02- lessons/03-RenderProps/exercise.tsx rename to src/course/02- lessons/02-Silver/RenderProps/exercise/exercise.tsx index 5e07985..62f783e 100644 --- a/src/course/02- lessons/03-RenderProps/exercise.tsx +++ b/src/course/02- lessons/02-Silver/RenderProps/exercise/exercise.tsx @@ -1,7 +1,7 @@ import { ChangeEvent, useState } from 'react'; -import { Input } from '../../../shared/components/Input/Input.component'; -import { Label } from '../../../shared/components/Label/Label.component'; -import { ErrorMessage } from '../../../shared/components/ErrorMessage/ErrorMessage.component'; +import { Input } from '@shared/components/Input/Input.component'; +import { Label } from '@shared/components/Label/Label.component'; +import { ErrorMessage } from '@shared/components/ErrorMessage/ErrorMessage.component'; export interface ITextInputFieldProps { name: string; diff --git a/src/course/02-solutions/08-Provider/final.stories.tsx b/src/course/02- lessons/02-Silver/RenderProps/final/final.stories.tsx similarity index 87% rename from src/course/02-solutions/08-Provider/final.stories.tsx rename to src/course/02- lessons/02-Silver/RenderProps/final/final.stories.tsx index e9d0334..d19b771 100644 --- a/src/course/02-solutions/08-Provider/final.stories.tsx +++ b/src/course/02- lessons/02-Silver/RenderProps/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/08 - Provider Pattern/03-Final', + title: 'Lessons/๐Ÿฅˆ Sliver/Render Props Pattern/03-Final', component: Final }; diff --git a/src/course/02-solutions/03-RenderProps/final.tsx b/src/course/02- lessons/02-Silver/RenderProps/final/final.tsx similarity index 90% rename from src/course/02-solutions/03-RenderProps/final.tsx rename to src/course/02- lessons/02-Silver/RenderProps/final/final.tsx index f453323..e22879c 100644 --- a/src/course/02-solutions/03-RenderProps/final.tsx +++ b/src/course/02- lessons/02-Silver/RenderProps/final/final.tsx @@ -1,7 +1,7 @@ import { ChangeEvent, HTMLAttributes, useState } from 'react'; -import { Input } from '../../../shared/components/Input/Input.component'; -import { Label } from '../../../shared/components/Label/Label.component'; -import { ErrorMessage } from '../../../shared/components/ErrorMessage/ErrorMessage.component'; +import { Input } from '@shared/components/Input/Input.component'; +import { Label } from '@shared/components/Label/Label.component'; +import { ErrorMessage } from '@shared/components/ErrorMessage/ErrorMessage.component'; interface ITextFieldProps { hasError: boolean; diff --git a/src/course/02- lessons/03-RenderProps/lesson.mdx b/src/course/02- lessons/02-Silver/RenderProps/lesson.mdx similarity index 96% rename from src/course/02- lessons/03-RenderProps/lesson.mdx rename to src/course/02- lessons/02-Silver/RenderProps/lesson.mdx index fb3b3d9..7e6dff2 100644 --- a/src/course/02- lessons/03-RenderProps/lesson.mdx +++ b/src/course/02- lessons/02-Silver/RenderProps/lesson.mdx @@ -1,6 +1,6 @@ import { Meta } from '@storybook/blocks'; - + # Render Props Pattern diff --git a/src/course/02- lessons/03-RenderProps/exercise.stories.tsx b/src/course/02- lessons/02-Silver/StateReducer/exercise/exercise.stories.tsx similarity index 87% rename from src/course/02- lessons/03-RenderProps/exercise.stories.tsx rename to src/course/02- lessons/02-Silver/StateReducer/exercise/exercise.stories.tsx index b039a1f..ff19034 100644 --- a/src/course/02- lessons/03-RenderProps/exercise.stories.tsx +++ b/src/course/02- lessons/02-Silver/StateReducer/exercise/exercise.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Exercise } from './exercise'; const meta: Meta = { - title: 'Lessons/03 - Render Props Pattern/02-Exercise', + title: 'Lessons/๐Ÿฅˆ Sliver/State Reducer Pattern/02-Exercise', component: Exercise }; diff --git a/src/course/02- lessons/09-StateReducer/exercise.tsx b/src/course/02- lessons/02-Silver/StateReducer/exercise/exercise.tsx similarity index 100% rename from src/course/02- lessons/09-StateReducer/exercise.tsx rename to src/course/02- lessons/02-Silver/StateReducer/exercise/exercise.tsx diff --git a/src/course/02-solutions/05-Hooks/final.stories.tsx b/src/course/02- lessons/02-Silver/StateReducer/final/final.stories.tsx similarity index 87% rename from src/course/02-solutions/05-Hooks/final.stories.tsx rename to src/course/02- lessons/02-Silver/StateReducer/final/final.stories.tsx index 6b78fa8..d6b730c 100644 --- a/src/course/02-solutions/05-Hooks/final.stories.tsx +++ b/src/course/02- lessons/02-Silver/StateReducer/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/05 - Hooks Pattern/03-Final', + title: 'Lessons/๐Ÿฅˆ Sliver/State Reducer Pattern/03-Final', component: Final }; diff --git a/src/course/02-solutions/09-StateReducer/final.tsx b/src/course/02- lessons/02-Silver/StateReducer/final/final.tsx similarity index 97% rename from src/course/02-solutions/09-StateReducer/final.tsx rename to src/course/02- lessons/02-Silver/StateReducer/final/final.tsx index 7414940..e34d56a 100644 --- a/src/course/02-solutions/09-StateReducer/final.tsx +++ b/src/course/02- lessons/02-Silver/StateReducer/final/final.tsx @@ -2,7 +2,7 @@ import { Dispatch, useEffect, useReducer } from 'react'; import { IPokemon, PokemonManager -} from '../../../shared/modules/PokemonManager/PokemonManager'; +} from '@shared/modules/PokemonManager/PokemonManager'; interface IPokemonReducerState { pokemons?: IPokemon[]; diff --git a/src/course/02- lessons/09-StateReducer/lesson.mdx b/src/course/02- lessons/02-Silver/StateReducer/lesson.mdx similarity index 97% rename from src/course/02- lessons/09-StateReducer/lesson.mdx rename to src/course/02- lessons/02-Silver/StateReducer/lesson.mdx index 7cd41e0..185de16 100644 --- a/src/course/02- lessons/09-StateReducer/lesson.mdx +++ b/src/course/02- lessons/02-Silver/StateReducer/lesson.mdx @@ -1,6 +1,6 @@ import { Meta } from '@storybook/blocks'; - + # State Reducer Pattern diff --git a/src/course/02- lessons/07-HigherOrderComponents/exercise.stories.tsx b/src/course/02- lessons/03-Gold/01-HigherOrderComponents/exercise/exercise.stories.tsx similarity index 87% rename from src/course/02- lessons/07-HigherOrderComponents/exercise.stories.tsx rename to src/course/02- lessons/03-Gold/01-HigherOrderComponents/exercise/exercise.stories.tsx index 09540c2..adedfa2 100644 --- a/src/course/02- lessons/07-HigherOrderComponents/exercise.stories.tsx +++ b/src/course/02- lessons/03-Gold/01-HigherOrderComponents/exercise/exercise.stories.tsx @@ -3,7 +3,8 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Exercise } from './exercise'; const meta: Meta = { - title: 'Lessons/07 - Higher Order Components Pattern/02-Exercise', + title: + 'Lessons/๐Ÿฅ‡ Gold/Higher Order Components Pattern/02-Exercise', component: Exercise }; diff --git a/src/course/02- lessons/07-HigherOrderComponents/exercise.tsx b/src/course/02- lessons/03-Gold/01-HigherOrderComponents/exercise/exercise.tsx similarity index 100% rename from src/course/02- lessons/07-HigherOrderComponents/exercise.tsx rename to src/course/02- lessons/03-Gold/01-HigherOrderComponents/exercise/exercise.tsx diff --git a/src/course/02- lessons/07-HigherOrderComponents/withPokemon.tsx b/src/course/02- lessons/03-Gold/01-HigherOrderComponents/exercise/withPokemon.tsx similarity index 100% rename from src/course/02- lessons/07-HigherOrderComponents/withPokemon.tsx rename to src/course/02- lessons/03-Gold/01-HigherOrderComponents/exercise/withPokemon.tsx diff --git a/src/course/02-solutions/07-HigherOrderComponents/final.stories.tsx b/src/course/02- lessons/03-Gold/01-HigherOrderComponents/final/final.stories.tsx similarity index 86% rename from src/course/02-solutions/07-HigherOrderComponents/final.stories.tsx rename to src/course/02- lessons/03-Gold/01-HigherOrderComponents/final/final.stories.tsx index 82482b7..43ddf05 100644 --- a/src/course/02-solutions/07-HigherOrderComponents/final.stories.tsx +++ b/src/course/02- lessons/03-Gold/01-HigherOrderComponents/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/07 - Higher Order Components Pattern/03-Final', + title: 'Lessons/๐Ÿฅ‡ Gold/Higher Order Components Pattern/03-Final', component: Final }; diff --git a/src/course/02-solutions/07-HigherOrderComponents/final.tsx b/src/course/02- lessons/03-Gold/01-HigherOrderComponents/final/final.tsx similarity index 95% rename from src/course/02-solutions/07-HigherOrderComponents/final.tsx rename to src/course/02- lessons/03-Gold/01-HigherOrderComponents/final/final.tsx index fe8200a..c3f4f0b 100644 --- a/src/course/02-solutions/07-HigherOrderComponents/final.tsx +++ b/src/course/02- lessons/03-Gold/01-HigherOrderComponents/final/final.tsx @@ -3,7 +3,7 @@ import { IPokemonManagerActions, withPokemons } from './withPokemon'; import { IPokemon, IPokemonManagerState -} from '../../../shared/modules/PokemonManager/PokemonManager'; +} from '@shared/modules/PokemonManager/PokemonManager'; interface IMapStateToPropsComponentOneResponse { pokemons: IPokemon[]; diff --git a/src/course/02-solutions/07-HigherOrderComponents/withPokemon.tsx b/src/course/02- lessons/03-Gold/01-HigherOrderComponents/final/withPokemon.tsx similarity index 94% rename from src/course/02-solutions/07-HigherOrderComponents/withPokemon.tsx rename to src/course/02- lessons/03-Gold/01-HigherOrderComponents/final/withPokemon.tsx index 375bba6..6d60112 100644 --- a/src/course/02-solutions/07-HigherOrderComponents/withPokemon.tsx +++ b/src/course/02- lessons/03-Gold/01-HigherOrderComponents/final/withPokemon.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from 'react'; import { IPokemonManagerState, PokemonManager -} from '../../../shared/modules/PokemonManager/PokemonManager'; +} from '@shared/modules/PokemonManager/PokemonManager'; export interface IPokemonManagerActions { fetchPokemons: (total: number) => Promise; diff --git a/src/course/02- lessons/07-HigherOrderComponents/lesson.mdx b/src/course/02- lessons/03-Gold/01-HigherOrderComponents/lesson.mdx similarity index 96% rename from src/course/02- lessons/07-HigherOrderComponents/lesson.mdx rename to src/course/02- lessons/03-Gold/01-HigherOrderComponents/lesson.mdx index aa8e0ba..eeddd2b 100644 --- a/src/course/02- lessons/07-HigherOrderComponents/lesson.mdx +++ b/src/course/02- lessons/03-Gold/01-HigherOrderComponents/lesson.mdx @@ -1,6 +1,6 @@ import { Meta } from '@storybook/blocks'; - + # Higher Order Components Pattern diff --git a/src/course/02-solutions/04-PresentationalAndContainer/mocks.ts b/src/course/02-solutions/04-PresentationalAndContainer/mocks.ts deleted file mode 100644 index 944b403..0000000 --- a/src/course/02-solutions/04-PresentationalAndContainer/mocks.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { useState } from 'react'; - -interface IAddress { - id: number; - displayAddress: string; -} - -export interface ICheckoutData { - deliveryAddress: IAddress; - billingAddress?: IAddress; -} - -type ApiHook = [ - () => Promise, - { - data: T; - isSuccess: boolean; - isError: boolean; - isLoading: boolean; - onBillingAddressUpdate?: () => void; - } -]; - -export const useCheckout = (): ApiHook => { - const [data, setData] = useState(); - const [isError] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [isSuccess, setIsSuccess] = useState(false); - - const getCheckoutInfo = async () => { - setIsLoading(true); - setData({ - deliveryAddress: { - id: 1, - displayAddress: '12 John Doe St, W12 5TH' - }, - billingAddress: undefined - }); - - setTimeout(() => { - setIsLoading(false); - setIsSuccess(true); - }, 1000); - }; - - const onBillingAddressUpdate = () => { - setData({ - deliveryAddress: data?.deliveryAddress as IAddress, - billingAddress: { - id: 2, - displayAddress: '12 John Doe Billing St, W12 5TH' - } - }); - }; - - return [ - getCheckoutInfo, - { data, isError, isLoading, isSuccess, onBillingAddressUpdate } - ]; -}; - -export const useBrandOnePayment = (): ApiHook => { - const [isError] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [isSuccess, setIsSuccess] = useState(false); - - const makePayment = async () => { - setIsLoading(true); - - makePayment(); - - setTimeout(() => { - setIsLoading(false); - setIsSuccess(true); - }, 1000); - }; - - return [ - makePayment, - { data: undefined, isSuccess, isError, isLoading } - ]; -}; - -export const useBrandTwoPayment = (): ApiHook => { - const [isError] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [isSuccess, setIsSuccess] = useState(false); - - const makePayment = async () => { - setIsLoading(true); - - makePayment(); - - setTimeout(() => { - setIsLoading(false); - setIsSuccess(true); - }, 1000); - }; - - return [ - makePayment, - { data: undefined, isSuccess, isError, isLoading } - ]; -}; diff --git a/src/course/02-solutions/05-Hooks/components.tsx b/src/course/02-solutions/05-Hooks/components.tsx deleted file mode 100644 index 2adee81..0000000 --- a/src/course/02-solutions/05-Hooks/components.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Input } from '../../../shared/components/Input/Input.component'; -import { Label } from '../../../shared/components/Label/Label.component'; -import { ErrorMessage } from '../../../shared/components/ErrorMessage/ErrorMessage.component'; -import { HTMLAttributes } from 'react'; - -export interface ITextFieldProps { - hasError: boolean; - errorMessage?: string; - id: string; - name: string; - label: string; - input: HTMLAttributes & { required?: boolean }; -} - -export const TextFieldComponent = ({ - hasError, - errorMessage, - input, - id, - name, - label -}: ITextFieldProps) => ( -
- - - {errorMessage && hasError && ( - - )} -
-); diff --git a/src/course/02-solutions/10-Compound/final.stories.tsx b/src/course/02-solutions/10-Compound/final.stories.tsx deleted file mode 100644 index e4f27ff..0000000 --- a/src/course/02-solutions/10-Compound/final.stories.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import { Final } from './final'; - -const meta: Meta = { - title: 'Lessons/10 - Compound Components Pattern/03-Final', - component: Final -}; - -export default meta; -type Story = StoryObj; - -/* - * See https://storybook.js.org/docs/writing-stories/play-function#working-with-the-canvas - * to learn more about using the canvasElement to query the DOM - */ -export const Default: Story = { - play: async () => {}, - args: {} -}; diff --git a/src/course/02-solutions/11-Slots/icons/index.tsx b/src/course/02-solutions/11-Slots/icons/index.tsx deleted file mode 100644 index 75775a1..0000000 --- a/src/course/02-solutions/11-Slots/icons/index.tsx +++ /dev/null @@ -1,31 +0,0 @@ -export const IconTwo = ( - - - -); - -export const IconOne = ( - - - -); diff --git a/tsconfig.app.json b/tsconfig.app.json index 3be60d9..23ed661 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -21,7 +21,10 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "paths": { + "@shared/*": ["./src/shared/*"] + } }, "include": ["src"], "ignore": ["src/**/*.mdx"] diff --git a/tsconfig.node.json b/tsconfig.node.json index 3afdd6e..f89e416 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -7,7 +7,10 @@ "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, "strict": true, - "noEmit": true + "noEmit": true, + "paths": { + "@shared/*": ["./src/shared/*"] + } }, "include": ["vite.config.ts"] } diff --git a/vite.config.ts b/vite.config.ts index 9cc50ea..6549182 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,7 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [react()] }); From 2130cb17554b80ba5597ce6c93c549cb661fb476 Mon Sep 17 00:00:00 2001 From: Matthew Claffey Date: Wed, 21 May 2025 15:27:05 +0100 Subject: [PATCH 03/29] feat: implement the plymorphic lesson (#37) --- .storybook/main.ts | 13 +- .storybook/styles/docs.styles.css | 4 + package-lock.json | 1311 +++++++++++++++-- package.json | 1 + src/course/01-introduction/01-Welcome.mdx | 13 +- .../Compound/exercise/exercise.stories.tsx | 2 +- .../Compound/final/final.stories.tsx | 2 +- .../02- lessons/02-Silver/Compound/lesson.mdx | 2 +- .../Controlled/exercise/exercise.stories.tsx | 2 +- .../Controlled/final/final.stories.tsx | 2 +- .../02-Silver/Controlled/lesson.mdx | 2 +- .../exercise/exercise.stories.tsx | 20 + .../exercise/exercise.tsx | 102 ++ .../final/final.stories.tsx | 20 + .../PolymorphicComponents/final/final.tsx | 85 ++ .../PolymorphicComponents/lesson.mdx | 103 ++ .../Portals/exercise/exercise.stories.tsx | 2 +- .../02-Silver/Portals/final/final.stories.tsx | 2 +- .../02- lessons/02-Silver/Portals/lesson.mdx | 2 +- .../Provider/exercise/exercise.stories.tsx | 2 +- .../Provider/final/final.stories.tsx | 2 +- .../02- lessons/02-Silver/Provider/lesson.mdx | 2 +- .../RenderProps/exercise/exercise.stories.tsx | 2 +- .../RenderProps/final/final.stories.tsx | 2 +- .../02-Silver/RenderProps/lesson.mdx | 2 +- .../exercise/exercise.stories.tsx | 2 +- .../StateReducer/final/final.stories.tsx | 2 +- .../02-Silver/StateReducer/lesson.mdx | 2 +- .../exercise/exercise.stories.tsx | 0 .../exercise/exercise.tsx | 0 .../exercise/withPokemon.tsx | 0 .../final/final.stories.tsx | 0 .../final/final.tsx | 0 .../final/withPokemon.tsx | 0 .../lesson.mdx | 0 35 files changed, 1590 insertions(+), 118 deletions(-) create mode 100644 src/course/02- lessons/02-Silver/PolymorphicComponents/exercise/exercise.stories.tsx create mode 100644 src/course/02- lessons/02-Silver/PolymorphicComponents/exercise/exercise.tsx create mode 100644 src/course/02- lessons/02-Silver/PolymorphicComponents/final/final.stories.tsx create mode 100644 src/course/02- lessons/02-Silver/PolymorphicComponents/final/final.tsx create mode 100644 src/course/02- lessons/02-Silver/PolymorphicComponents/lesson.mdx rename src/course/02- lessons/03-Gold/{01-HigherOrderComponents => HigherOrderComponents}/exercise/exercise.stories.tsx (100%) rename src/course/02- lessons/03-Gold/{01-HigherOrderComponents => HigherOrderComponents}/exercise/exercise.tsx (100%) rename src/course/02- lessons/03-Gold/{01-HigherOrderComponents => HigherOrderComponents}/exercise/withPokemon.tsx (100%) rename src/course/02- lessons/03-Gold/{01-HigherOrderComponents => HigherOrderComponents}/final/final.stories.tsx (100%) rename src/course/02- lessons/03-Gold/{01-HigherOrderComponents => HigherOrderComponents}/final/final.tsx (100%) rename src/course/02- lessons/03-Gold/{01-HigherOrderComponents => HigherOrderComponents}/final/withPokemon.tsx (100%) rename src/course/02- lessons/03-Gold/{01-HigherOrderComponents => HigherOrderComponents}/lesson.mdx (100%) diff --git a/.storybook/main.ts b/.storybook/main.ts index 3ade951..029ba2e 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,5 +1,6 @@ import type { StorybookConfig } from '@storybook/react-vite'; import { mergeConfig } from 'vite'; +import remarkGfm from 'remark-gfm'; import path from 'path'; const config: StorybookConfig = { @@ -11,7 +12,17 @@ const config: StorybookConfig = { '@storybook/addon-onboarding', '@storybook/addon-links', '@storybook/addon-interactions', - '@storybook/addon-essentials' + '@storybook/addon-essentials', + { + name: '@storybook/addon-docs', + options: { + mdxPluginOptions: { + mdxCompileOptions: { + remarkPlugins: [remarkGfm] + } + } + } + } ], framework: { name: '@storybook/react-vite', diff --git a/.storybook/styles/docs.styles.css b/.storybook/styles/docs.styles.css index ec41664..d5b4c92 100644 --- a/.storybook/styles/docs.styles.css +++ b/.storybook/styles/docs.styles.css @@ -27,3 +27,7 @@ .sbdocs-content h3 { font-size: 1.75rem; } + +.sbdocs-content ol { + list-style: decimal; +} diff --git a/package-lock.json b/package-lock.json index 7337777..39050e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "eslint-plugin-storybook": "^0.8.0", "playwright": "^1.44.1", "postcss": "^8.4.38", + "remark-gfm": "^4.0.1", "storybook": "^8.4.7", "tailwindcss": "^3.4.5", "typescript": "^5.2.2", @@ -3764,6 +3765,16 @@ "@types/node": "*" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -3839,6 +3850,16 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/mdx": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", @@ -3851,6 +3872,13 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "18.19.38", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.38.tgz", @@ -3941,6 +3969,13 @@ "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==" }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/uuid": { "version": "9.0.8", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", @@ -4758,6 +4793,17 @@ "@babel/core": "^7.0.0" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -5015,6 +5061,17 @@ } ] }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chai": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", @@ -5523,6 +5580,31 @@ "node": ">=0.10.0" } }, + "node_modules/decode-named-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz", + "integrity": "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/decode-named-character-reference/node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dedent": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", @@ -5646,6 +5728,20 @@ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -6447,6 +6543,13 @@ "integrity": "sha512-+kn8561vHAY+dt+0gMqqj1oY+g5xWrsuGMk4QGxotT2WS545nVqqjs37z6hrYfIuucwqthzwJfCJUEYqixyljg==", "dev": true }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7780,6 +7883,19 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -10347,6 +10463,17 @@ "dev": true, "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -10446,6 +10573,17 @@ "integrity": "sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==", "dev": true }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -10456,148 +10594,952 @@ "node": ">= 0.4" } }, - "node_modules/memoizerific": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz", - "integrity": "sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==", + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", "dev": true, + "license": "MIT", "dependencies": { - "map-or-similar": "^1.5.0" + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" }, - "engines": { - "node": ">=8.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", "dev": true, - "engines": { - "node": ">= 0.6" + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", "dev": true, + "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" }, - "engines": { - "node": ">= 0.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mimic-fn": { + "node_modules/mdast-util-gfm-footnote": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", "dev": true, - "engines": { - "node": ">=6" + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", "dev": true, - "engines": { - "node": ">=4" + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", "dev": true, + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", "dev": true, - "engines": { - "node": ">=8" + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", "dev": true, - "bin": { - "mkdirp": "bin/cmd.js" + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" }, - "engines": { - "node": ">=10" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", "dev": true, + "license": "MIT", "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "node_modules/memoizerific": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz", + "integrity": "sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==", + "dev": true, + "dependencies": { + "map-or-similar": "^1.5.0" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", @@ -11920,6 +12862,58 @@ "node": ">=4" } }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -12884,6 +13878,17 @@ "tree-kill": "cli.js" } }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -13013,6 +14018,85 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -13174,6 +14258,36 @@ "node": ">=10.12.0" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "5.4.19", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", @@ -13617,6 +14731,17 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index 40ed1d8..6b7b550 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "eslint-plugin-storybook": "^0.8.0", "playwright": "^1.44.1", "postcss": "^8.4.38", + "remark-gfm": "^4.0.1", "storybook": "^8.4.7", "tailwindcss": "^3.4.5", "typescript": "^5.2.2", diff --git a/src/course/01-introduction/01-Welcome.mdx b/src/course/01-introduction/01-Welcome.mdx index 87e2a82..56b3a3d 100644 --- a/src/course/01-introduction/01-Welcome.mdx +++ b/src/course/01-introduction/01-Welcome.mdx @@ -48,12 +48,13 @@ Each lesson is broken down in an exercise file and a final file. The exercise fi #### ๐Ÿฅˆ Silver -- [Compound components pattern](?path=/docs/lessons-๐Ÿฅˆ-sliver-compound-components-pattern-01-lesson--docs) -- [Controlled component pattern](?path=/docs/lessons-๐Ÿฅˆ-sliver-controlled-components-pattern-01-lesson--docs) -- [Render props pattern](?path=/docs/lessons-๐Ÿฅˆ-sliver-render-props-pattern-01-lesson--docs) -- [The Provider pattern](?path=/docs/lessons-๐Ÿฅˆ-sliver-provider-pattern-01-lesson--docs) -- [The State Reducer pattern](?path=/docs/lessons-๐Ÿฅˆ-sliver-state-reducer-pattern-01-lesson--docs) -- [Portals pattern](?path=/docs/lessons-๐Ÿฅˆ-sliver-portals-01-lesson--docs) +- [Compound components pattern](?path=/docs/lessons-๐Ÿฅˆ-Silver-compound-components-pattern-01-lesson--docs) +- [Controlled component pattern](?path=/docs/lessons-๐Ÿฅˆ-Silver-controlled-components-pattern-01-lesson--docs) +- [Render props pattern](?path=/docs/lessons-๐Ÿฅˆ-Silver-render-props-pattern-01-lesson--docs) +- [The Provider pattern](?path=/docs/lessons-๐Ÿฅˆ-Silver-provider-pattern-01-lesson--docs) +- [The State Reducer pattern](?path=/docs/lessons-๐Ÿฅˆ-Silver-state-reducer-pattern-01-lesson--docs) +- [Portals pattern](?path=/docs/lessons-๐Ÿฅˆ-Silver-portals-01-lesson--docs) +- [Polymorphic components pattern](?path=/docs/lessons-๐Ÿฅˆ-silver-polymorphic-components-01-lesson--docs) #### ๐Ÿฅ‡ Gold diff --git a/src/course/02- lessons/02-Silver/Compound/exercise/exercise.stories.tsx b/src/course/02- lessons/02-Silver/Compound/exercise/exercise.stories.tsx index b35e34d..2553528 100644 --- a/src/course/02- lessons/02-Silver/Compound/exercise/exercise.stories.tsx +++ b/src/course/02- lessons/02-Silver/Compound/exercise/exercise.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Exercise } from './exercise'; const meta: Meta = { - title: 'Lessons/๐Ÿฅˆ Sliver/Compound Components Pattern/02-Exercise', + title: 'Lessons/๐Ÿฅˆ Silver/Compound Components Pattern/02-Exercise', component: Exercise }; diff --git a/src/course/02- lessons/02-Silver/Compound/final/final.stories.tsx b/src/course/02- lessons/02-Silver/Compound/final/final.stories.tsx index d09a9cb..e7a6771 100644 --- a/src/course/02- lessons/02-Silver/Compound/final/final.stories.tsx +++ b/src/course/02- lessons/02-Silver/Compound/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/๐Ÿฅˆ Sliver/Compound Components Pattern/03-Final', + title: 'Lessons/๐Ÿฅˆ Silver/Compound Components Pattern/03-Final', component: Final }; diff --git a/src/course/02- lessons/02-Silver/Compound/lesson.mdx b/src/course/02- lessons/02-Silver/Compound/lesson.mdx index 9751a69..739f3a3 100644 --- a/src/course/02- lessons/02-Silver/Compound/lesson.mdx +++ b/src/course/02- lessons/02-Silver/Compound/lesson.mdx @@ -1,6 +1,6 @@ import { Meta } from '@storybook/blocks'; - + # Compound Components Pattern diff --git a/src/course/02- lessons/02-Silver/Controlled/exercise/exercise.stories.tsx b/src/course/02- lessons/02-Silver/Controlled/exercise/exercise.stories.tsx index 1bda094..99a98cd 100644 --- a/src/course/02- lessons/02-Silver/Controlled/exercise/exercise.stories.tsx +++ b/src/course/02- lessons/02-Silver/Controlled/exercise/exercise.stories.tsx @@ -4,7 +4,7 @@ import { Exercise } from './exercise'; const meta: Meta = { title: - 'Lessons/๐Ÿฅˆ Sliver/Controlled Components Pattern/02-Exercise', + 'Lessons/๐Ÿฅˆ Silver/Controlled Components Pattern/02-Exercise', component: Exercise }; diff --git a/src/course/02- lessons/02-Silver/Controlled/final/final.stories.tsx b/src/course/02- lessons/02-Silver/Controlled/final/final.stories.tsx index 61763d8..56ee988 100644 --- a/src/course/02- lessons/02-Silver/Controlled/final/final.stories.tsx +++ b/src/course/02- lessons/02-Silver/Controlled/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/๐Ÿฅˆ Sliver/Controlled Components Pattern/03-Final', + title: 'Lessons/๐Ÿฅˆ Silver/Controlled Components Pattern/03-Final', component: Final }; diff --git a/src/course/02- lessons/02-Silver/Controlled/lesson.mdx b/src/course/02- lessons/02-Silver/Controlled/lesson.mdx index f238cf0..77f4b7f 100644 --- a/src/course/02- lessons/02-Silver/Controlled/lesson.mdx +++ b/src/course/02- lessons/02-Silver/Controlled/lesson.mdx @@ -1,6 +1,6 @@ import { Meta } from '@storybook/blocks'; - + # Controlled Components Pattern diff --git a/src/course/02- lessons/02-Silver/PolymorphicComponents/exercise/exercise.stories.tsx b/src/course/02- lessons/02-Silver/PolymorphicComponents/exercise/exercise.stories.tsx new file mode 100644 index 0000000..6e64026 --- /dev/null +++ b/src/course/02- lessons/02-Silver/PolymorphicComponents/exercise/exercise.stories.tsx @@ -0,0 +1,20 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Exercise } from './exercise'; + +const meta: Meta = { + title: 'Lessons/๐Ÿฅˆ Silver/Polymorphic Components/02-Exercise', + component: Exercise +}; + +export default meta; +type Story = StoryObj; + +/* + * See https://storybook.js.org/docs/writing-stories/play-function#working-with-the-canvas + * to learn more about using the canvasElement to query the DOM + */ +export const Default: Story = { + play: async () => {}, + args: {} +}; diff --git a/src/course/02- lessons/02-Silver/PolymorphicComponents/exercise/exercise.tsx b/src/course/02- lessons/02-Silver/PolymorphicComponents/exercise/exercise.tsx new file mode 100644 index 0000000..f54dfd3 --- /dev/null +++ b/src/course/02- lessons/02-Silver/PolymorphicComponents/exercise/exercise.tsx @@ -0,0 +1,102 @@ +import { HTMLAttributes } from 'react'; + +/** + * Exercise: Refactor the Heading component to correctly use the polymorphic pattern. + * + * ๐Ÿค” Observations of this file + * In the current component you can see that the as prop is a string so if a developer in a team uses the wrong element they would just get the h2 element. + * Font sizes are clearly defined to the element so there is no flexibility in sizes which can lead to developers pleasing designers but... breaking accessibility or vice versa where designs do not look the same as what was provided. + * + * We need to tackle this in stages... + * + * Stage one - Refactoring the component to use Polymorphic style so we remove the switch statement. + * Stage two - decouple the font size to the element + * Stage three - allow for developers to have a size medium breakpoint for special designs. + * + */ + +// ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 1.a - Create a type called allowedHTMLElements + +// ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 2.a - Create a type called FontSizes and it's a union of 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' + +interface IHeading extends HTMLAttributes { + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 1.b - Update the type of string to be the type you defined as part of 1.a + as?: string; + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 2.b - Create a new prop called size?: FontSizes; + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 3.a - Create a new prop called sizeMd?: FontSizes; + children?: React.ReactNode | React.ReactNode[]; +} + +const Heading = ({ + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 1.c - add : Element = 'h2' what this will do is redefine the prop to be a capital variable which can be used as a React Component. + as = 'h2', + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 2.c - Create a new prop called size + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 3.b - Create a new prop called sizeMd + children, + ...rest +}: IHeading) => { + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 1.d - Create a variable called elementFontSize which uses useMemo to return a string from an object key mapping. For example: useMemo(() => ({ h1: 'text-3xl' }[Element]), [Element]); + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 2.d - In the useMemo add the size as a dependency and then check if size exists. If it does, return `text-${size}` if not, return what was there previously. Move onto 3.a. + + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 3.c - create another useMemo for largeFontSizes where we find an array of md:text-(sm-3xl) and we need to "find" which one in that array "includes" sizeMd props value. + + // ๐Ÿงช 3.d Head down to the storybook Exercise Component and add a few more variants in. + + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 1.e return the Element with the className={classNames('mb-3 font-semibold', elementFontSize)} don't forget the ...rest + // ๐Ÿ’ฃ 1.f remove the old code below. Move onto step 2.a. + if (as) + switch (as) { + case 'h1': + return ( +

+ {children} +

+ ); + case 'h3': + return ( +

+ {children} +

+ ); + case 'h4': + return ( +

+ {children} +

+ ); + case 'h5': + return ( +
+ {children} +
+ ); + case 'h6': + return ( +
+ {children} +
+ ); + default: + return ( +

+ {children} +

+ ); + } +}; + +export const Exercise = () => ( +
+ Heading One + Heading Two + Heading Three + Heading Four + Heading Five + Heading Six + {/* 3.e ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Implement a heading as h2 and size sm */} + + {/* 3.e ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Implement a heading as h2 and size sm and sizeMd is 3xl */} + + {/* 3.e ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Implement a heading as h2 and sizeMd is 3xl */} +
+); diff --git a/src/course/02- lessons/02-Silver/PolymorphicComponents/final/final.stories.tsx b/src/course/02- lessons/02-Silver/PolymorphicComponents/final/final.stories.tsx new file mode 100644 index 0000000..e677c1f --- /dev/null +++ b/src/course/02- lessons/02-Silver/PolymorphicComponents/final/final.stories.tsx @@ -0,0 +1,20 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Final } from './final'; + +const meta: Meta = { + title: 'Lessons/๐Ÿฅˆ Silver/Polymorphic Components/03-Final', + component: Final +}; + +export default meta; +type Story = StoryObj; + +/* + * See https://storybook.js.org/docs/writing-stories/play-function#working-with-the-canvas + * to learn more about using the canvasElement to query the DOM + */ +export const Default: Story = { + play: async () => {}, + args: {} +}; diff --git a/src/course/02- lessons/02-Silver/PolymorphicComponents/final/final.tsx b/src/course/02- lessons/02-Silver/PolymorphicComponents/final/final.tsx new file mode 100644 index 0000000..58c6cfa --- /dev/null +++ b/src/course/02- lessons/02-Silver/PolymorphicComponents/final/final.tsx @@ -0,0 +1,85 @@ +import classNames from 'classnames'; +import { HTMLAttributes, useMemo } from 'react'; + +type AllowedHTMLElements = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; + +type FontSizes = 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl'; + +// ๐Ÿ’… could use conditional types here to use the correct element but for the sake of the example we can keep heading element. +interface IHeading extends HTMLAttributes { + as?: AllowedHTMLElements; + size?: FontSizes; + sizeMd?: FontSizes; + children?: React.ReactNode | React.ReactNode[]; +} + +const Heading = ({ + as: Element = 'h2', + size, + sizeMd, + children, + ...rest +}: IHeading) => { + const elementFontSize = useMemo( + () => + size + ? `text-${size}` + : { + h1: 'text-3xl', + h2: 'text-2xl', + h3: 'text-xl', + h4: 'text-lg', + h5: 'text-md', + h6: 'text-sm' + }[Element], + [Element, size] + ); + + const largeFontSize = useMemo( + () => + [ + 'md:text-sm', + 'md:text-md', + 'md:text-lg', + 'md:text-xl', + 'md:text-2xl', + 'md:text-3xl' + ].find((fontSizeClassName) => + sizeMd ? fontSizeClassName.includes(sizeMd) : false + ), + [sizeMd] + ); + + return ( + + {children} + + ); +}; + +export const Final = () => ( +
+ Heading One + Heading Two + Heading Three + Heading Four + Heading Five + Heading Six + + Heading Two (sm size) + + + Heading Two (sm size mobile and md breakpoint 3xl) + + + Heading Two (md breakpoint 3xl) + +
+); diff --git a/src/course/02- lessons/02-Silver/PolymorphicComponents/lesson.mdx b/src/course/02- lessons/02-Silver/PolymorphicComponents/lesson.mdx new file mode 100644 index 0000000..7e43870 --- /dev/null +++ b/src/course/02- lessons/02-Silver/PolymorphicComponents/lesson.mdx @@ -0,0 +1,103 @@ +import { Meta } from '@storybook/blocks'; + + + +# Polymorphic Components Pattern + +React excels at building reusable components, but repetition can creep in when components only differ slightlyโ€”like headings or buttons with varied HTML tags. + +Consider this: + +```jsx +export const Paragraph = ({ children }) => ( +

{children}

+); +export const HeadingOne = ({ children }) => ( +

{children}

+); +export const HeadingTwo = ({ children }) => ( +

{children}

+); +export const HeadingThree = ({ children }) => ( +

{children}

+); +``` + +Each of these components is nearly identical. This isn't scalable and can be difficult to maintain. The **polymorphic component pattern** solves this by allowing you to render different HTML elements using a single component, often via an **as** prop. + +## Who else does this? + +Most modern UI libraries (like Chakra UI or Radix UI) support polymorphic components. The **as** prop lets developers choose the HTML tag to render, giving flexibility while keeping styling and logic consistent. + +Example: + +```JSX + + My heading is a h3 tag but styled like a h4. + +``` + +This would render as: + +```jsx +

My heading is a h3 tag but styled like a h4.

+``` + +## Implementation + +Hereโ€™s a simple implementation of a polymorphic **Heading** component: + +```jsx +export const Heading = ({ + as: Component = 'h2', + size = 'h2', + children +}) => { + return {children}; +}; +``` + +> ๐Ÿ’ก **Tip:** When rendering dynamic elements in React, make sure your **as** value (e.g., **Component**) is capitalized or passed as a variable. React treats lowercase JSX tags as native HTML. + +## Why use this pattern? + +This pattern is extremely useful in design systems, especially when building: + +- Typography components +- Buttons +- Form elements +- Reusable layout primitives + +It ensures your components stay flexible while keeping semantic HTML and accessible markup intact. + +Here's a quick visual guide: + +| Usage | Renders As | Styled As | +| ----------------------------- | ---------- | --------- | +| **Heading** | **h2** | **h2** | +| **Heading as="h3"** | **h3** | **h2** | +| **Heading as="h3" size="h4"** | **h3** | **h4** | + +## Exercise + +### Scenario + +The software engineering group are using a Heading component and it works fairly well from an implementation point of view but there is often friction between the design teams and developers around design consistency vs accessibility. + +The previous developer built a heading component using the right intentions however, the flexibility of the component is a little rigid. + +### What we are going to do? + +In todayโ€™s exercise, weโ€™re going to refactor this component to use the Polymorphic pattern. + +It should: + +1. Render a semantic HTML tag based on the **as** prop BUT we have typescript in place to only allow for the heading tags and p tags. +2. Apply a CSS class based on a **size** prop (defaulting to the tag if **size** is not provided). +3. Fall back to defaults (p, p) if neither is provided. + +## Feedback + +Feedback is a gift and it helps me make these courses better for you. If you have 5 minutes, Iโ€™d love for you to fill out the feedback form: + +[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new) diff --git a/src/course/02- lessons/02-Silver/Portals/exercise/exercise.stories.tsx b/src/course/02- lessons/02-Silver/Portals/exercise/exercise.stories.tsx index f032f5d..0bae7c2 100644 --- a/src/course/02- lessons/02-Silver/Portals/exercise/exercise.stories.tsx +++ b/src/course/02- lessons/02-Silver/Portals/exercise/exercise.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Exercise } from './exercise'; const meta: Meta = { - title: 'Lessons/๐Ÿฅˆ Sliver/Portals/02-Exercise', + title: 'Lessons/๐Ÿฅˆ Silver/Portals/02-Exercise', component: Exercise }; diff --git a/src/course/02- lessons/02-Silver/Portals/final/final.stories.tsx b/src/course/02- lessons/02-Silver/Portals/final/final.stories.tsx index 7bf7e01..2902679 100644 --- a/src/course/02- lessons/02-Silver/Portals/final/final.stories.tsx +++ b/src/course/02- lessons/02-Silver/Portals/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/๐Ÿฅˆ Sliver/Portals/03-Final', + title: 'Lessons/๐Ÿฅˆ Silver/Portals/03-Final', component: Final }; diff --git a/src/course/02- lessons/02-Silver/Portals/lesson.mdx b/src/course/02- lessons/02-Silver/Portals/lesson.mdx index 517a6d2..75767b6 100644 --- a/src/course/02- lessons/02-Silver/Portals/lesson.mdx +++ b/src/course/02- lessons/02-Silver/Portals/lesson.mdx @@ -1,6 +1,6 @@ import { Meta } from '@storybook/blocks'; - + # Portals Pattern diff --git a/src/course/02- lessons/02-Silver/Provider/exercise/exercise.stories.tsx b/src/course/02- lessons/02-Silver/Provider/exercise/exercise.stories.tsx index 2775324..634dca0 100644 --- a/src/course/02- lessons/02-Silver/Provider/exercise/exercise.stories.tsx +++ b/src/course/02- lessons/02-Silver/Provider/exercise/exercise.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Exercise } from './exercise'; const meta: Meta = { - title: 'Lessons/๐Ÿฅˆ Sliver/Provider Pattern/02-Exercise', + title: 'Lessons/๐Ÿฅˆ Silver/Provider Pattern/02-Exercise', component: Exercise }; diff --git a/src/course/02- lessons/02-Silver/Provider/final/final.stories.tsx b/src/course/02- lessons/02-Silver/Provider/final/final.stories.tsx index dfc29b9..78349af 100644 --- a/src/course/02- lessons/02-Silver/Provider/final/final.stories.tsx +++ b/src/course/02- lessons/02-Silver/Provider/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/๐Ÿฅˆ Sliver/Provider Pattern/03-Final', + title: 'Lessons/๐Ÿฅˆ Silver/Provider Pattern/03-Final', component: Final }; diff --git a/src/course/02- lessons/02-Silver/Provider/lesson.mdx b/src/course/02- lessons/02-Silver/Provider/lesson.mdx index 032ca68..31cd02d 100644 --- a/src/course/02- lessons/02-Silver/Provider/lesson.mdx +++ b/src/course/02- lessons/02-Silver/Provider/lesson.mdx @@ -1,6 +1,6 @@ import { Meta } from '@storybook/blocks'; - + # Provider Pattern diff --git a/src/course/02- lessons/02-Silver/RenderProps/exercise/exercise.stories.tsx b/src/course/02- lessons/02-Silver/RenderProps/exercise/exercise.stories.tsx index 4ae95da..133be89 100644 --- a/src/course/02- lessons/02-Silver/RenderProps/exercise/exercise.stories.tsx +++ b/src/course/02- lessons/02-Silver/RenderProps/exercise/exercise.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Exercise } from './exercise'; const meta: Meta = { - title: 'Lessons/๐Ÿฅˆ Sliver/Render Props Pattern/02-Exercise', + title: 'Lessons/๐Ÿฅˆ Silver/Render Props Pattern/02-Exercise', component: Exercise }; diff --git a/src/course/02- lessons/02-Silver/RenderProps/final/final.stories.tsx b/src/course/02- lessons/02-Silver/RenderProps/final/final.stories.tsx index d19b771..cd576d2 100644 --- a/src/course/02- lessons/02-Silver/RenderProps/final/final.stories.tsx +++ b/src/course/02- lessons/02-Silver/RenderProps/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/๐Ÿฅˆ Sliver/Render Props Pattern/03-Final', + title: 'Lessons/๐Ÿฅˆ Silver/Render Props Pattern/03-Final', component: Final }; diff --git a/src/course/02- lessons/02-Silver/RenderProps/lesson.mdx b/src/course/02- lessons/02-Silver/RenderProps/lesson.mdx index 7e6dff2..c9b5497 100644 --- a/src/course/02- lessons/02-Silver/RenderProps/lesson.mdx +++ b/src/course/02- lessons/02-Silver/RenderProps/lesson.mdx @@ -1,6 +1,6 @@ import { Meta } from '@storybook/blocks'; - + # Render Props Pattern diff --git a/src/course/02- lessons/02-Silver/StateReducer/exercise/exercise.stories.tsx b/src/course/02- lessons/02-Silver/StateReducer/exercise/exercise.stories.tsx index ff19034..08eace5 100644 --- a/src/course/02- lessons/02-Silver/StateReducer/exercise/exercise.stories.tsx +++ b/src/course/02- lessons/02-Silver/StateReducer/exercise/exercise.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Exercise } from './exercise'; const meta: Meta = { - title: 'Lessons/๐Ÿฅˆ Sliver/State Reducer Pattern/02-Exercise', + title: 'Lessons/๐Ÿฅˆ Silver/State Reducer Pattern/02-Exercise', component: Exercise }; diff --git a/src/course/02- lessons/02-Silver/StateReducer/final/final.stories.tsx b/src/course/02- lessons/02-Silver/StateReducer/final/final.stories.tsx index d6b730c..d458cab 100644 --- a/src/course/02- lessons/02-Silver/StateReducer/final/final.stories.tsx +++ b/src/course/02- lessons/02-Silver/StateReducer/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/๐Ÿฅˆ Sliver/State Reducer Pattern/03-Final', + title: 'Lessons/๐Ÿฅˆ Silver/State Reducer Pattern/03-Final', component: Final }; diff --git a/src/course/02- lessons/02-Silver/StateReducer/lesson.mdx b/src/course/02- lessons/02-Silver/StateReducer/lesson.mdx index 185de16..ffbf94e 100644 --- a/src/course/02- lessons/02-Silver/StateReducer/lesson.mdx +++ b/src/course/02- lessons/02-Silver/StateReducer/lesson.mdx @@ -1,6 +1,6 @@ import { Meta } from '@storybook/blocks'; - + # State Reducer Pattern diff --git a/src/course/02- lessons/03-Gold/01-HigherOrderComponents/exercise/exercise.stories.tsx b/src/course/02- lessons/03-Gold/HigherOrderComponents/exercise/exercise.stories.tsx similarity index 100% rename from src/course/02- lessons/03-Gold/01-HigherOrderComponents/exercise/exercise.stories.tsx rename to src/course/02- lessons/03-Gold/HigherOrderComponents/exercise/exercise.stories.tsx diff --git a/src/course/02- lessons/03-Gold/01-HigherOrderComponents/exercise/exercise.tsx b/src/course/02- lessons/03-Gold/HigherOrderComponents/exercise/exercise.tsx similarity index 100% rename from src/course/02- lessons/03-Gold/01-HigherOrderComponents/exercise/exercise.tsx rename to src/course/02- lessons/03-Gold/HigherOrderComponents/exercise/exercise.tsx diff --git a/src/course/02- lessons/03-Gold/01-HigherOrderComponents/exercise/withPokemon.tsx b/src/course/02- lessons/03-Gold/HigherOrderComponents/exercise/withPokemon.tsx similarity index 100% rename from src/course/02- lessons/03-Gold/01-HigherOrderComponents/exercise/withPokemon.tsx rename to src/course/02- lessons/03-Gold/HigherOrderComponents/exercise/withPokemon.tsx diff --git a/src/course/02- lessons/03-Gold/01-HigherOrderComponents/final/final.stories.tsx b/src/course/02- lessons/03-Gold/HigherOrderComponents/final/final.stories.tsx similarity index 100% rename from src/course/02- lessons/03-Gold/01-HigherOrderComponents/final/final.stories.tsx rename to src/course/02- lessons/03-Gold/HigherOrderComponents/final/final.stories.tsx diff --git a/src/course/02- lessons/03-Gold/01-HigherOrderComponents/final/final.tsx b/src/course/02- lessons/03-Gold/HigherOrderComponents/final/final.tsx similarity index 100% rename from src/course/02- lessons/03-Gold/01-HigherOrderComponents/final/final.tsx rename to src/course/02- lessons/03-Gold/HigherOrderComponents/final/final.tsx diff --git a/src/course/02- lessons/03-Gold/01-HigherOrderComponents/final/withPokemon.tsx b/src/course/02- lessons/03-Gold/HigherOrderComponents/final/withPokemon.tsx similarity index 100% rename from src/course/02- lessons/03-Gold/01-HigherOrderComponents/final/withPokemon.tsx rename to src/course/02- lessons/03-Gold/HigherOrderComponents/final/withPokemon.tsx diff --git a/src/course/02- lessons/03-Gold/01-HigherOrderComponents/lesson.mdx b/src/course/02- lessons/03-Gold/HigherOrderComponents/lesson.mdx similarity index 100% rename from src/course/02- lessons/03-Gold/01-HigherOrderComponents/lesson.mdx rename to src/course/02- lessons/03-Gold/HigherOrderComponents/lesson.mdx From 787201c7b9f35aff57597676c4b20c239ca3ab7e Mon Sep 17 00:00:00 2001 From: Matthew Claffey Date: Fri, 23 May 2025 09:33:28 +0100 Subject: [PATCH 04/29] feat: implement state colocation + state lifting (#38) * feat: implement the state lifting & colocation pattern * chore: update some copy * fix: build --- public/pokemon-battleground.webp | Bin 0 -> 74782 bytes public/pokemon-logo.png | Bin 0 -> 119125 bytes .../exercise/components/Form.tsx | 111 ++++++++++++++ .../exercise/components/PokemonOptions.tsx | 129 ++++++++++++++++ .../exercise/components/Screen.tsx | 84 +++++++++++ .../exercise/exercise.stories.tsx | 24 +++ .../exercise/exercise.tsx | 4 + .../final/components/Form.tsx | 115 ++++++++++++++ .../final/components/PokemonOptions.tsx | 141 ++++++++++++++++++ .../final/components/Screen.tsx | 88 +++++++++++ .../final/final.stories.tsx | 24 +++ .../final/final.tsx | 3 + .../StateColocationVsStateLifting/lesson.mdx | 123 +++++++++++++++ src/shared/hooks/usePokedex.ts | 84 +++++++++++ 14 files changed, 930 insertions(+) create mode 100644 public/pokemon-battleground.webp create mode 100644 public/pokemon-logo.png create mode 100644 src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/Form.tsx create mode 100644 src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/PokemonOptions.tsx create mode 100644 src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/Screen.tsx create mode 100644 src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/exercise.stories.tsx create mode 100644 src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/exercise.tsx create mode 100644 src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/Form.tsx create mode 100644 src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/PokemonOptions.tsx create mode 100644 src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/Screen.tsx create mode 100644 src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/final.stories.tsx create mode 100644 src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/final.tsx create mode 100644 src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/lesson.mdx create mode 100644 src/shared/hooks/usePokedex.ts diff --git a/public/pokemon-battleground.webp b/public/pokemon-battleground.webp new file mode 100644 index 0000000000000000000000000000000000000000..87ca346c522eeef372fb3583ea7e7304d7661dc5 GIT binary patch literal 74782 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%0Diu5i2o{yhUiN`*7#T!pD?!zSod?* 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 literal 0 HcmV?d00001 diff --git a/public/pokemon-logo.png b/public/pokemon-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ed90ba2207fd626ca74fa4ea7977d71e44394315 GIT binary patch literal 119125 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

+

+ Select you favorite pokemon types (max 4) +

+
+ {isLoading && ( +
+ + + + + + + + + + + + +
+ )} +
+ {data && + data.length && + data.map((pokemonType) => { + const isSelected = + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 1.d: replace the empty array with the selectedPokemonTypes state variable. + // ๐Ÿ’ฃ We can remove these ignores when we apply the code + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + [].includes(pokemonType); + const hasSelectedAllOptions = [].length === 4; + + return ( + + ); + })} +
+ + +
+
+ ); +}; diff --git a/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/PokemonOptions.tsx b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/PokemonOptions.tsx new file mode 100644 index 0000000..e2c27c5 --- /dev/null +++ b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/PokemonOptions.tsx @@ -0,0 +1,129 @@ +import { Skeleton } from '@shared/components/Skeleton/Skeleton.component'; +import { + TPokemonCardsApiResponse, + usePokedex +} from '@shared/hooks/usePokedex'; +import classNames from 'classnames'; +import { FormEvent } from 'react'; + +interface IPokemonOptions { + type: string; + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 2.h: Add two new props called onPokemonSelection which takes a string[] and string as params and another + // variable called defaultSelectedPokemon which is an optional string[]. +} + +export const PokemonOptions = ({ type }: IPokemonOptions) => { + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 2.d: Create a selectedPokemon useState variable. Default value to be [] + + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 2.i: Replace the default of selectedPokemon from [] to the defaultSelectedPokemon. This will remember which cards were selected when the component re-renders. + + // โœ๐Ÿป This is already done for you. Feel free to have a look how it works in shared/hooks/usePokedex + const { data, isLoading, isError } = usePokedex< + TPokemonCardsApiResponse[] + >({ + path: 'cards', + queryParams: `pageSize=4&q=types:${type}&supertype:pokemon`, + skip: type === undefined + }); + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 2.j: call onPokemonSelection(selectedPokemon, type); + }; + + const togglePokemonSelection = (pokemonId: string) => { + // ๐Ÿ’ฃ We can get rid of this line once we start using the type param. + console.log(pokemonId); + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 2.g: We need to now update the state for when a pokemon card is selected or not. + // IF selectedPokemon includes the pokemonId then we need to de-select that pokemon card. + // ELSE we add the pokemon id to the array of pokemon cards [...selectedPokemon, pokemonId] and then setSelectedPokemon(newValues) (make this a variable as we will need it for later.) + // You should now start to see the pokemon being selected and de-selected. But the next thing we need to do is update the state within the screen. Search for 2.h + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 2.k: Inside the ELSE, check if the newlySelectedPokemon has the length of 2. IF it does, call onPokemonSelection(newlySelectedPokemon, type);. Head over to the Screen.tsx component to finish it off. + }; + + return ( +
+

+ {type} Pokemon +

+
+
+ {isLoading && ( +
+ + + + +
+ )} +
+ {data && + data.length > 0 && + data.map((pokemonCard) => { + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 2.e: Replace the empty arrays with the selectedPokemon variable we created. + const isSelected = [].find( + (pokemonId) => pokemonId === pokemonCard.id + ); + const allPokemonSelected = [].length === 2; + + return ( + + ); + })} +
+ + +
+
+
+ ); +}; diff --git a/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/Screen.tsx b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/Screen.tsx new file mode 100644 index 0000000..62d6c72 --- /dev/null +++ b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/Screen.tsx @@ -0,0 +1,84 @@ +/** + * Exercise: ๐Ÿงผ State Colocation vs ๐Ÿชœ State Lifting + * + * ๐Ÿค” Observations of this file + * So the lead developer wanted us to manage the state for the selected pokemon types & the selected pokemon at this level so those variables can be used here and within the children. Each variable will be an array of strings which represent the type or the id of the selected pokemon. + * + * We need to tackle this in stages... + * + * Stage one (follow 1.* steps) - Creating the form that returns the pokemon types and saving those selected types to the lifted state variable + * Stage two (following 2.* steps) - Using those types, we will render the pokemon options component + * + */ + +export const Screen = () => { + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 1.a: Create a useState variable called selectedPokemonTypes, setSelectedPokemonTypes. Default to be an empty array. + + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 2.a: Create a useState> variable called selectedPokemon, setSelectedPokemon. Default to be an object {}. + + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 1.h: Create a function called onPokemonTypesUpdate which will take a types: string[] param. Pass that into the Form component as a prop. The function will just need setSelectedPokemonTypes for now. + // ๐ŸŽ‰ STAGE ONE COMPLETED you should now be able to see the types display, select them and then the state in the screen gets updated. + + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 2.m: You will now start to see the happy path all working fine, however when you change to different pokemon types and receive a new set of pokemon you now get some messy state where selectedPokemon is more than 8. To fix this, write the following code inside 1.h function (before the setSelectedPokemonTypes) + /* + const newlyUpdatedPokemon = { ...selectedPokemon }; + + selectedPokemonTypes + .filter((type) => { + return !types.find((selectedType) => selectedType === type); + }) + .forEach((type) => { + delete newlyUpdatedPokemon[type]; + }); + + setSelectedPokemon(newlyUpdatedPokemon); + */ + // STAGE TWO completed. You have now built the screen. BUT ๐Ÿž there is a bug where the PokemonOptions re-renders the types that did not need to update when you change your types after one try. The reason is the "key" prop using index. The api has no identifier per type. If you enjoyed this exercise have a look into fixing it and make a pr. + + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 2.l: Create a function called onPokemonSelection which will take a pokemon: string[], type: string + // Then create a newlySelectedPokemon variable which will be a copy of the current selectedPokemon {...selectedPokemon} + // assign the newlySelectedPokemon[type] to equal the pokemon variable. + // setSelectedPokemon(newlySelectedPokemon); + + return ( +
+ hello +
+
+
+

+ Pokemon Battle Picker + + Battle Picker + +

+
+
+ {/* ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 1.b: Render pokemon types form from ./components/Form and then head over to the form component */} +
+
+ {/* ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 2.b: Loop through the selectedPokemonTypes and pass down the type property to the PokemonOptions (./components/PokemonOptions) component. You will also need a key to be on the component. I used `${pokemonType}-${index}` */} +
+ + {/* ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 2.c: We need to check if the KEYS in the selectedPokemon object equal 4 and the selectedPokemonTypes length is 4 before rendering the code snippet below. Head over to PokemonOptions when completed. */} + {/* + + */} +
+
+ ); +}; diff --git a/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/exercise.stories.tsx b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/exercise.stories.tsx new file mode 100644 index 0000000..b808a12 --- /dev/null +++ b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/exercise.stories.tsx @@ -0,0 +1,24 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Exercise } from './exercise'; + +const meta: Meta = { + title: + 'Lessons/๐Ÿฅ‰ Bronze/๐Ÿงผ State Colocation vs ๐Ÿชœ State Lifting/02-Exercise', + component: Exercise, + parameters: { + layout: 'fullscreen' + } +}; + +export default meta; +type Story = StoryObj; + +/* + * See https://storybook.js.org/docs/writing-stories/play-function#working-with-the-canvas + * to learn more about using the canvasElement to query the DOM + */ +export const Default: Story = { + play: async () => {}, + args: {} +}; diff --git a/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/exercise.tsx b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/exercise.tsx new file mode 100644 index 0000000..ee28457 --- /dev/null +++ b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/exercise/exercise.tsx @@ -0,0 +1,4 @@ +import { Screen } from './components/Screen'; + +// Head over to screen to get started. +export const Exercise = () => ; diff --git a/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/Form.tsx b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/Form.tsx new file mode 100644 index 0000000..e52e78f --- /dev/null +++ b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/Form.tsx @@ -0,0 +1,115 @@ +import { Skeleton } from '@shared/components/Skeleton/Skeleton.component'; +import { + TPokemonTypesApiResponse, + usePokedex +} from '@shared/hooks/usePokedex'; +import classNames from 'classnames'; +import { FormEvent, useState } from 'react'; + +interface IForm { + onPokemonTypesUpdate: (types: string[]) => void; +} + +export const Form = ({ onPokemonTypesUpdate }: IForm) => { + const [selectedPokemonTypes, setSelectedPokemonTypes] = useState< + string[] + >([]); + + const { data, isLoading } = usePokedex({ + path: 'types', + queryParams: 'pageSize=8' + }); + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + + if (selectedPokemonTypes.length === 4) { + onPokemonTypesUpdate(selectedPokemonTypes); + } + }; + + const onPokemonTypeSelection = (type: string) => { + if (selectedPokemonTypes.includes(type)) { + setSelectedPokemonTypes( + selectedPokemonTypes.filter( + (pokemonType) => pokemonType !== type + ) + ); + } else { + setSelectedPokemonTypes([...selectedPokemonTypes, type]); + } + }; + + return ( +
+

+ Select you favorite pokemon types (max 4) +

+
+ {isLoading && ( +
+ + + + + + + + + + + + +
+ )} +
+ {data && + data.length && + data.map((pokemonType) => { + const isSelected = + selectedPokemonTypes.includes(pokemonType); + const hasSelectedAllOptions = + selectedPokemonTypes.length === 4; + + return ( + + ); + })} +
+ + +
+
+ ); +}; diff --git a/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/PokemonOptions.tsx b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/PokemonOptions.tsx new file mode 100644 index 0000000..cb2c8e5 --- /dev/null +++ b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/PokemonOptions.tsx @@ -0,0 +1,141 @@ +import { Skeleton } from '@shared/components/Skeleton/Skeleton.component'; +import { + TPokemonCardsApiResponse, + usePokedex +} from '@shared/hooks/usePokedex'; +import classNames from 'classnames'; +import { FormEvent, useState } from 'react'; + +interface IPokemonOptions { + type: string; + onPokemonSelection: (pokemonIds: string[], type: string) => void; + defaultSelectedPokemon?: string[]; +} + +export const PokemonOptions = ({ + type, + onPokemonSelection, + defaultSelectedPokemon = [] +}: IPokemonOptions) => { + // ๐Ÿงผ State Colocation: we only want to have 2 cards in each component selected and use that for validation. + const [selectedPokemon, setSelectedPokemon] = useState( + // ๐Ÿชœ State Lifting: We inherit the lifted state to maintain the selected options when we change pokemon types. + defaultSelectedPokemon + ); + + // ๐Ÿงผ State Colocation: We want to only call this api for the type provided and not all the types selected. + const { data, isLoading, isError } = usePokedex< + TPokemonCardsApiResponse[] + >({ + path: 'cards', + queryParams: `pageSize=4&q=types:${type}&supertype:pokemon`, + skip: type === undefined + }); + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + + // ๐Ÿชœ State Lifting passing a function in as a prop to update the state above. + onPokemonSelection(selectedPokemon, type); + }; + + const togglePokemonSelection = (pokemonId: string) => { + if (selectedPokemon.includes(pokemonId)) { + setSelectedPokemon( + selectedPokemon.filter( + (selectedPokemonId) => selectedPokemonId !== pokemonId + ) + ); + } else { + const newlySelectedPokemon = [...selectedPokemon, pokemonId]; + // ๐Ÿงผ State Colocation: Updating the visual state of the selected pokemon. + setSelectedPokemon(newlySelectedPokemon); + + if (newlySelectedPokemon.length === 2) { + // ๐Ÿชœ State Lifting passing a function in as a prop to update the state above. + onPokemonSelection(newlySelectedPokemon, type); + } + } + }; + + return ( +
+

+ {type} Pokemon +

+
+
+ {isLoading && ( +
+ + + + +
+ )} +
+ {data && + data.length > 0 && + data.map((pokemonCard) => { + const isSelected = selectedPokemon.find( + (pokemonId) => pokemonId === pokemonCard.id + ); + const allPokemonSelected = + selectedPokemon.length === 2; + + return ( + + ); + })} +
+ + +
+
+
+ ); +}; diff --git a/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/Screen.tsx b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/Screen.tsx new file mode 100644 index 0000000..045650a --- /dev/null +++ b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/Screen.tsx @@ -0,0 +1,88 @@ +import { useState } from 'react'; +import { Form } from './Form'; +import { PokemonOptions } from './PokemonOptions'; + +export const Screen = () => { + // ๐Ÿชœ State Lifting: Setting up shared state variables so the components in this scope can read and use them + const [selectedPokemonTypes, setSelectedPokemonTypes] = useState< + string[] + >([]); + + // ๐Ÿชœ State Lifting: Setting up shared state variables so the components in this scope can read and use them + const [selectedPokemon, setSelectedPokemon] = useState< + Record + >({}); + + // ๐Ÿชœ State Lifting: Setting up a function which will update the state instead of bleeding this complexity into the UI component. + const onPokemonTypesUpdate = (types: string[]) => { + const newlyUpdatedPokemon = { ...selectedPokemon }; + + selectedPokemonTypes + .filter((type) => { + return !types.find((selectedType) => selectedType === type); + }) + .forEach((type) => { + delete newlyUpdatedPokemon[type]; + }); + + setSelectedPokemon(newlyUpdatedPokemon); + + setSelectedPokemonTypes(types); + }; + + // ๐Ÿชœ State Lifting: Setting up a function which will update the state instead of bleeding this complexity into the UI component. + const onPokemonSelection = (pokemon: string[], type: string) => { + const newPokemon = { ...selectedPokemon }; + newPokemon[type] = pokemon; + setSelectedPokemon(newPokemon); + }; + + return ( +
+ hello +
+
+
+

+ Pokemon Battle Picker + + Battle Picker + +

+
+
+
+
+
+ {selectedPokemonTypes.map((pokemonType, index) => ( + + ))} +
+ + {Object.keys(selectedPokemon).length === 4 && + selectedPokemonTypes.length === 4 && ( + + )} +
+
+ ); +}; diff --git a/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/final.stories.tsx b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/final.stories.tsx new file mode 100644 index 0000000..0b0f87b --- /dev/null +++ b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/final.stories.tsx @@ -0,0 +1,24 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Final } from './final'; + +const meta: Meta = { + title: + 'Lessons/๐Ÿฅ‰ Bronze/๐Ÿงผ State Colocation vs ๐Ÿชœ State Lifting/03-Final', + component: Final, + parameters: { + layout: 'fullscreen' + } +}; + +export default meta; +type Story = StoryObj; + +/* + * See https://storybook.js.org/docs/writing-stories/play-function#working-with-the-canvas + * to learn more about using the canvasElement to query the DOM + */ +export const Default: Story = { + play: async () => {}, + args: {} +}; diff --git a/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/final.tsx b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/final.tsx new file mode 100644 index 0000000..0bcdfa9 --- /dev/null +++ b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/final.tsx @@ -0,0 +1,3 @@ +import { Screen } from './components/Screen'; + +export const Final = () => ; diff --git a/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/lesson.mdx b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/lesson.mdx new file mode 100644 index 0000000..79ff880 --- /dev/null +++ b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/lesson.mdx @@ -0,0 +1,123 @@ +import { Meta } from '@storybook/blocks'; + + + +--- + +# ๐Ÿงผ State Colocation vs ๐Ÿชœ State Lifting + +Understanding the difference between **state colocation** and **state lifting** is crucial for managing state effectively in React apps. Although they both deal with the _placement_ of state, they serve **opposite purposes**. + +--- + +## ๐Ÿงผ State Colocation + +### What is it? + +Keep state **as close as possible** to where itโ€™s needed. + +```jsx +function SearchInput() { + const [query, setQuery] = useState(''); // colocated inside the component that needs it + + return ( + setQuery(e.target.value)} /> + ); +} +``` + +### What It Means: + +Only the component that directly uses the state should own it. This minimizes prop drilling and makes the component more self-contained. + +### When to Use + +- Only one component uses the state +- No need to share the state with siblings or parents + +### Benefits + +- Easier to maintain and test +- Reduces unnecessary props +- Keeps state logically scoped + +--- + +## ๐Ÿชœ State Lifting + +### What is it? + +Move state **up** the component tree to share it across multiple components. + +```jsx +function Parent() { + const [count, setCount] = useState(0); // lifted up here + + return ( + <> + setCount(count + 1)} + /> + + + ); +} +``` + +### What It Means + +If two or more components need access to or control over the same piece of state, lift it to their **closest common ancestor**. + +### When to Use + +- Two or more components need to read from or update the same state +- You need synchronized behavior across sibling components + +### Benefits + +- Keeps state in sync +- Prevents duplication or divergence of values +- Encourages better separation of concerns + +--- + +## ๐Ÿง  Summary Table + +| Pattern | Goal | Where State Lives | When to Use | +| -------------------- | ------------------------------------------ | ----------------------- | --------------------------------------- | +| **State Colocation** | Keep state near the component that uses it | Inside the component | When only one component needs the state | +| **State Lifting** | Share state across components | Common parent component | When multiple components need the state | + +--- + +## ๐Ÿง  Rule of Thumb + +- Use **colocation** by default. +- Use **lifting** when you need to **share or sync state** between components. + +--- + +## Exercise + +### Scenario + +You are creating a part of the new Pokemon Battle game and the screen you are working on is the "Pick your team" screen. We need to be able to choose up to four pokemon types and then choose 2 randomly picked pokemon from each type before we move onto the battle screen. The screen will work like this: + +1. You select four options from the form and select get Pokemon. +2. You will then be presented a grid which will display four different Pokemon per type. +3. You select two pokemon from each type to be your chosen pokemon for the battle +4. You click Ready to battle. + +### What we are going to do? + +The lead developer on the team has structured how the screen should work: + +- **components/Screen** - manages the state selectedPokemonTypes & selectedPokemonForBattle and handles the updating of those variables. +- **components/Form** - fetches all the pokemon types and displays a form where the player can select up to four types and then click get pokemon to update the screens state. +- **components/PokemonOptions** - fetches the pokemon for the type it has been given, handles the selection of only 2 in each group and then updates the screens state. +- **shared/hooks/usePokedex** - There is a reuseable react hook that we can already use in the form/pokemon options to display the data that we want to display. The pokedex has two apis known as "types" and "cards" api. + +## Issues + +Any issues or improvements to the course please raise [here](https://github.com/code-mattclaffey/react-design-patterns/issues/new). diff --git a/src/shared/hooks/usePokedex.ts b/src/shared/hooks/usePokedex.ts new file mode 100644 index 0000000..5b2062a --- /dev/null +++ b/src/shared/hooks/usePokedex.ts @@ -0,0 +1,84 @@ +// define the api types like a generic + +import { useEffect, useReducer } from 'react'; + +export type TPokemonCardsApiResponse = { + id: string; + name: string; + images: { + small: string; + }; +}; + +export type TPokemonTypesApiResponse = string; + +type TTypesApi = { + path: 'types'; + skip?: boolean; + queryParams?: string; +}; + +type TCardsApi = { + path: 'cards'; + queryParams?: string; + skip?: boolean; +}; + +interface IUsePokedexState { + data?: TResponse; + isError?: boolean; + isLoading?: boolean; +} + +const usePokedexReducer = ( + state: IUsePokedexState, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + action: { type: string; payload?: any } +): IUsePokedexState => { + switch (action.type) { + case 'SUCCESS': + return { ...state, isLoading: false, data: action.payload }; + case 'LOADING': + return { ...state, isLoading: true }; + case 'ERROR': + return { ...state, isLoading: false }; + default: + return state; + } +}; + +export const usePokedex = ({ + path, + queryParams = '', + skip = false +}: TCardsApi | TTypesApi): IUsePokedexState => { + const [state, dispatch] = useReducer(usePokedexReducer, { + isError: false, + isLoading: false, + data: undefined + }); + + useEffect(() => { + if (skip) return; + + const getData = async () => { + try { + dispatch({ type: 'LOADING' }); + const response = await fetch( + `https://api.pokemontcg.io/v2/${path}?${queryParams}` + ); + const json = await response.json(); + + dispatch({ type: 'SUCCESS', payload: json.data }); + } catch (e) { + dispatch({ type: 'ERROR' }); + + console.error('Error'); + } + }; + + getData(); + }, [skip, path, queryParams]); + + return state; +}; From 0143ceb5a9e6cd22a26aeb5e15a481cf40363999 Mon Sep 17 00:00:00 2001 From: code-mattclaffey Date: Fri, 23 May 2025 10:15:14 +0100 Subject: [PATCH 05/29] fix: button colours --- .../StateColocationVsStateLifting/final/components/Form.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/Form.tsx b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/Form.tsx index e52e78f..7b56896 100644 --- a/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/Form.tsx +++ b/src/course/02- lessons/01-Bronze/StateColocationVsStateLifting/final/components/Form.tsx @@ -75,15 +75,15 @@ export const Form = ({ onPokemonTypesUpdate }: IForm) => {
{ +interface ButtonProps extends HTMLAttributes { className?: string; iconLeft?: React.ReactNode; iconRight?: React.ReactNode; children: React.ReactNode | React.ReactNode[]; } -const buttonClasses = - 'middle none center rounded-lg bg-blue-500 py-3 px-6 font-sans text-xs font-bold uppercase text-white shadow-md shadow-blue-500/20 transition-all hover:shadow-lg hover:shadow-blue-500/40 focus:opacity-[0.85] focus:shadow-none active:opacity-[0.85] active:shadow-none disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none inline-flex items-center justify-center'; +const buttonClasses = [ + 'middle none center rounded-lg bg-blue-500 py-3 px-6', + 'font-sans text-xs font-bold uppercase text-white', + 'shadow-md shadow-blue-500/20 transition-all', + 'hover:shadow-lg hover:shadow-blue-500/40', + 'focus:opacity-[0.85] focus:shadow-none', + 'active:opacity-[0.85] active:shadow-none', + 'disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none', + 'inline-flex items-center justify-center' +].join(' '); export const Button = ({ className, @@ -18,7 +26,7 @@ export const Button = ({ iconLeft, iconRight, ...rest -}: IButton) => { +}: ButtonProps) => { return ( ; -}; + // 1f - ๐Ÿ‘จ๐Ÿป๐Ÿ’ป add onClick function onEarnBadge to the button + return ; +}; \ No newline at end of file diff --git a/src/course/02-lessons/01-Bronze/ConditionalRendering/final/final.stories.tsx b/src/course/02-lessons/01-Bronze/ConditionalRendering/final/final.stories.tsx index 7052c79..74bb2aa 100644 --- a/src/course/02-lessons/01-Bronze/ConditionalRendering/final/final.stories.tsx +++ b/src/course/02-lessons/01-Bronze/ConditionalRendering/final/final.stories.tsx @@ -1,17 +1,19 @@ import type { Meta, StoryObj } from '@storybook/react'; import { userEvent, within, expect } from '@storybook/test'; -import { ComponentOne } from './final'; -const meta: Meta = { - title: 'Lessons/๐Ÿฅ‰ Bronze/Conditional Rendering Pattern/03-Final', - component: ComponentOne +import { PokemonTrainerStatus } from './final'; + +const meta: Meta = { + title: + 'Lessons/๐Ÿฅ‰ Bronze/๐Ÿ”€ Conditional Rendering Pattern/03-Final', + component: PokemonTrainerStatus }; export default meta; -type Story = StoryObj; +type Story = StoryObj; -const username = 'John Doe'; +const trainerName = 'Ash'; /* * See https://storybook.js.org/docs/writing-stories/play-function#working-with-the-canvas @@ -22,28 +24,22 @@ export const Default: Story = { const canvas = within(canvasElement); await userEvent.click( - canvas.getByRole('button', { name: 'Login' }) + canvas.getByRole('button', { name: '๐ŸŽฏ Challenge Gym Leader' }) ); await expect( - canvas.getByText(`Welcome ${username}`) + canvas.getByText(`Welcome Gym Leader ${trainerName}! ๐Ÿ†`) ).toBeInTheDocument(); - await expect( - canvas.queryByRole('button', { name: 'Login' }) - ).toBeNull(); await userEvent.click( - canvas.getByRole('button', { name: 'Logout' }) + canvas.getByRole('button', { name: '๐Ÿ”„ Reset Journey' }) ); await expect( - canvas.queryByText(`Welcome ${username}`) - ).toBeNull(); - await expect( - canvas.queryByRole('button', { name: 'Logout' }) - ).toBeNull(); + canvas.getByRole('button', { name: '๐ŸŽฏ Challenge Gym Leader' }) + ).toBeInTheDocument(); }, args: { - username + trainerName } -}; +}; \ No newline at end of file diff --git a/src/course/02-lessons/01-Bronze/ConditionalRendering/final/final.tsx b/src/course/02-lessons/01-Bronze/ConditionalRendering/final/final.tsx index d2779ef..a7c89a8 100644 --- a/src/course/02-lessons/01-Bronze/ConditionalRendering/final/final.tsx +++ b/src/course/02-lessons/01-Bronze/ConditionalRendering/final/final.tsx @@ -1,31 +1,39 @@ import { useState } from 'react'; import { Button } from '@shared/components/Button/Button.component'; -interface IComponentProps { - username: string; +interface ITrainerProps { + trainerName: string; } -export const ComponentOne = (props: IComponentProps) => { - const [isAuthenticated, setIsAuthenticated] = useState(false); +export const PokemonTrainerStatus = (props: ITrainerProps) => { + const [hasGymBadges, setHasGymBadges] = useState(false); - const onLogin = () => { - setIsAuthenticated(true); + const onEarnBadge = () => { + setHasGymBadges(true); }; - const onLogout = () => { - setIsAuthenticated(false); + const onLoseBadges = () => { + setHasGymBadges(false); }; return ( -
- {/* Other components */} - {!isAuthenticated && } - {isAuthenticated && ( - <> - -

Welcome {props.username}

- +
+ {!hasGymBadges && ( +
+

๐ŸŽ’ Pokemon Trainer

+

Ready to challenge the Gym Leader?

+ +
)} -
+ {hasGymBadges && ( +
+

+ Welcome Gym Leader {props.trainerName}! ๐Ÿ† +

+

๐Ÿฅ‡ You've earned your gym badges!

+ +
+ )} +
); -}; +}; \ No newline at end of file diff --git a/src/course/02-lessons/01-Bronze/ConditionalRendering/lesson.mdx b/src/course/02-lessons/01-Bronze/ConditionalRendering/lesson.mdx index ccecd36..cc53d7b 100644 --- a/src/course/02-lessons/01-Bronze/ConditionalRendering/lesson.mdx +++ b/src/course/02-lessons/01-Bronze/ConditionalRendering/lesson.mdx @@ -1,17 +1,17 @@ import { Meta } from '@storybook/blocks'; - + -# Conditional Rendering Pattern +# ๐Ÿ”€ Conditional Rendering Pattern -The conditional rendering pattern is a way to dynamically change UI based what values are set at that time. The most common way that this is done is by using an if statement. For example: +The conditional rendering pattern is a way to dynamically change UI based on what values are set at that time. The most common way that this is done is by using an if statement. For example: ```jsx -const Component = (props) => { - if (props.isAuthenticated) { - return

Welcome {props.username}!

; +const TrainerStatus = (props) => { + if (props.hasGymBadges) { + return

Welcome Gym Leader {props.trainerName}!

; } else { - return

Not logged in.

; + return

Welcome Pokemon Trainer!

; } }; ``` @@ -19,11 +19,11 @@ const Component = (props) => { There are many syntactical ways you can do the same as above such as the ternary: ```jsx -const Component = (props) => { - return props.isAuthenticated ? ( -

Welcome {props.username}!

+const TrainerStatus = (props) => { + return props.hasGymBadges ? ( +

Welcome Gym Leader {props.trainerName}!

) : ( -

Not logged in.

+

Welcome Pokemon Trainer!

); }; ``` @@ -31,23 +31,23 @@ const Component = (props) => { Or you can use the AND syntax: ```jsx -const Component = (props) => { - return props.isAuthenticated &&

Welcome {props.username}!

; +const TrainerStatus = (props) => { + return props.hasGymBadges &&

Welcome Gym Leader {props.trainerName}!

; }; ``` -If I had a lot of complexity in this component that still would be getting executed but the component will just return nothing if it's not authenticated. The best way to return something like this is to do the conditional render outside of the component, for example: +If I had a lot of complexity in this component that still would be getting executed but the component will just return nothing if the trainer doesn't have badges. The best way to return something like this is to do the conditional render outside of the component, for example: ```jsx -const ComponentOne = (props) => { - return

Welcome {props.username}

; +const GymLeaderWelcome = (props) => { + return

Welcome Gym Leader {props.trainerName}

; }; -const ComponentTwo = (props) => { +const TrainerDashboard = (props) => { return (
{/* Other components */} - {props.isAuthenticated && } + {props.hasGymBadges && }
); }; @@ -55,28 +55,28 @@ const ComponentTwo = (props) => { ### Event driven rendering -There may be times when you need to conditionally render a component based on an event that has been changed. This example conditionally renders a box when you click the button. +There may be times when you need to conditionally render a component based on an event that has been changed. This example conditionally renders a wild Pokemon encounter when you click the explore button. ```jsx -const Component = () => { - const [displayBox, setDisplayBox] = useState(false); +const PokemonExplorer = () => { + const [wildPokemonFound, setWildPokemonFound] = useState(false); - const showBox = () => { - setDisplayBox(true); + const startExploring = () => { + setWildPokemonFound(true); }; - const hideBox = () => { - setDisplayBox(false); + const stopExploring = () => { + setWildPokemonFound(false); }; return ( <> - - {displayBox && ( -
-

Box

+ {wildPokemonFound && ( +
+

๐ŸŒฟ A wild Pikachu appeared!

)} @@ -86,7 +86,7 @@ const Component = () => { ## Exercise -In the first exercise we are going to look into building a login and logout toggle which will render a username when they have logged in. Go to the exercise.tsx inside the ConditionalRendering folder and start the exercise. Once completed, the Tests will show as passed in the storybook "Interactions" addon section. +In the first exercise we are going to look into building a Pokemon trainer status system that shows different content based on whether the trainer has earned gym badges. Go to the exercise.tsx inside the ConditionalRendering folder and start the exercise. Once completed, the Tests will show as passed in the storybook "Interactions" addon section. ## Feedback diff --git a/src/course/02-lessons/01-Bronze/Hooks/exercise/exercise.stories.tsx b/src/course/02-lessons/01-Bronze/Hooks/exercise/exercise.stories.tsx index 2407fb0..2781dbc 100644 --- a/src/course/02-lessons/01-Bronze/Hooks/exercise/exercise.stories.tsx +++ b/src/course/02-lessons/01-Bronze/Hooks/exercise/exercise.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Exercise } from './exercise'; const meta: Meta = { - title: 'Lessons/๐Ÿฅ‰ Bronze/Hooks Pattern/02-Exercise', + title: 'Lessons/๐Ÿฅ‰ Bronze/๐ŸŽฃ Hooks Pattern/02-Exercise', component: Exercise }; diff --git a/src/course/02-lessons/01-Bronze/Hooks/final/final.stories.tsx b/src/course/02-lessons/01-Bronze/Hooks/final/final.stories.tsx index bc3c325..0e69151 100644 --- a/src/course/02-lessons/01-Bronze/Hooks/final/final.stories.tsx +++ b/src/course/02-lessons/01-Bronze/Hooks/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/๐Ÿฅ‰ Bronze/Hooks Pattern/03-Final', + title: 'Lessons/๐Ÿฅ‰ Bronze/๐ŸŽฃ Hooks Pattern/03-Final', component: Final }; diff --git a/src/course/02-lessons/01-Bronze/Hooks/lesson.mdx b/src/course/02-lessons/01-Bronze/Hooks/lesson.mdx index 6ab9443..aba50c0 100644 --- a/src/course/02-lessons/01-Bronze/Hooks/lesson.mdx +++ b/src/course/02-lessons/01-Bronze/Hooks/lesson.mdx @@ -1,8 +1,8 @@ import { Meta } from '@storybook/blocks'; - + -# Hooks Pattern +# ๐ŸŽฃ Hooks Pattern React Hooks, introduced in React 16.8, revolutionize the traditional presentational and container component pattern by allowing functional components to manage state and side effects directly. This means you no longer need to separate components into presentational (stateless) and container (stateful) types. Hooks like useState and useEffect enable functional components to handle both UI rendering and business logic, simplifying code and improving readability. diff --git a/src/course/02-lessons/01-Bronze/PresentationalAndContainer/exercise/exercise.stories.tsx b/src/course/02-lessons/01-Bronze/PresentationalAndContainer/exercise/exercise.stories.tsx index 0f004ef..3bd9880 100644 --- a/src/course/02-lessons/01-Bronze/PresentationalAndContainer/exercise/exercise.stories.tsx +++ b/src/course/02-lessons/01-Bronze/PresentationalAndContainer/exercise/exercise.stories.tsx @@ -4,7 +4,7 @@ import { BrandPageOne, BrandPageTwo } from './exercise'; const meta: Meta = { title: - 'Lessons/๐Ÿฅ‰ Bronze/Presentational & Container Pattern/02-Exercise', + 'Lessons/๐Ÿฅ‰ Bronze/๐ŸŽญ Presentational & Container Pattern/02-Exercise', component: BrandPageOne }; diff --git a/src/course/02-lessons/01-Bronze/PresentationalAndContainer/final/final.stories.tsx b/src/course/02-lessons/01-Bronze/PresentationalAndContainer/final/final.stories.tsx index 448e8fe..5b3caf5 100644 --- a/src/course/02-lessons/01-Bronze/PresentationalAndContainer/final/final.stories.tsx +++ b/src/course/02-lessons/01-Bronze/PresentationalAndContainer/final/final.stories.tsx @@ -4,7 +4,7 @@ import { BrandPageOne, BrandPageTwo } from './final'; const meta: Meta = { title: - 'Lessons/๐Ÿฅ‰ Bronze/Presentational & Container Pattern/03-Final', + 'Lessons/๐Ÿฅ‰ Bronze/๐ŸŽญ Presentational & Container Pattern/03-Final', component: BrandPageOne }; diff --git a/src/course/02-lessons/01-Bronze/PresentationalAndContainer/lesson.mdx b/src/course/02-lessons/01-Bronze/PresentationalAndContainer/lesson.mdx index 080b3ae..b01ec5a 100644 --- a/src/course/02-lessons/01-Bronze/PresentationalAndContainer/lesson.mdx +++ b/src/course/02-lessons/01-Bronze/PresentationalAndContainer/lesson.mdx @@ -1,8 +1,8 @@ import { Meta } from '@storybook/blocks'; - + -# Presentational & Container Pattern +# ๐ŸŽญ Presentational & Container Pattern This is an old pattern created by Dan Abramov. However, he doesn't suggest to use this pattern anymore as it was more linked to before reack hooks. This pattern has become more obsolete since hooks came into the mix. diff --git a/src/course/02-lessons/01-Bronze/PropsCombination/exercise/exercise.stories.tsx b/src/course/02-lessons/01-Bronze/PropsCombination/exercise/exercise.stories.tsx index 3a85de5..97894a9 100644 --- a/src/course/02-lessons/01-Bronze/PropsCombination/exercise/exercise.stories.tsx +++ b/src/course/02-lessons/01-Bronze/PropsCombination/exercise/exercise.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Exercise } from './exercise'; const meta: Meta = { - title: 'Lessons/๐Ÿฅ‰ Bronze/Props Combination Pattern/02-Exercise', + title: 'Lessons/๐Ÿฅ‰ Bronze/๐Ÿงฉ Props Combination Pattern/02-Exercise', component: Exercise }; diff --git a/src/course/02-lessons/01-Bronze/PropsCombination/final/final.stories.tsx b/src/course/02-lessons/01-Bronze/PropsCombination/final/final.stories.tsx index fe71bb7..71c16d1 100644 --- a/src/course/02-lessons/01-Bronze/PropsCombination/final/final.stories.tsx +++ b/src/course/02-lessons/01-Bronze/PropsCombination/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/๐Ÿฅ‰ Bronze/Props Combination Pattern/03-Final', + title: 'Lessons/๐Ÿฅ‰ Bronze/๐Ÿงฉ Props Combination Pattern/03-Final', component: Final }; diff --git a/src/course/02-lessons/01-Bronze/PropsCombination/lesson.mdx b/src/course/02-lessons/01-Bronze/PropsCombination/lesson.mdx index 2115d3c..da091c1 100644 --- a/src/course/02-lessons/01-Bronze/PropsCombination/lesson.mdx +++ b/src/course/02-lessons/01-Bronze/PropsCombination/lesson.mdx @@ -1,8 +1,8 @@ import { Meta } from '@storybook/blocks'; - + -# Props Combination Pattern +# ๐Ÿงฉ Props Combination Pattern Props are used to pass data from one component to another. The prop combination pattern groups related props into a single object. This object is then passed as a single prop to a component. diff --git a/src/course/02-lessons/01-Bronze/Slots/exercise/exercise.stories.tsx b/src/course/02-lessons/01-Bronze/Slots/exercise/exercise.stories.tsx index a40b57b..0f6fdc4 100644 --- a/src/course/02-lessons/01-Bronze/Slots/exercise/exercise.stories.tsx +++ b/src/course/02-lessons/01-Bronze/Slots/exercise/exercise.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Exercise } from './exercise'; const meta: Meta = { - title: 'Lessons/๐Ÿฅ‰ Bronze/Slots/02-Exercise', + title: 'Lessons/๐Ÿฅ‰ Bronze/๐ŸŽฐ Slots Pattern/02-Exercise', component: Exercise }; diff --git a/src/course/02-lessons/01-Bronze/Slots/final/final.stories.tsx b/src/course/02-lessons/01-Bronze/Slots/final/final.stories.tsx index 8fb0fda..b8ac096 100644 --- a/src/course/02-lessons/01-Bronze/Slots/final/final.stories.tsx +++ b/src/course/02-lessons/01-Bronze/Slots/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/๐Ÿฅ‰ Bronze/Slots/03-Final', + title: 'Lessons/๐Ÿฅ‰ Bronze/๐ŸŽฐ Slots Pattern/03-Final', component: Final }; diff --git a/src/course/02-lessons/01-Bronze/Slots/lesson.mdx b/src/course/02-lessons/01-Bronze/Slots/lesson.mdx index 3538912..79b8743 100644 --- a/src/course/02-lessons/01-Bronze/Slots/lesson.mdx +++ b/src/course/02-lessons/01-Bronze/Slots/lesson.mdx @@ -1,8 +1,8 @@ import { Meta } from '@storybook/blocks'; - + -# Slots Pattern +# ๐ŸŽฐ Slots Pattern The slots pattern is very similar to the [render props pattern](?path=/docs/lessons-03-render-props-pattern-01-lesson--docs) but it has a slight difference which justifies why you would do one over the other. diff --git a/src/course/02-lessons/02-Silver/Compound/exercise/exercise.stories.tsx b/src/course/02-lessons/02-Silver/Compound/exercise/exercise.stories.tsx index 2553528..d4f8ce4 100644 --- a/src/course/02-lessons/02-Silver/Compound/exercise/exercise.stories.tsx +++ b/src/course/02-lessons/02-Silver/Compound/exercise/exercise.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Exercise } from './exercise'; const meta: Meta = { - title: 'Lessons/๐Ÿฅˆ Silver/Compound Components Pattern/02-Exercise', + title: 'Lessons/๐Ÿฅˆ Silver/๐Ÿงฉ Compound Components Pattern/02-Exercise', component: Exercise }; diff --git a/src/course/02-lessons/02-Silver/Compound/final/final.stories.tsx b/src/course/02-lessons/02-Silver/Compound/final/final.stories.tsx index e7a6771..f8fb201 100644 --- a/src/course/02-lessons/02-Silver/Compound/final/final.stories.tsx +++ b/src/course/02-lessons/02-Silver/Compound/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/๐Ÿฅˆ Silver/Compound Components Pattern/03-Final', + title: 'Lessons/๐Ÿฅˆ Silver/๐Ÿงฉ Compound Components Pattern/03-Final', component: Final }; diff --git a/src/course/02-lessons/02-Silver/Compound/lesson.mdx b/src/course/02-lessons/02-Silver/Compound/lesson.mdx index 739f3a3..7bbd99d 100644 --- a/src/course/02-lessons/02-Silver/Compound/lesson.mdx +++ b/src/course/02-lessons/02-Silver/Compound/lesson.mdx @@ -1,8 +1,8 @@ import { Meta } from '@storybook/blocks'; - + -# Compound Components Pattern +# ๐Ÿงฉ Compound Components Pattern Compound components is an advanced React container pattern that provides a simple and efficient way for multiple components to share states and handle logic โ€” working together. diff --git a/src/course/02-lessons/02-Silver/Controlled/exercise/exercise.stories.tsx b/src/course/02-lessons/02-Silver/Controlled/exercise/exercise.stories.tsx index 99a98cd..e9e36b5 100644 --- a/src/course/02-lessons/02-Silver/Controlled/exercise/exercise.stories.tsx +++ b/src/course/02-lessons/02-Silver/Controlled/exercise/exercise.stories.tsx @@ -4,7 +4,7 @@ import { Exercise } from './exercise'; const meta: Meta = { title: - 'Lessons/๐Ÿฅˆ Silver/Controlled Components Pattern/02-Exercise', + 'Lessons/๐Ÿฅˆ Silver/๐ŸŽฎ Controlled Components Pattern/02-Exercise', component: Exercise }; diff --git a/src/course/02-lessons/02-Silver/Controlled/final/final.stories.tsx b/src/course/02-lessons/02-Silver/Controlled/final/final.stories.tsx index 56ee988..ea63a77 100644 --- a/src/course/02-lessons/02-Silver/Controlled/final/final.stories.tsx +++ b/src/course/02-lessons/02-Silver/Controlled/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/๐Ÿฅˆ Silver/Controlled Components Pattern/03-Final', + title: 'Lessons/๐Ÿฅˆ Silver/๐ŸŽฎ Controlled Components Pattern/03-Final', component: Final }; diff --git a/src/course/02-lessons/02-Silver/Controlled/lesson.mdx b/src/course/02-lessons/02-Silver/Controlled/lesson.mdx index 77f4b7f..f1226fa 100644 --- a/src/course/02-lessons/02-Silver/Controlled/lesson.mdx +++ b/src/course/02-lessons/02-Silver/Controlled/lesson.mdx @@ -1,8 +1,8 @@ import { Meta } from '@storybook/blocks'; - + -# Controlled Components Pattern +# ๐ŸŽฎ Controlled Components Pattern The concept of controlled components involves creating components with highly predictable behavior by managing their state through props. A controlled components behavior changes based on the state passed to it as a prop. diff --git a/src/course/02-lessons/02-Silver/PolymorphicComponents/exercise/exercise.stories.tsx b/src/course/02-lessons/02-Silver/PolymorphicComponents/exercise/exercise.stories.tsx index 6e64026..b729d74 100644 --- a/src/course/02-lessons/02-Silver/PolymorphicComponents/exercise/exercise.stories.tsx +++ b/src/course/02-lessons/02-Silver/PolymorphicComponents/exercise/exercise.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Exercise } from './exercise'; const meta: Meta = { - title: 'Lessons/๐Ÿฅˆ Silver/Polymorphic Components/02-Exercise', + title: 'Lessons/๐Ÿฅˆ Silver/๐Ÿฆ„ Polymorphic Components Pattern/02-Exercise', component: Exercise }; diff --git a/src/course/02-lessons/02-Silver/PolymorphicComponents/final/final.stories.tsx b/src/course/02-lessons/02-Silver/PolymorphicComponents/final/final.stories.tsx index e677c1f..c4fed3a 100644 --- a/src/course/02-lessons/02-Silver/PolymorphicComponents/final/final.stories.tsx +++ b/src/course/02-lessons/02-Silver/PolymorphicComponents/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/๐Ÿฅˆ Silver/Polymorphic Components/03-Final', + title: 'Lessons/๐Ÿฅˆ Silver/๐Ÿฆ„ Polymorphic Components Pattern/03-Final', component: Final }; diff --git a/src/course/02-lessons/02-Silver/PolymorphicComponents/lesson.mdx b/src/course/02-lessons/02-Silver/PolymorphicComponents/lesson.mdx index 7e43870..1f713d3 100644 --- a/src/course/02-lessons/02-Silver/PolymorphicComponents/lesson.mdx +++ b/src/course/02-lessons/02-Silver/PolymorphicComponents/lesson.mdx @@ -1,8 +1,8 @@ import { Meta } from '@storybook/blocks'; - + -# Polymorphic Components Pattern +# ๐Ÿฆ„ Polymorphic Components Pattern React excels at building reusable components, but repetition can creep in when components only differ slightlyโ€”like headings or buttons with varied HTML tags. diff --git a/src/course/02-lessons/02-Silver/Portals/exercise/exercise.stories.tsx b/src/course/02-lessons/02-Silver/Portals/exercise/exercise.stories.tsx index 0bae7c2..c57ec29 100644 --- a/src/course/02-lessons/02-Silver/Portals/exercise/exercise.stories.tsx +++ b/src/course/02-lessons/02-Silver/Portals/exercise/exercise.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Exercise } from './exercise'; const meta: Meta = { - title: 'Lessons/๐Ÿฅˆ Silver/Portals/02-Exercise', + title: 'Lessons/๐Ÿฅˆ Silver/๐ŸŒŒ Portals Pattern/02-Exercise', component: Exercise }; diff --git a/src/course/02-lessons/02-Silver/Portals/final/final.stories.tsx b/src/course/02-lessons/02-Silver/Portals/final/final.stories.tsx index f908de1..fd4502b 100644 --- a/src/course/02-lessons/02-Silver/Portals/final/final.stories.tsx +++ b/src/course/02-lessons/02-Silver/Portals/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/๐Ÿฅˆ Silver/Portals/03-Final', + title: 'Lessons/๐Ÿฅˆ Silver/๐ŸŒŒ Portals Pattern/03-Final', component: Final }; diff --git a/src/course/02-lessons/02-Silver/Portals/lesson.mdx b/src/course/02-lessons/02-Silver/Portals/lesson.mdx index 75767b6..1442280 100644 --- a/src/course/02-lessons/02-Silver/Portals/lesson.mdx +++ b/src/course/02-lessons/02-Silver/Portals/lesson.mdx @@ -1,8 +1,8 @@ import { Meta } from '@storybook/blocks'; - + -# Portals Pattern +# ๐ŸŒŒ Portals Pattern A React portal lets you render some children into a different part of the DOM. When you call the **createPortal** it will trigger the creation of the portal. When you unmount, the portal removes itself. This is how it looks in React: diff --git a/src/course/02-lessons/02-Silver/Provider/exercise/exercise.stories.tsx b/src/course/02-lessons/02-Silver/Provider/exercise/exercise.stories.tsx index 634dca0..d329d80 100644 --- a/src/course/02-lessons/02-Silver/Provider/exercise/exercise.stories.tsx +++ b/src/course/02-lessons/02-Silver/Provider/exercise/exercise.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Exercise } from './exercise'; const meta: Meta = { - title: 'Lessons/๐Ÿฅˆ Silver/Provider Pattern/02-Exercise', + title: 'Lessons/๐Ÿฅˆ Silver/๐ŸŽ Provider Pattern/02-Exercise', component: Exercise }; diff --git a/src/course/02-lessons/02-Silver/Provider/final/final.stories.tsx b/src/course/02-lessons/02-Silver/Provider/final/final.stories.tsx index 78349af..6b9ad89 100644 --- a/src/course/02-lessons/02-Silver/Provider/final/final.stories.tsx +++ b/src/course/02-lessons/02-Silver/Provider/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/๐Ÿฅˆ Silver/Provider Pattern/03-Final', + title: 'Lessons/๐Ÿฅˆ Silver/๐ŸŽ Provider Pattern/03-Final', component: Final }; diff --git a/src/course/02-lessons/02-Silver/Provider/lesson.mdx b/src/course/02-lessons/02-Silver/Provider/lesson.mdx index 31cd02d..7d89fc6 100644 --- a/src/course/02-lessons/02-Silver/Provider/lesson.mdx +++ b/src/course/02-lessons/02-Silver/Provider/lesson.mdx @@ -1,8 +1,8 @@ import { Meta } from '@storybook/blocks'; - + -# Provider Pattern +# ๐ŸŽ Provider Pattern The Provider Pattern is a design approach used to efficiently supply data to various parts of an application without the need to manually pass it down through multiple layers of components. In the context of React, this pattern helps avoid "prop drilling," where props have to be passed through each component in the hierarchy until they reach the desired component. Instead, by utilizing React Context, you can create a provider component that wraps parts of your app, making the necessary data or functionality available to all nested components. These components can then access the data or functions directly via custom React hooks, leading to cleaner, more maintainable code. diff --git a/src/course/02-lessons/02-Silver/RenderProps/exercise/exercise.stories.tsx b/src/course/02-lessons/02-Silver/RenderProps/exercise/exercise.stories.tsx index 133be89..ee6894a 100644 --- a/src/course/02-lessons/02-Silver/RenderProps/exercise/exercise.stories.tsx +++ b/src/course/02-lessons/02-Silver/RenderProps/exercise/exercise.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Exercise } from './exercise'; const meta: Meta = { - title: 'Lessons/๐Ÿฅˆ Silver/Render Props Pattern/02-Exercise', + title: 'Lessons/๐Ÿฅˆ Silver/๐ŸŽจ Render Props Pattern/02-Exercise', component: Exercise }; diff --git a/src/course/02-lessons/02-Silver/RenderProps/final/final.stories.tsx b/src/course/02-lessons/02-Silver/RenderProps/final/final.stories.tsx index cd576d2..42bf44f 100644 --- a/src/course/02-lessons/02-Silver/RenderProps/final/final.stories.tsx +++ b/src/course/02-lessons/02-Silver/RenderProps/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/๐Ÿฅˆ Silver/Render Props Pattern/03-Final', + title: 'Lessons/๐Ÿฅˆ Silver/๐ŸŽจ Render Props Pattern/03-Final', component: Final }; diff --git a/src/course/02-lessons/02-Silver/RenderProps/lesson.mdx b/src/course/02-lessons/02-Silver/RenderProps/lesson.mdx index c9b5497..b72aa24 100644 --- a/src/course/02-lessons/02-Silver/RenderProps/lesson.mdx +++ b/src/course/02-lessons/02-Silver/RenderProps/lesson.mdx @@ -1,8 +1,8 @@ import { Meta } from '@storybook/blocks'; - + -# Render Props Pattern +# ๐ŸŽจ Render Props Pattern Render props is a common pattern you will see in popular NPM packages like [Formik](https://formik.org/) or [React Final form](https://final-form.org/react) and it is very useful for building components that manage the logic and pass information to their children/prop so they can use that logic in the UI layer. diff --git a/src/course/02-lessons/02-Silver/StateReducer/exercise/exercise.stories.tsx b/src/course/02-lessons/02-Silver/StateReducer/exercise/exercise.stories.tsx index 08eace5..3880071 100644 --- a/src/course/02-lessons/02-Silver/StateReducer/exercise/exercise.stories.tsx +++ b/src/course/02-lessons/02-Silver/StateReducer/exercise/exercise.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Exercise } from './exercise'; const meta: Meta = { - title: 'Lessons/๐Ÿฅˆ Silver/State Reducer Pattern/02-Exercise', + title: 'Lessons/๐Ÿฅˆ Silver/โš™๏ธ State Reducer Pattern/02-Exercise', component: Exercise }; diff --git a/src/course/02-lessons/02-Silver/StateReducer/final/final.stories.tsx b/src/course/02-lessons/02-Silver/StateReducer/final/final.stories.tsx index d458cab..7c4c75c 100644 --- a/src/course/02-lessons/02-Silver/StateReducer/final/final.stories.tsx +++ b/src/course/02-lessons/02-Silver/StateReducer/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/๐Ÿฅˆ Silver/State Reducer Pattern/03-Final', + title: 'Lessons/๐Ÿฅˆ Silver/โš™๏ธ State Reducer Pattern/03-Final', component: Final }; diff --git a/src/course/02-lessons/02-Silver/StateReducer/lesson.mdx b/src/course/02-lessons/02-Silver/StateReducer/lesson.mdx index ffbf94e..20fb7cf 100644 --- a/src/course/02-lessons/02-Silver/StateReducer/lesson.mdx +++ b/src/course/02-lessons/02-Silver/StateReducer/lesson.mdx @@ -1,8 +1,8 @@ import { Meta } from '@storybook/blocks'; - + -# State Reducer Pattern +# โš™๏ธ State Reducer Pattern When you have a component which has a lot of useState logic and it updates multiple state values are a sign to start refacotring over to using the State Reducer Pattern. diff --git a/src/course/02-lessons/03-Gold/HigherOrderComponents/exercise/exercise.stories.tsx b/src/course/02-lessons/03-Gold/HigherOrderComponents/exercise/exercise.stories.tsx index adedfa2..ec8548e 100644 --- a/src/course/02-lessons/03-Gold/HigherOrderComponents/exercise/exercise.stories.tsx +++ b/src/course/02-lessons/03-Gold/HigherOrderComponents/exercise/exercise.stories.tsx @@ -4,7 +4,7 @@ import { Exercise } from './exercise'; const meta: Meta = { title: - 'Lessons/๐Ÿฅ‡ Gold/Higher Order Components Pattern/02-Exercise', + 'Lessons/๐Ÿฅ‡ Gold/๐ŸŽ† Higher Order Components Pattern/02-Exercise', component: Exercise }; diff --git a/src/course/02-lessons/03-Gold/HigherOrderComponents/final/final.stories.tsx b/src/course/02-lessons/03-Gold/HigherOrderComponents/final/final.stories.tsx index 43ddf05..c8861d6 100644 --- a/src/course/02-lessons/03-Gold/HigherOrderComponents/final/final.stories.tsx +++ b/src/course/02-lessons/03-Gold/HigherOrderComponents/final/final.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Final } from './final'; const meta: Meta = { - title: 'Lessons/๐Ÿฅ‡ Gold/Higher Order Components Pattern/03-Final', + title: 'Lessons/๐Ÿฅ‡ Gold/๐ŸŽ† Higher Order Components Pattern/03-Final', component: Final }; diff --git a/src/course/02-lessons/03-Gold/HigherOrderComponents/lesson.mdx b/src/course/02-lessons/03-Gold/HigherOrderComponents/lesson.mdx index eeddd2b..e4df04d 100644 --- a/src/course/02-lessons/03-Gold/HigherOrderComponents/lesson.mdx +++ b/src/course/02-lessons/03-Gold/HigherOrderComponents/lesson.mdx @@ -1,8 +1,8 @@ import { Meta } from '@storybook/blocks'; - + -# Higher Order Components Pattern +# ๐ŸŽ† Higher Order Components Pattern In some cases you may want to have some logic that is consistent across your application. You could use hooks but this requires implementing some logic of code within each component you apply the hook which isn't sustainable as a higher order component. From 5badabf12b15b43a705f9a178fc518d4136d8d62 Mon Sep 17 00:00:00 2001 From: Matthew Claffey Date: Thu, 4 Sep 2025 10:37:53 +0100 Subject: [PATCH 10/29] feat: rebrand props combination pattern (#45) --- .../exercise/exercise.stories.tsx | 34 ++-- .../PropsCombination/exercise/exercise.tsx | 147 ++++++++++-------- .../PropsCombination/final/final.stories.tsx | 39 +++-- .../PropsCombination/final/final.tsx | 120 ++++++++------ .../01-Bronze/PropsCombination/lesson.mdx | 53 ++++--- 5 files changed, 226 insertions(+), 167 deletions(-) diff --git a/src/course/02-lessons/01-Bronze/PropsCombination/exercise/exercise.stories.tsx b/src/course/02-lessons/01-Bronze/PropsCombination/exercise/exercise.stories.tsx index 97894a9..83e2daf 100644 --- a/src/course/02-lessons/01-Bronze/PropsCombination/exercise/exercise.stories.tsx +++ b/src/course/02-lessons/01-Bronze/PropsCombination/exercise/exercise.stories.tsx @@ -16,22 +16,22 @@ type Story = StoryObj; */ export const Default: Story = { args: { - title: 'The Coldest Sunset Festival', - subText: - 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Voluptatibus quia, nulla! Maiores et perferendis eaque, exercitationem praesentium nihil.', - ctaText: '#festival', - ctaUrl: '/', - imageAltText: 'DJ playing at a festival', - imageUrlMobile: - 'https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?w=767&h=640&fit=crop', - imageUrlTablet: - 'https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?w=1024&h=640&fit=crop', - imageUrlDesktop: - 'https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?w=1600&h=900&fit=crop', - containerClassName: 'containerClassName', - titleClassName: 'titleClassName', - subTextClassName: 'subTextClassName', - ctaClassName: 'ctaClassName', - imageClassName: 'imageClassName' + pokemonName: 'Charizard', + pokemonType: 'Fire/Flying', + pokemonHp: 180, + pokemonLevel: 55, + attackName: 'Fire Blast', + attackDamage: 120, + attackDescription: 'A powerful fire attack that may leave the target with a burn.', + imageAltText: 'Charizard breathing fire', + imageUrlSmall: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/6.png', + imageUrlMedium: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/6.png', + imageUrlLarge: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/6.png', + cardClassName: 'shadow-xl', + nameClassName: 'text-red-600', + typeClassName: 'bg-red-500', + hpClassName: 'text-red-700', + attackClassName: 'border-red-400', + imageClassName: 'hover:scale-105 transition-transform' } }; diff --git a/src/course/02-lessons/01-Bronze/PropsCombination/exercise/exercise.tsx b/src/course/02-lessons/01-Bronze/PropsCombination/exercise/exercise.tsx index ffa858a..c51e03a 100644 --- a/src/course/02-lessons/01-Bronze/PropsCombination/exercise/exercise.tsx +++ b/src/course/02-lessons/01-Bronze/PropsCombination/exercise/exercise.tsx @@ -2,104 +2,129 @@ import classnames from 'classnames'; /* - 1a๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป group the following together: + 1a๐Ÿ‘จ๐Ÿป๐Ÿ’ป group the following together: - * image - imageAltText, imageUrlMobile, imageUrlTablet, imageUrlDesktop - * cta - ctaText, ctaUrl - * classNames - containerClassName, titleClassName, subTextClassName, ctaClassName, imageClassName + * pokemon - pokemonName, pokemonType, pokemonHp, pokemonLevel + * attack - attackName, attackDamage, attackDescription + * image - imageAltText, imageUrlSmall, imageUrlMedium, imageUrlLarge + * styling - cardClassName, nameClassName, typeClassName, hpClassName, attackClassName, imageClassName */ -interface IExerciseProps { - title: string; - subText: string; - ctaText: string; - ctaUrl: string; +interface IPokemonCardProps { + pokemonName: string; + pokemonType: string; + pokemonHp: number; + pokemonLevel: number; + attackName: string; + attackDamage: number; + attackDescription: string; imageAltText: string; - imageUrlMobile: string; - imageUrlTablet: string; - imageUrlDesktop: string; - containerClassName?: string; - titleClassName?: string; - subTextClassName?: string; - ctaClassName?: string; + imageUrlSmall: string; + imageUrlMedium: string; + imageUrlLarge: string; + cardClassName?: string; + nameClassName?: string; + typeClassName?: string; + hpClassName?: string; + attackClassName?: string; imageClassName?: string; } /* - 1b๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Update the props to match the new types defined above. + 1b๐Ÿ‘จ๐Ÿป๐Ÿ’ป Update the props to match the new grouped types defined above. */ export const Exercise = ({ - title, - subText, - ctaText, - ctaUrl, + pokemonName, + pokemonType, + pokemonHp, + pokemonLevel, + attackName, + attackDamage, + attackDescription, imageAltText, - imageUrlMobile, - imageUrlTablet, - imageUrlDesktop, - containerClassName, - titleClassName, - subTextClassName, - ctaClassName, + imageUrlSmall, + imageUrlMedium, + imageUrlLarge, + cardClassName, + nameClassName, + typeClassName, + hpClassName, + attackClassName, imageClassName -}: IExerciseProps) => { +}: IPokemonCardProps) => { /* - 2a ๐Ÿค” Could we destructure the image to be [mobile, tablet, desktop]? + 2a ๐Ÿค” Could we destructure the image to be [small, medium, large]? */ /* - 1c๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Update the props in the jsx + 1c๐Ÿ‘จ๐Ÿป๐Ÿ’ป Update the props in the jsx to use the grouped structure */ return (
- - {/* โœ๐Ÿป picture elements are a great way to display responsive images */} - {/* โœ๐Ÿป Using rem instead of pixels will change the image when you zoom in the page */} - {/* โœ๐Ÿป Link: https://web.dev/learn/design/picture-element */} - - - +
+ + โญ Level {pokemonLevel} + +
+ + + + + {imageAltText} +

- {title} + {pokemonName}

-

- {subText} -

-
- + +
- {ctaText} - +

+ โšก {attackName} - {attackDamage} damage +

+

{attackDescription}

+
); diff --git a/src/course/02-lessons/01-Bronze/PropsCombination/final/final.stories.tsx b/src/course/02-lessons/01-Bronze/PropsCombination/final/final.stories.tsx index 71c16d1..3bd8eba 100644 --- a/src/course/02-lessons/01-Bronze/PropsCombination/final/final.stories.tsx +++ b/src/course/02-lessons/01-Bronze/PropsCombination/final/final.stories.tsx @@ -16,27 +16,32 @@ type Story = StoryObj; */ export const Default: Story = { args: { - title: 'The Coldest Sunset Festival', - subText: - 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Voluptatibus quia, nulla! Maiores et perferendis eaque, exercitationem praesentium nihil.', - cta: { - text: '#festival', - url: '/' + pokemon: { + name: 'Pikachu', + type: 'Electric', + hp: 120, + level: 25 + }, + attack: { + name: 'Thunderbolt', + damage: 90, + description: 'A strong electric blast that may paralyze the target.' }, image: { - alt: 'DJ playing at a festival', - images: [ - 'https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?w=767&h=640&fit=crop', - 'https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?w=1024&h=640&fit=crop', - 'https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?w=1600&h=900&fit=crop' + alt: 'Pikachu with electric sparks', + sources: [ + 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/25.png', + 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/25.png', + 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/25.png' ] }, - classNames: { - container: 'container', - title: 'title', - subText: 'subText', - cta: 'cta', - image: 'image' + styling: { + card: 'shadow-xl border-yellow-400', + name: 'text-yellow-600', + type: 'bg-yellow-500', + hp: 'text-yellow-700', + attack: 'border-yellow-400', + image: 'hover:scale-105 transition-transform' } } }; diff --git a/src/course/02-lessons/01-Bronze/PropsCombination/final/final.tsx b/src/course/02-lessons/01-Bronze/PropsCombination/final/final.tsx index cc5a54f..9227d28 100644 --- a/src/course/02-lessons/01-Bronze/PropsCombination/final/final.tsx +++ b/src/course/02-lessons/01-Bronze/PropsCombination/final/final.tsx @@ -1,84 +1,108 @@ import classnames from 'classnames'; -interface IFinalProps { - title: string; - subText: string; - cta: { - text: string; - url: string; +interface IPokemonCardProps { + pokemon: { + name: string; + type: string; + hp: number; + level: number; + }; + attack: { + name: string; + damage: number; + description: string; }; image: { alt: string; - images: string[]; + sources: string[]; }; - classNames?: { - container?: string; - title?: string; - subText?: string; - cta?: string; + styling?: { + card?: string; + name?: string; + type?: string; + hp?: string; + attack?: string; image?: string; }; } export const Final = ({ - title, - subText, - cta, + pokemon, + attack, image, - classNames -}: IFinalProps) => { - const [mobile, tablet, desktop] = image.images; + styling +}: IPokemonCardProps) => { + const [small, medium, large] = image.sources; return ( ); diff --git a/src/course/02-lessons/01-Bronze/PropsCombination/lesson.mdx b/src/course/02-lessons/01-Bronze/PropsCombination/lesson.mdx index da091c1..237ff54 100644 --- a/src/course/02-lessons/01-Bronze/PropsCombination/lesson.mdx +++ b/src/course/02-lessons/01-Bronze/PropsCombination/lesson.mdx @@ -8,46 +8,51 @@ Props are used to pass data from one component to another. The prop combination Some benefits of this pattern include reduction of boilerplate code, improving code readability and maintainability. -Let's take a look at an example: +Let's take a look at an example with a Pokemon trading card: ```jsx -const CardComponent = ({ - title, - subText, - ctaText, - ctaUrl, +const PokemonCard = ({ + pokemonName, + pokemonType, + pokemonHp, + pokemonLevel, + attackName, + attackDamage, + attackDescription, imageAltText, - imageUrlMobile, - imageUrlTablet, - imageUrlDesktop, - containerClassName, - titleClassName, - subTextClassName, - ctaClassName, - imageClassName -}) =>
{/* Lots of imaginary code here... */}
; + imageUrlSmall, + imageUrlMedium, + imageUrlLarge, + cardClassName, + nameClassName, + typeClassName, + statsClassName, + attackClassName +}) =>
{/* Lots of Pokemon card code here... */}
; ``` -Yes... this is pretty wild but it's very common to see this in the real world. What we have going into this component is: +Yes... this is pretty wild but it's very common to see this in the real world. What we have going into this Pokemon card component is: -- image - the image sources, imageAltText -- cta - text and ctaUrl -- content - title, sub title -- classNames - all classNames +- pokemon - name, type, hp, level +- attack - name, damage, description +- image - the image sources and alt text +- styling - all className props Now look at this when it is grouped: ```jsx -const CardComponent = ({ title, subText, cta, image }) => ( -
{/* Lots of imaginary code here... */}
+const PokemonCard = ({ pokemon, attack, image, styling }) => ( +
{/* Lots of Pokemon card code here... */}
); ``` -It is now a lot simpler to understand, we know the component has a title, it also needs a cta and an image. If we then wanted to, we would add some styles in case we need to tweak the card. +It is now a lot simpler to understand, we know the component needs pokemon data, an attack, an image, and styling options. If we then wanted to, we could easily add more pokemon stats or attacks. ## Exercise -In this exercise we are going to do the same thing that we did to the card snippet above and then we have some extras to add to it so it will get you thinking about how to change the props if necessary. +In this exercise we are going to do the same thing that we did to the Pokemon card snippet above and then we have some extras to add to it so it will get you thinking about how to change the props if necessary. + +You'll be refactoring a Pokemon trading card component that currently has too many individual props into a cleaner, grouped structure. Head over to the exercise file and let's begin. From 8fe3bdae2dab841729b1f4a14fe54d84ffb666c3 Mon Sep 17 00:00:00 2001 From: Matthew Claffey Date: Thu, 4 Sep 2025 11:20:41 +0100 Subject: [PATCH 11/29] feat: implement react hooks rebrand (#46) --- .../01-Bronze/Hooks/exercise/exercise.tsx | 241 ++++++++++++------ .../01-Bronze/Hooks/final/final.tsx | 218 +++++++++++----- .../02-lessons/01-Bronze/Hooks/lesson.mdx | 74 +++--- .../components/Button/Button.component.tsx | 1 + 4 files changed, 359 insertions(+), 175 deletions(-) diff --git a/src/course/02-lessons/01-Bronze/Hooks/exercise/exercise.tsx b/src/course/02-lessons/01-Bronze/Hooks/exercise/exercise.tsx index 7fc3b4a..dfa1922 100644 --- a/src/course/02-lessons/01-Bronze/Hooks/exercise/exercise.tsx +++ b/src/course/02-lessons/01-Bronze/Hooks/exercise/exercise.tsx @@ -1,111 +1,188 @@ -import { ChangeEvent, useState } from 'react'; -import { TextFieldProps, TextFieldComponent } from '../components'; +import { useState } from 'react'; +import { Button } from '@shared/components/Button/Button.component'; /* * Observations * ๐Ÿ’… The current implementation uses the Render Props Pattern - * Don't worry about the UI in the components file. + * We need to refactor this into a custom hook for Pokemon capture mechanics */ -interface IFieldProps { +interface IPokemonCaptureProps { + area: string; + children: (captureState: { + wildPokemon: Pokemon | null; + pokeballs: number; + capturing: boolean; + capturedPokemon: Pokemon[]; + attemptCapture: () => void; + encounterPokemon: () => void; + runAway: () => void; + restockPokeballs: (amount?: number) => void; + }) => React.ReactNode; +} + +interface Pokemon { + id: number; name: string; - validate?: (value: string) => boolean; - required?: boolean; - - // 1B ๐Ÿ’ฃ - remove these four params and references in the function. - id: string; - label: string; - errorMessage?: string; - children: (props: TextFieldProps) => React.ReactNode; + type: string; + captureRate: number; } -const validateTextString = (value: string) => - value.trim().length === 0; - -// 1A ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป - We need to refactor this to be called useField -export const Field = ({ - name, - required, - validate, - // 1B ๐Ÿ’ฃ - remove these four params and references in the function. - label, - id, - errorMessage, +const WILD_POKEMON = [ + { id: 1, name: 'Pidgey', type: 'Flying', captureRate: 0.8 }, + { id: 2, name: 'Rattata', type: 'Normal', captureRate: 0.9 }, + { id: 3, name: 'Pikachu', type: 'Electric', captureRate: 0.3 } +]; + +// 1A ๐Ÿ‘จ๐Ÿป๐Ÿ’ป - We need to refactor this to be called usePokemonCapture +export const PokemonCaptureSystem = ({ children -}: IFieldProps) => { - const [value, setValue] = useState(''); - const [hasError, setHasError] = useState(false); - const [isTouched, setIsTouched] = useState(false); - - const onChange = (event: ChangeEvent) => { - if (required && validate) { - setHasError(validate(event.target.value)); - } +}: IPokemonCaptureProps) => { + const [wildPokemon, setWildPokemon] = useState( + null + ); + const [pokeballs, setPokeballs] = useState(10); + const [capturing, setCapturing] = useState(false); + const [capturedPokemon, setCapturedPokemon] = useState( + [] + ); - setValue(event.target.value!); + const encounterPokemon = () => { + const randomPokemon = + WILD_POKEMON[Math.floor(Math.random() * WILD_POKEMON.length)]; + setWildPokemon(randomPokemon); }; - const onFocus = () => { - if (isTouched) { - setHasError(false); + const attemptCapture = async () => { + if (!wildPokemon || pokeballs <= 0) return; + + setCapturing(true); + setPokeballs((prev) => prev - 1); + + // Simulate capture attempt + await new Promise((resolve) => setTimeout(resolve, 1500)); + + const success = Math.random() < wildPokemon.captureRate; + if (success) { + setCapturedPokemon((prev) => [...prev, wildPokemon]); + setWildPokemon(null); } - setIsTouched(true); + setCapturing(false); }; - const onBlur = () => { - if (value && validate && validate(value)) { - setHasError(true); - } + const runAway = () => { + setWildPokemon(null); + }; + + const restockPokeballs = (amount: number = 5) => { + setPokeballs(prev => prev + amount); }; - // 1C ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป - Just return the object instead of children. + // 1C ๐Ÿ‘จ๐Ÿป๐Ÿ’ป - Just return the object instead of children return children({ - // 1D ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป - move name into input - name, - input: { - required, - onBlur, - onFocus, - onChange - }, - hasError, - // 1B ๐Ÿ’ฃ - remove these three params and references in the function. - label, - id, - errorMessage + wildPokemon, + pokeballs, + capturing, + capturedPokemon, + attemptCapture, + encounterPokemon, + runAway, + restockPokeballs }); }; -// 2A ๐Ÿค” - What if we wanted to make multiple Fields? Our current solution would -// require us to call useField multiple times in the same component. Let's refactor -// what we have done into a field component which uses IFieldProps as params. +// 2A ๐Ÿค” - What if we wanted to use this capture logic in multiple components? +// Let's make a component which uses the usePokemonCapture hook and takes an area prop export const Exercise = () => { - // 1E ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป - call the useField and pass the { name: "input", validate: validateTextString, required: true } + // 1E ๐Ÿ‘จ๐Ÿป๐Ÿ’ป - call the usePokemonCapture hook here return ( - - {/* 1F ๐Ÿ’ฃ - Remove the Field component and pull the values required for TextFieldComponent to run */} - - {({ name, label, id, errorMessage, hasError, input }) => ( - +
+

+ ๐ŸŒฟ Pokemon Capture System +

+ + {/* 1F ๐Ÿ’ฃ - Remove the PokemonCaptureSystem component and use the hook directly */} + + {({ + wildPokemon, + pokeballs, + capturing, + capturedPokemon, + attemptCapture, + encounterPokemon, + runAway, + restockPokeballs + }) => ( +
+
+
+

Pokeballs: {pokeballs} ๐Ÿ”ด

+

+ Captured: {capturedPokemon.length} Pokemon +

+
+ +
+ + {!wildPokemon ? ( +
+

No wild Pokemon in sight...

+ +
+ ) : ( +
+

+ A wild {wildPokemon.name} appeared! โšก +

+

+ Type: {wildPokemon.type} | Capture Rate:{' '} + {(wildPokemon.captureRate * 100).toFixed(0)}% +

+ + {capturing ? ( +
+

+ ๐Ÿ”ด Pokeball is shaking... +

+
+ ) : ( +
+ + +
+ )} +
+ )} + + {capturedPokemon.length > 0 && ( +
+

Captured Pokemon:

+
+ {capturedPokemon.map((pokemon, index) => ( + + {pokemon.name} + + ))} +
+
+ )} +
)} - - +
+
); }; diff --git a/src/course/02-lessons/01-Bronze/Hooks/final/final.tsx b/src/course/02-lessons/01-Bronze/Hooks/final/final.tsx index ec340b2..af92980 100644 --- a/src/course/02-lessons/01-Bronze/Hooks/final/final.tsx +++ b/src/course/02-lessons/01-Bronze/Hooks/final/final.tsx @@ -1,85 +1,185 @@ -import { ChangeEvent, useState } from 'react'; -import { TextFieldComponent } from '../components'; +/* eslint-disable react-refresh/only-export-components */ +import { useState } from 'react'; +import { Button } from '@shared/components/Button/Button.component'; -interface IFieldProps { +interface Pokemon { + id: number; name: string; - required?: boolean; - validate?: (value: string) => boolean; + type: string; + captureRate: number; } -const validateTextString = (value: string) => - value.trim().length === 0; - -export const useField = ({ - name, - required, - validate -}: IFieldProps) => { - const [value, setValue] = useState(''); - const [hasError, setHasError] = useState(false); - const [isTouched, setIsTouched] = useState(false); - - const onChange = (event: ChangeEvent) => { - if (required && validate) { - setHasError(validate(event.target.value)); - } +interface UsePokemonCaptureProps { + area: string; + initialPokeballs?: number; +} + +const WILD_POKEMON = [ + { id: 1, name: 'Pidgey', type: 'Flying', captureRate: 0.8 }, + { id: 2, name: 'Rattata', type: 'Normal', captureRate: 0.9 }, + { id: 3, name: 'Pikachu', type: 'Electric', captureRate: 0.3 }, + { id: 4, name: 'Caterpie', type: 'Bug', captureRate: 0.95 }, + { id: 5, name: 'Weedle', type: 'Bug/Poison', captureRate: 0.95 } +]; - setValue(event.target.value!); +export const usePokemonCapture = ({ + initialPokeballs = 10 +}: UsePokemonCaptureProps) => { + const [wildPokemon, setWildPokemon] = useState( + null + ); + const [pokeballs, setPokeballs] = useState(initialPokeballs); + const [capturing, setCapturing] = useState(false); + const [capturedPokemon, setCapturedPokemon] = useState( + [] + ); + + const encounterPokemon = () => { + const randomPokemon = + WILD_POKEMON[Math.floor(Math.random() * WILD_POKEMON.length)]; + setWildPokemon(randomPokemon); }; - const onFocus = () => { - if (isTouched) { - setHasError(false); + const attemptCapture = async () => { + if (!wildPokemon || pokeballs <= 0) return; + + setCapturing(true); + setPokeballs((prev) => prev - 1); + + // Simulate capture attempt with animation delay + await new Promise((resolve) => setTimeout(resolve, 1500)); + + const success = Math.random() < wildPokemon.captureRate; + if (success) { + setCapturedPokemon((prev) => [...prev, wildPokemon]); + setWildPokemon(null); } - setIsTouched(true); + setCapturing(false); }; - const onBlur = () => { - if (value && validate && validate(value)) { - setHasError(true); - } + const runAway = () => { + setWildPokemon(null); + }; + + const restockPokeballs = (amount: number = 5) => { + setPokeballs((prev) => prev + amount); }; return { - hasError, - input: { - name, - required, - onBlur, - onFocus, - onChange - } + wildPokemon, + pokeballs, + capturing, + capturedPokemon, + encounterPokemon, + attemptCapture, + runAway, + restockPokeballs }; }; -const Field = ({ required, validate, name }: IFieldProps) => { - const { hasError, input } = useField({ - name: 'input', - validate, - required - }); +const PokemonCaptureInterface = ({ area }: { area: string }) => { + const { + wildPokemon, + pokeballs, + capturing, + capturedPokemon, + encounterPokemon, + attemptCapture, + runAway, + restockPokeballs + } = usePokemonCapture({ area }); return ( - +
+

๐ŸŒฟ {area} Area

+ +
+
+

Pokeballs: {pokeballs} ๐Ÿ”ด

+

+ Captured: {capturedPokemon.length} Pokemon +

+
+ +
+ + {!wildPokemon ? ( +
+

No wild Pokemon in sight...

+ +
+ ) : ( +
+

+ A wild {wildPokemon.name} appeared! โšก +

+

+ Type: {wildPokemon.type} | Capture Rate:{' '} + {(wildPokemon.captureRate * 100).toFixed(0)}% +

+ + {capturing ? ( +
+

+ ๐Ÿ”ด Pokeball is shaking... +

+
+ ) : ( +
+ + +
+ )} +
+ )} + + {capturedPokemon.length > 0 && ( +
+

Captured Pokemon:

+
+ {capturedPokemon.map((pokemon, index) => ( + + {pokemon.name} ({pokemon.type}) + + ))} +
+
+ )} +
); }; export const Final = () => { return ( -
- - +
+

+ ๐ŸŽฏ Pokemon Capture System +

+

+ Using the usePokemonCapture() hook to manage capture mechanics + across different areas. +

+ +
+ + +
+
); }; diff --git a/src/course/02-lessons/01-Bronze/Hooks/lesson.mdx b/src/course/02-lessons/01-Bronze/Hooks/lesson.mdx index aba50c0..8a760f4 100644 --- a/src/course/02-lessons/01-Bronze/Hooks/lesson.mdx +++ b/src/course/02-lessons/01-Bronze/Hooks/lesson.mdx @@ -10,49 +10,51 @@ Additionally, Hooks enhance reusability and reduce prop drilling. Custom Hooks a ## Before Hooks -Before hooks we used to have to make class components which enforced a lot of prop drilling into components and a lot more complicated setup for state management. The Presentational & Container pattern was used a lot more back when we were using class components and that was purely for that seperation of concerns. +Before hooks we used to have to make class components which enforced a lot of prop drilling into components and a lot more complicated setup for state management. The Presentational & Container pattern was used a lot more back when we were using class components and that was purely for that separation of concerns. ```jsx import React, { Component } from 'react'; -class Profile extends Component { +class PokemonCapture extends Component { constructor(props) { super(props); this.state = { - loading: false, - user: {} + capturing: false, + wildPokemon: null, + pokeballs: 10, + capturedPokemon: [] }; } componentDidMount() { - this.updateProfile(this.props.id); + this.encounterWildPokemon(this.props.area); } componentDidUpdate(prevProps) { - if (prevProps.id !== this.props.id) { - this.updateProfile(this.props.id); + if (prevProps.area !== this.props.area) { + this.encounterWildPokemon(this.props.area); } } componentWillUnmount() { - // do some unmounting actions + // cleanup capture animations } - fetchUser(id) { - // fetch users logic here + encounterWildPokemon(area) { + // find random pokemon in area } - async updateProfile(id) { - this.setState({ loading: true }); - // fetch users data - await this.fetchUser(id); - this.setState({ loading: false }); + async attemptCapture(pokemon) { + this.setState({ capturing: true }); + // capture logic with success rate + await this.throwPokeball(pokemon); + this.setState({ capturing: false }); } render() { - // ... some jsx + // ... pokemon capture jsx } } -export default Profile; +export default PokemonCapture; ``` ## With Hooks @@ -60,36 +62,40 @@ export default Profile; With Hooks, functional components can handle both the presentational aspects and the business logic. This means you no longer need to separate your components strictly into presentational and container types. ```jsx -import React from 'react'; +import React, { useState, useEffect } from 'react'; -const Profile = ({ id }) => { - const [isLoading, setIsLoading] = useState(false); - const [user, setUser] = useState({}); +const PokemonCapture = ({ area }) => { + const [capturing, setCapturing] = useState(false); + const [wildPokemon, setWildPokemon] = useState(null); + const [pokeballs, setPokeballs] = useState(10); + const [capturedPokemon, setCapturedPokemon] = useState([]); useEffect(() => { - updateProfile(id); - }, [id]); + encounterWildPokemon(area); + }, [area]); - const fetchUser = (id) => { - // fetch users logic here + const encounterWildPokemon = (area) => { + // find random pokemon in area }; - const updateProfile = async (id) => { - setIsLoading(true); - // fetch users data - await fetchUser(id); - setIsLoading(false); + const attemptCapture = async (pokemon) => { + setCapturing(true); + // capture logic with success rate + await throwPokeball(pokemon); + setCapturing(false); }; - return { - // Some jsx code - }; + return ( + // Pokemon capture interface jsx + ); }; ``` ## Exercise -In this lesson we are going to refactor the Field component that we made into a react hook instead of following the render props pattern. +In this lesson we are going to refactor the Pokemon capture component into a custom React hook called **usePokemonCapture()** instead of following the render props pattern. + +You'll create a hook that manages capture attempts, pokeball inventory, success rates, and wild Pokemon encounters. Head over to the exercise and let's get started. diff --git a/src/shared/components/Button/Button.component.tsx b/src/shared/components/Button/Button.component.tsx index 04f4b45..4b84895 100644 --- a/src/shared/components/Button/Button.component.tsx +++ b/src/shared/components/Button/Button.component.tsx @@ -4,6 +4,7 @@ import { HTMLAttributes } from 'react'; interface IButton extends HTMLAttributes { className?: string; children: React.ReactNode | React.ReactNode[]; + disabled?: boolean; } const buttonClasses = From 399167663cdad98741d3ea3dc2fcb4d41b60549e Mon Sep 17 00:00:00 2001 From: code-mattclaffey Date: Thu, 4 Sep 2025 11:55:47 +0100 Subject: [PATCH 12/29] feat: implement slots rebranded --- .../01-Bronze/Slots/exercise/exercise.tsx | 180 +++++++++++++++--- .../01-Bronze/Slots/final/final.tsx | 169 ++++++++++++---- .../02-lessons/01-Bronze/Slots/lesson.mdx | 34 +++- 3 files changed, 306 insertions(+), 77 deletions(-) diff --git a/src/course/02-lessons/01-Bronze/Slots/exercise/exercise.tsx b/src/course/02-lessons/01-Bronze/Slots/exercise/exercise.tsx index 01dd4d3..7e7a5e9 100644 --- a/src/course/02-lessons/01-Bronze/Slots/exercise/exercise.tsx +++ b/src/course/02-lessons/01-Bronze/Slots/exercise/exercise.tsx @@ -1,38 +1,166 @@ import classNames from 'classnames'; -import { HTMLAttributes } from 'react'; -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 1A - Add two new types of "iconLeft" & "iconRight" -interface IButton extends HTMLAttributes { +// ๐Ÿ‘จ๐Ÿป๐Ÿ’ป 1A - This component uses individual props for each map location. Can we refactor it to use slots instead? +interface IPokemonMap { className?: string; - children: React.ReactNode | React.ReactNode[]; + + // North area props + showNorthArea?: boolean; + northAreaName?: string; + northAreaIcon?: string; + northAreaColor?: string; + + // South area props + showSouthArea?: boolean; + southAreaName?: string; + southAreaIcon?: string; + southAreaColor?: string; + + // East area props + showEastArea?: boolean; + eastAreaName?: string; + eastAreaIcon?: string; + eastAreaColor?: string; + + // West area props + showWestArea?: boolean; + westAreaName?: string; + westAreaIcon?: string; + westAreaColor?: string; + + // Center area props + showCenterArea?: boolean; + centerAreaName?: string; + centerAreaIcon?: string; + centerAreaColor?: string; } -const buttonClasses = - 'middle none center rounded-lg bg-blue-500 py-3 px-6 font-sans text-xs font-bold uppercase text-white shadow-md shadow-blue-500/20 transition-all hover:shadow-lg hover:shadow-blue-500/40 focus:opacity-[0.85] focus:shadow-none active:opacity-[0.85] active:shadow-none disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none inline-flex items-center justify-center'; +const mapContainerClasses = 'grid grid-cols-3 grid-rows-3 gap-2 w-80 h-80 p-4 bg-green-100 rounded-lg border-2 border-green-300'; +const areaClasses = 'flex flex-col items-center justify-center p-3 rounded-lg border-2 text-sm font-bold text-white shadow-md'; -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 1B - Extract those types out from the props and then add iconLeft above children and iconRight below -// ๐Ÿ’„ 1C - styling - icon && {icon} -export const Button = ({ className, children, ...rest }: IButton) => { +// ๐Ÿ‘จ๐Ÿป๐Ÿ’ป 1B - Look at all these props and conditional logic! This is hard to maintain. +// ๐Ÿ‘จ๐Ÿป๐Ÿ’ป 1C - Refactor this to use northSlot, southSlot, eastSlot, westSlot, centerSlot instead +export const PokemonMap = ({ + className, + showNorthArea, + northAreaName, + northAreaIcon, + northAreaColor, + showSouthArea, + southAreaName, + southAreaIcon, + southAreaColor, + showEastArea, + eastAreaName, + eastAreaIcon, + eastAreaColor, + showWestArea, + westAreaName, + westAreaIcon, + westAreaColor, + showCenterArea, + centerAreaName, + centerAreaIcon, + centerAreaColor +}: IPokemonMap) => { return ( - +
+ {/* Empty top-left */} +
+ + {/* North area */} +
+ {showNorthArea && ( + <> + {northAreaIcon} + {northAreaName} + + )} +
+ + {/* Empty top-right */} +
+ + {/* West area */} +
+ {showWestArea && ( + <> + {westAreaIcon} + {westAreaName} + + )} +
+ + {/* Center area */} +
+ {showCenterArea && ( + <> + {centerAreaIcon} + {centerAreaName} + + )} +
+ + {/* East area */} +
+ {showEastArea && ( + <> + {eastAreaIcon} + {eastAreaName} + + )} +
+ + {/* Empty bottom-left */} +
+ + {/* South area */} +
+ {showSouthArea && ( + <> + {southAreaIcon} + {southAreaName} + + )} +
+ + {/* Empty bottom-right */} +
+
); }; -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 1D - Add iconLeft={IconOne} from the icons folder to the first button -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 1E - Add iconRight={IconTwo} from the icons folder to the second button -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 1F - Add iconRight and iconLeft to the third button. -// Check storybook, you should see some black icons.... Why? -// ๐Ÿ’… 2A - head over to ./icons/index.tsx +// ๐Ÿ‘จ๐Ÿป๐Ÿ’ป 1D - Look at how verbose these prop combinations are! +// ๐Ÿ‘จ๐Ÿป๐Ÿ’ป 1E - Refactor these to use slots: northSlot={}, centerSlot={}, etc. export const Exercise = () => ( -
- - - +
+

๐Ÿ—บ๏ธ Pokemon World Map

+ +
-); +); \ No newline at end of file diff --git a/src/course/02-lessons/01-Bronze/Slots/final/final.tsx b/src/course/02-lessons/01-Bronze/Slots/final/final.tsx index 35633b6..946585e 100644 --- a/src/course/02-lessons/01-Bronze/Slots/final/final.tsx +++ b/src/course/02-lessons/01-Bronze/Slots/final/final.tsx @@ -1,55 +1,140 @@ import classNames from 'classnames'; -import { HTMLAttributes } from 'react'; -import { IconOne, IconTwo } from '../icons'; -interface ButtonProps extends HTMLAttributes { +interface PokemonMapProps { className?: string; - iconLeft?: React.ReactNode; - iconRight?: React.ReactNode; - children: React.ReactNode | React.ReactNode[]; + northSlot?: React.ReactNode; + southSlot?: React.ReactNode; + eastSlot?: React.ReactNode; + westSlot?: React.ReactNode; + centerSlot?: React.ReactNode; } -const buttonClasses = [ - 'middle none center rounded-lg bg-blue-500 py-3 px-6', - 'font-sans text-xs font-bold uppercase text-white', - 'shadow-md shadow-blue-500/20 transition-all', - 'hover:shadow-lg hover:shadow-blue-500/40', - 'focus:opacity-[0.85] focus:shadow-none', - 'active:opacity-[0.85] active:shadow-none', - 'disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none', - 'inline-flex items-center justify-center' -].join(' '); +const mapContainerClasses = 'grid grid-cols-3 grid-rows-3 gap-2 w-80 h-80 p-4 bg-green-100 rounded-lg border-2 border-green-300'; -export const Button = ({ +export const PokemonMap = ({ className, - children, - iconLeft, - iconRight, - ...rest -}: ButtonProps) => { + northSlot, + southSlot, + eastSlot, + westSlot, + centerSlot +}: PokemonMapProps) => { return ( - +
+ {/* Empty top-left */} +
+ + {/* North slot */} +
+ {northSlot} +
+ + {/* Empty top-right */} +
+ + {/* West slot */} +
+ {westSlot} +
+ + {/* Center slot */} +
+ {centerSlot} +
+ + {/* East slot */} +
+ {eastSlot} +
+ + {/* Empty bottom-left */} +
+ + {/* South slot */} +
+ {southSlot} +
+ + {/* Empty bottom-right */} +
+
); }; -export const Final = () => ( -
- - - +const LocationCard = ({ name, icon, bgColor }: { name: string; icon: string; bgColor: string }) => ( +
+ {icon} + {name}
); + +export const Final = () => ( +
+

๐Ÿ—บ๏ธ Pokemon World Map

+ + + } + southSlot={ + + } + eastSlot={ + + } + westSlot={ + + } + centerSlot={ + + } + /> + +
+

Alternative Layout

+ + } + northSlot={ + + } + eastSlot={ + + } + /> +
+
+); \ No newline at end of file diff --git a/src/course/02-lessons/01-Bronze/Slots/lesson.mdx b/src/course/02-lessons/01-Bronze/Slots/lesson.mdx index 79b8743..13517c4 100644 --- a/src/course/02-lessons/01-Bronze/Slots/lesson.mdx +++ b/src/course/02-lessons/01-Bronze/Slots/lesson.mdx @@ -9,13 +9,13 @@ The slots pattern is very similar to the [render props pattern](?path=/docs/less Here is an example of how you do a render prop pattern: ```jsx -const HelloWorld = ({ render }) => render({ hello: 'world' }); +const MapArea = ({ render }) => render({ area: 'forest', pokemonCount: 3 }); // How you would use it. -

{hello}

} />; +

{area}: {pokemonCount} Pokemon

} />; ``` -The key thing here is that we are passing some state from our react component which can be used with components outside. What if we didn't need the the hello prop? We could still use this pattern but an alternative would be a slot. +The key thing here is that we are passing some state from our react component which can be used with components outside. What if we didn't need the area data? We could still use this pattern but an alternative would be a slot. ## Children is a slot & a render prop @@ -23,24 +23,40 @@ Now the easiest way to form a connection with the wording of "slot" is to think ``` {children} // slot -{children({ hello: 'world' })} // render prop +{children({ area: 'forest', pokemonCount: 3 })} // render prop ``` ## Using slots as props -Here is an example of us using the HelloWorld example above. +Here is an example of us using a Pokemon map layout with slots. ```jsx -const HelloWorld = ({ slot }) =>

Hello {slot}

; +const PokemonMap = ({ northSlot, southSlot, eastSlot, westSlot, centerSlot }) => ( +
+
{northSlot}
+
{westSlot}
+
{centerSlot}
+
{eastSlot}
+
{southSlot}
+
+); // How you would use it. -World} />; +๐ŸŒฒ Forest
} + centerSlot={
๐Ÿ  Pallet Town
} + southSlot={
๐ŸŒŠ Route 1
+ eastSlot={
โšก Power Plant
} + westSlot={
๐Ÿ”๏ธ Mt. Silver
} +/>; ``` The pros to using this approach are: -- Useful for layout components where you make the responsibility of the layout purely column based and port the header/footer/nav using slots. +- Useful for layout components where you make the responsibility of the layout purely slot-based and port different areas/locations using slots. - Removing many if statements and prop drilling from your presentational component +- Flexible map layouts for different regions (Kanto, Johto, etc.) +- Easy to swap out different locations without changing the map structure The cons of using this approach: @@ -48,7 +64,7 @@ The cons of using this approach: ## Exercise -So in the exercise we have a task to extend the functionality of our Button component. The design team want to be able to put an icon on the left side of the button and/or on the right side. Our task is to implement this feature in a way so that any icon can be put inside this component. +So in the exercise we have a task to create a Pokemon world map layout component. The Pokemon world needs to display different locations in specific positions: towns in the center, routes connecting them, and special areas like forests and mountains. Our task is to implement this using slots so that any location can be placed in any position on the map. ## Feedback From 4457f6b3d8d57b89f88c93e7b74461003c4e0776 Mon Sep 17 00:00:00 2001 From: code-mattclaffey Date: Thu, 4 Sep 2025 13:26:31 +0100 Subject: [PATCH 13/29] feat: implement the compound pattern in pokemon style --- .../exercise/exercise.tsx | 14 +- .../01-Bronze/Hooks/exercise/exercise.tsx | 13 +- .../PropsCombination/exercise/exercise.tsx | 6 +- .../01-Bronze/Slots/exercise/exercise.tsx | 111 ++++++++---- .../exercise/components/Accordion.tsx | 113 ------------ .../exercise/components/Accoridon.module.css | 38 ---- .../exercise/components/ChevronDown.tsx | 21 --- .../components/PokemonTeamBuilder.tsx | 94 ++++++++++ .../02-Silver/Compound/exercise/exercise.tsx | 164 ++++++++---------- .../Compound/final/components/Accordion.tsx | 103 ----------- .../final/components/Accoridon.module.css | 38 ---- .../Compound/final/components/ChevronDown.tsx | 21 --- .../final/components/PokemonTeamBuilder.tsx | 109 ++++++++++++ .../02-Silver/Compound/final/final.tsx | 102 ++++------- .../02-lessons/02-Silver/Compound/lesson.mdx | 25 +-- 15 files changed, 412 insertions(+), 560 deletions(-) delete mode 100644 src/course/02-lessons/02-Silver/Compound/exercise/components/Accordion.tsx delete mode 100644 src/course/02-lessons/02-Silver/Compound/exercise/components/Accoridon.module.css delete mode 100644 src/course/02-lessons/02-Silver/Compound/exercise/components/ChevronDown.tsx create mode 100644 src/course/02-lessons/02-Silver/Compound/exercise/components/PokemonTeamBuilder.tsx delete mode 100644 src/course/02-lessons/02-Silver/Compound/final/components/Accordion.tsx delete mode 100644 src/course/02-lessons/02-Silver/Compound/final/components/Accoridon.module.css delete mode 100644 src/course/02-lessons/02-Silver/Compound/final/components/ChevronDown.tsx create mode 100644 src/course/02-lessons/02-Silver/Compound/final/components/PokemonTeamBuilder.tsx diff --git a/src/course/02-lessons/01-Bronze/ConditionalRendering/exercise/exercise.tsx b/src/course/02-lessons/01-Bronze/ConditionalRendering/exercise/exercise.tsx index 1a486e2..d1abf80 100644 --- a/src/course/02-lessons/01-Bronze/ConditionalRendering/exercise/exercise.tsx +++ b/src/course/02-lessons/01-Bronze/ConditionalRendering/exercise/exercise.tsx @@ -9,15 +9,15 @@ interface ITrainerProps { // @ts-expect-error // eslint-disable-next-line @typescript-eslint/no-unused-vars export const PokemonTrainerStatus = (props: ITrainerProps) => { - // 1a - ๐Ÿ‘จ๐Ÿป๐Ÿ’ป add a useState that has false as default. Name the variable [hasGymBadges, setHasGymBadges] + // 1a - ๐Ÿ’ป add a useState that has false as default. Name the variable [hasGymBadges, setHasGymBadges] - // 1b - ๐Ÿ‘จ๐Ÿป๐Ÿ’ป create me a onEarnBadge function which setHasGymBadges to be true + // 1b - ๐Ÿ’ป create me a onEarnBadge function which setHasGymBadges to be true - // 1c - ๐Ÿ‘จ๐Ÿป๐Ÿ’ป create me a onLoseBadges function which setHasGymBadges to be false + // 1c - ๐Ÿ’ป create me a onLoseBadges function which setHasGymBadges to be false - // 1d - ๐Ÿ‘จ๐Ÿป๐Ÿ’ป if hasGymBadges, return a button called "Reset Journey" with the onClick of onLoseBadges - // 1e - ๐Ÿ‘จ๐Ÿป๐Ÿ’ป if hasGymBadges, return some text called "Welcome Gym Leader {props.trainerName}! ๐Ÿ†" + // 1d - ๐Ÿ’ป if hasGymBadges, return a button called "Reset Journey" with the onClick of onLoseBadges + // 1e - ๐Ÿ’ป if hasGymBadges, return some text called "Welcome Gym Leader {props.trainerName}! ๐Ÿ†" - // 1f - ๐Ÿ‘จ๐Ÿป๐Ÿ’ป add onClick function onEarnBadge to the button + // 1f - ๐Ÿ’ป add onClick function onEarnBadge to the button return ; -}; \ No newline at end of file +}; diff --git a/src/course/02-lessons/01-Bronze/Hooks/exercise/exercise.tsx b/src/course/02-lessons/01-Bronze/Hooks/exercise/exercise.tsx index dfa1922..65db7cc 100644 --- a/src/course/02-lessons/01-Bronze/Hooks/exercise/exercise.tsx +++ b/src/course/02-lessons/01-Bronze/Hooks/exercise/exercise.tsx @@ -34,7 +34,7 @@ const WILD_POKEMON = [ { id: 3, name: 'Pikachu', type: 'Electric', captureRate: 0.3 } ]; -// 1A ๐Ÿ‘จ๐Ÿป๐Ÿ’ป - We need to refactor this to be called usePokemonCapture +// 1A ๐Ÿ’ป - We need to refactor this to be called usePokemonCapture export const PokemonCaptureSystem = ({ children }: IPokemonCaptureProps) => { @@ -76,10 +76,10 @@ export const PokemonCaptureSystem = ({ }; const restockPokeballs = (amount: number = 5) => { - setPokeballs(prev => prev + amount); + setPokeballs((prev) => prev + amount); }; - // 1C ๐Ÿ‘จ๐Ÿป๐Ÿ’ป - Just return the object instead of children + // 1C ๐Ÿ’ป - Just return the object instead of children return children({ wildPokemon, pokeballs, @@ -96,7 +96,7 @@ export const PokemonCaptureSystem = ({ // Let's make a component which uses the usePokemonCapture hook and takes an area prop export const Exercise = () => { - // 1E ๐Ÿ‘จ๐Ÿป๐Ÿ’ป - call the usePokemonCapture hook here + // 1E ๐Ÿ’ป - call the usePokemonCapture hook here return (

@@ -123,7 +123,10 @@ export const Exercise = () => { Captured: {capturedPokemon.length} Pokemon

-
diff --git a/src/course/02-lessons/01-Bronze/PropsCombination/exercise/exercise.tsx b/src/course/02-lessons/01-Bronze/PropsCombination/exercise/exercise.tsx index c51e03a..e1dd376 100644 --- a/src/course/02-lessons/01-Bronze/PropsCombination/exercise/exercise.tsx +++ b/src/course/02-lessons/01-Bronze/PropsCombination/exercise/exercise.tsx @@ -2,7 +2,7 @@ import classnames from 'classnames'; /* - 1a๐Ÿ‘จ๐Ÿป๐Ÿ’ป group the following together: + 1a๐Ÿ’ป group the following together: * pokemon - pokemonName, pokemonType, pokemonHp, pokemonLevel * attack - attackName, attackDamage, attackDescription @@ -31,7 +31,7 @@ interface IPokemonCardProps { } /* - 1b๐Ÿ‘จ๐Ÿป๐Ÿ’ป Update the props to match the new grouped types defined above. + 1b๐Ÿ’ป Update the props to match the new grouped types defined above. */ export const Exercise = ({ pokemonName, @@ -56,7 +56,7 @@ export const Exercise = ({ 2a ๐Ÿค” Could we destructure the image to be [small, medium, large]? */ /* - 1c๐Ÿ‘จ๐Ÿป๐Ÿ’ป Update the props in the jsx to use the grouped structure + 1c๐Ÿ’ป Update the props in the jsx to use the grouped structure */ return (
{/* Empty top-left */}
- + {/* North area */} -
+
{showNorthArea && ( <> {northAreaIcon} - {northAreaName} + + {northAreaName} + )}
- + {/* Empty top-right */}
- + {/* West area */} -
+
{showWestArea && ( <> {westAreaIcon} - {westAreaName} + + {westAreaName} + )}
- + {/* Center area */} -
+
{showCenterArea && ( <> {centerAreaIcon} - {centerAreaName} + + {centerAreaName} + )}
- + {/* East area */} -
+
{showEastArea && ( <> {eastAreaIcon} - {eastAreaName} + + {eastAreaName} + )}
- + {/* Empty bottom-left */}
- + {/* South area */} -
+
{showSouthArea && ( <> {southAreaIcon} - {southAreaName} + + {southAreaName} + )}
- + {/* Empty bottom-right */}
); }; -// ๐Ÿ‘จ๐Ÿป๐Ÿ’ป 1D - Look at how verbose these prop combinations are! -// ๐Ÿ‘จ๐Ÿป๐Ÿ’ป 1E - Refactor these to use slots: northSlot={}, centerSlot={}, etc. +// ๐Ÿ’ป 1D - Look at how verbose these prop combinations are! +// ๐Ÿ’ป 1E - Refactor these to use slots: northSlot={}, centerSlot={}, etc. export const Exercise = () => (
-

๐Ÿ—บ๏ธ Pokemon World Map

- +

+ ๐Ÿ—บ๏ธ Pokemon World Map +

+
-); \ No newline at end of file +); diff --git a/src/course/02-lessons/02-Silver/Compound/exercise/components/Accordion.tsx b/src/course/02-lessons/02-Silver/Compound/exercise/components/Accordion.tsx deleted file mode 100644 index 3fe5c27..0000000 --- a/src/course/02-lessons/02-Silver/Compound/exercise/components/Accordion.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import classNames from 'classnames'; -import styles from './Accoridon.module.css'; -import { ChevronDown } from './ChevronDown'; - -interface IAccordion { - id: string; - children: - | React.ReactElement - | React.ReactElement[]; - title: string; -} - -interface IAccordionItem { - id: string; - children: React.ReactNode | React.ReactNode[]; - title: string; - isSelected?: boolean; - onClick?: VoidFunction; - onFocus?: VoidFunction; -} - -export const Accordion = ({ id, children, title }: IAccordion) => { - // ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 1B - Paste that useState here - - // ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 1C - Replacing {children} - // We need to map the children and apply the props to the AccordionItem here so we can manage the state within the accordion. It looks like this the syntax: - // Children.map(children, (child: React.ReactElement, index) => cloneElement(child, { PROPS (look at the current props) })) - - // ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 1D - Notice how there was an accordion-one in the id of the props on AccordionItem in exercise.tsx? - // We need to use the index from the children map function as an identifier. - /* - isSelected: selectedAccordion === index, - id: `${id}_${child.props.id}_${index}`, - onClick: () => setSelectedAccordion(index), - onFocus: () => setSelectedAccordion(index) - */ - - // Once this is completed return to the exercise.tsx file. - - // ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 3B Now where you have the onClick which just does this - onClick: () => setSelectedAccordion(index) atm - // Make it do this instead - /* - onClick: () => { - if (child.props.onClick) { - child.props.onClick(); - } - - setSelectedAccordion(index) - } - */ - // What is happening here now is that we are checking if the AccordionItem already has a onClick prop and firing that if it does exist as well as managing the local state of the accordion. - return ( -
-

{title}

- {children} -
- ); -}; - -export const AccordionItem = ({ - id, - title, - children, - isSelected, - onClick, - onFocus -}: IAccordionItem) => ( -
-

- -

-
- {children} -
-
-); diff --git a/src/course/02-lessons/02-Silver/Compound/exercise/components/Accoridon.module.css b/src/course/02-lessons/02-Silver/Compound/exercise/components/Accoridon.module.css deleted file mode 100644 index 53889b7..0000000 --- a/src/course/02-lessons/02-Silver/Compound/exercise/components/Accoridon.module.css +++ /dev/null @@ -1,38 +0,0 @@ -.accordionPanel { - @apply py-0; - @apply px-4; - @apply h-0; - @apply transition-all; - @apply duration-300; -} - -.accordionItem:focus-within .accordionPanel, -.accordionPanelSelected { - @apply p-4; - @apply h-auto; - @apply max-h-[1000px]; - @apply transition-all; - @apply duration-300; -} - -.accordionButton { - @apply bg-blue-100; - @apply hover:bg-blue-200; - @apply text-blue-950; -} - -.accordionItem:focus-within .accordionButton, -.accordionButtonSelected { - @apply bg-blue-950; - @apply hover:bg-blue-950; - @apply text-white; -} - -.accordionIcon { - @apply rotate-[-90deg]; -} - -.accordionItem:focus-within .accordionIcon, -.accordionButtonSelected .accordionIcon { - @apply rotate-0; -} diff --git a/src/course/02-lessons/02-Silver/Compound/exercise/components/ChevronDown.tsx b/src/course/02-lessons/02-Silver/Compound/exercise/components/ChevronDown.tsx deleted file mode 100644 index c3f2c35..0000000 --- a/src/course/02-lessons/02-Silver/Compound/exercise/components/ChevronDown.tsx +++ /dev/null @@ -1,21 +0,0 @@ -export const ChevronDown = () => ( - - - - - - -); diff --git a/src/course/02-lessons/02-Silver/Compound/exercise/components/PokemonTeamBuilder.tsx b/src/course/02-lessons/02-Silver/Compound/exercise/components/PokemonTeamBuilder.tsx new file mode 100644 index 0000000..18e33c4 --- /dev/null +++ b/src/course/02-lessons/02-Silver/Compound/exercise/components/PokemonTeamBuilder.tsx @@ -0,0 +1,94 @@ +import classNames from 'classnames'; + +interface IPokemonTeamBuilder { + title: string; + children: React.ReactNode; +} + +interface ITeamSlot { + position: number; + pokemonName?: string; + pokemonLevel?: number; + pokemonType?: string; + isSelected: boolean; + slotId: string; + onClick: () => void; + onSelect: () => void; +} + +// 1B Move the useState from exercise.tsx here and manage the state internally +// 1C Create a React Context to share state between PokemonTeamBuilder and TeamSlot +// 1D Remove isSelected, slotId, onClick, onSelect from ITeamSlot interface +export const PokemonTeamBuilder = ({ + title, + children +}: IPokemonTeamBuilder) => { + return ( +
+

+ {title} +

+
+ {children} +
+
+ ); +}; + +// 1E Update this component to use useContext to get state instead of direct props +export const TeamSlot = ({ + position, + pokemonName, + pokemonLevel, + pokemonType, + isSelected, + onClick, + onSelect +}: ITeamSlot) => { + return ( +
+
+
+ Slot {position} +
+ + {pokemonName ? ( +
+
+ {pokemonName} +
+
+ Level {pokemonLevel} +
+
+ {pokemonType} +
+
+ ) : ( +
+
โž•
+
Empty Slot
+
+ )} +
+
+ ); +}; + +// ๐Ÿ’ป 1F export const PokemonTeam which will be an object containing the PokemonTeamBuilder and TeamSlot components +// Tip: export const ComponentName = Object.assign(ParentComponent, { MyComponent: Component }); +export const PokemonTeam = Object.assign(PokemonTeamBuilder, { + Slot: TeamSlot +}); diff --git a/src/course/02-lessons/02-Silver/Compound/exercise/exercise.tsx b/src/course/02-lessons/02-Silver/Compound/exercise/exercise.tsx index dfd7355..3326f4e 100644 --- a/src/course/02-lessons/02-Silver/Compound/exercise/exercise.tsx +++ b/src/course/02-lessons/02-Silver/Compound/exercise/exercise.tsx @@ -1,108 +1,84 @@ import { useState } from 'react'; -import { Accordion, AccordionItem } from './components/Accordion'; +import { + PokemonTeamBuilder, + TeamSlot +} from './components/PokemonTeamBuilder'; /** - * Exercise: Convert the current accordion implementation to use the compound pattern + * Exercise: Convert the current Pokemon team builder implementation to use the compound pattern * * ๐Ÿค” Observations of this file - * As you can see in this component we have some useState which is managing which accordion item is open at any given time. We need to move this logic into the Accordion component and pass down the props into the AccordionItem that way instead of managing it here in this file. + * As you can see in this component we have some useState which is managing which team slot is selected at any given time. We need to move this logic into the PokemonTeamBuilder component and pass down the props into the TeamSlot that way instead of managing it here in this file. * */ -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 1A Copy the useState on line 14 and go to ./components/Accordion.tsx +// ๐Ÿ’ป 1A Copy the useState on line 16 and go to ./components/PokemonTeamBuilder.tsx export const Exercise = () => { - // ๐Ÿ’ฃ 2A Remove the useState and the isSelected, id, onClick, onFocus props from all the AccordionItems + // ๐Ÿ’ฃ 2A Remove the useState and the isSelected, slotId, onClick, onSelect props from all the TeamSlots - const [selectedAccordion, setSelectedAccordion] = - useState(); + const [selectedSlot, setSelectedSlot] = useState(); + + // ๐Ÿ’ป 2B Import PokemonTeam from ./components/PokemonTeamBuilder.tsx + // Change PokemonTeamBuilder to PokemonTeam and change TeamSlot to PokemonTeam.Slot - // ๐Ÿค” 3A (Bonus round) - now the customer wants to add event tracking when you click ONLY the first accordion item. Since the props now live in the accordion for onClick, we need to persist that onClick to the accordionItem if we specify one at this level. Add an onClick on the first AccordionItem with a console.log('TRACK') and then move over to the Accordion.tsx. return ( - - setSelectedAccordion('accordion-one')} - onFocus={() => setSelectedAccordion('accordion-one')} - > -

- Per torquent, mus cursus hendrerit id aenean justo auctor - donec. Turpis magna et, egestas dignissim nascetur. Sapien - augue nisl varius diam aliquet. Litora velit, tortor at - ante. Eros lacus faucibus consequat scelerisque proin - volutpat. In pellentesque est curae; dapibus nisl risus - sociosqu penatibus. Lobortis pulvinar scelerisque lacus. - Elit vel eros facilisi dis mauris magna posuere? Cum class - viverra bibendum rutrum odio scelerisque scelerisque libero, - nisl est convallis non. Ac convallis odio suspendisse velit - mollis libero. Morbi enim blandit venenatis{' '} - lorem! -

-
- setSelectedAccordion('accordion-two')} - onFocus={() => setSelectedAccordion('accordion-two')} - > -

- Per torquent, mus cursus hendrerit id aenean justo auctor - donec. Turpis magna et, egestas dignissim nascetur. Sapien - augue nisl varius diam aliquet. Litora velit, tortor at - ante. Eros lacus faucibus consequat scelerisque proin - volutpat. In pellentesque est curae; dapibus nisl risus - sociosqu penatibus. Lobortis pulvinar scelerisque lacus. - Elit vel eros facilisi dis mauris magna posuere? Cum class - viverra bibendum rutrum odio scelerisque scelerisque libero, - nisl est convallis non. Ac convallis odio suspendisse velit - mollis libero. Morbi enim blandit venenatis{' '} - lorem! -

-
- setSelectedAccordion('accordion-three')} - onFocus={() => setSelectedAccordion('accordion-three')} - > -

- Per torquent, mus cursus hendrerit id aenean justo auctor - donec. Turpis magna et, egestas dignissim nascetur. Sapien - augue nisl varius diam aliquet. Litora velit, tortor at - ante. Eros lacus faucibus consequat scelerisque proin - volutpat. In pellentesque est curae; dapibus nisl risus - sociosqu penatibus. Lobortis pulvinar scelerisque lacus. - Elit vel eros facilisi dis mauris magna posuere? Cum class - viverra bibendum rutrum odio scelerisque scelerisque libero, - nisl est convallis non. Ac convallis odio suspendisse velit - mollis libero. Morbi enim blandit venenatis{' '} - lorem! -

-
- setSelectedAccordion('accordion-four')} - onFocus={() => setSelectedAccordion('accordion-four')} - > -

- Per torquent, mus cursus hendrerit id aenean justo auctor - donec. Turpis magna et, egestas dignissim nascetur. Sapien - augue nisl varius diam aliquet. Litora velit, tortor at - ante. Eros lacus faucibus consequat scelerisque proin - volutpat. In pellentesque est curae; dapibus nisl risus - sociosqu penatibus. Lobortis pulvinar scelerisque lacus. - Elit vel eros facilisi dis mauris magna posuere? Cum class - viverra bibendum rutrum odio scelerisque scelerisque libero, - nisl est convallis non. Ac convallis odio suspendisse velit - mollis libero. Morbi enim blandit venenatis{' '} - lorem! -

-
-
+
+ + setSelectedSlot('slot-1')} + onSelect={() => setSelectedSlot('slot-1')} + /> + setSelectedSlot('slot-2')} + onSelect={() => setSelectedSlot('slot-2')} + /> + setSelectedSlot('slot-3')} + onSelect={() => setSelectedSlot('slot-3')} + /> + setSelectedSlot('slot-4')} + onSelect={() => setSelectedSlot('slot-4')} + /> + setSelectedSlot('slot-5')} + onSelect={() => setSelectedSlot('slot-5')} + /> + setSelectedSlot('slot-6')} + onSelect={() => setSelectedSlot('slot-6')} + /> + +
); }; diff --git a/src/course/02-lessons/02-Silver/Compound/final/components/Accordion.tsx b/src/course/02-lessons/02-Silver/Compound/final/components/Accordion.tsx deleted file mode 100644 index f80a88f..0000000 --- a/src/course/02-lessons/02-Silver/Compound/final/components/Accordion.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import classNames from 'classnames'; -import { Children, cloneElement, useState } from 'react'; -import styles from './Accoridon.module.css'; -import { ChevronDown } from './ChevronDown'; - -interface IAccordion { - id: string; - children: - | React.ReactElement - | React.ReactElement[]; - title: string; -} - -interface IAccordionItem { - id: string; - children: React.ReactNode | React.ReactNode[]; - title: string; - isSelected?: boolean; - onClick?: VoidFunction; - onFocus?: VoidFunction; -} - -export const Accordion = ({ id, children, title }: IAccordion) => { - const [selectedAccordion, setSelectedAccordion] = - useState(); - - return ( -
-

{title}

- {Children.map( - children, - (child: React.ReactElement, index) => - cloneElement(child, { - isSelected: selectedAccordion === index, - id: `${id}_${child.props.id}_${index}`, - onClick: () => { - if (child.props.onClick) { - child.props.onClick(); - } - - setSelectedAccordion(index); - }, - onFocus: () => setSelectedAccordion(index) - }) - )} -
- ); -}; - -export const AccordionItem = ({ - id, - title, - children, - isSelected, - onClick, - onFocus -}: IAccordionItem) => ( -
-

- -

-
- {children} -
-
-); diff --git a/src/course/02-lessons/02-Silver/Compound/final/components/Accoridon.module.css b/src/course/02-lessons/02-Silver/Compound/final/components/Accoridon.module.css deleted file mode 100644 index 53889b7..0000000 --- a/src/course/02-lessons/02-Silver/Compound/final/components/Accoridon.module.css +++ /dev/null @@ -1,38 +0,0 @@ -.accordionPanel { - @apply py-0; - @apply px-4; - @apply h-0; - @apply transition-all; - @apply duration-300; -} - -.accordionItem:focus-within .accordionPanel, -.accordionPanelSelected { - @apply p-4; - @apply h-auto; - @apply max-h-[1000px]; - @apply transition-all; - @apply duration-300; -} - -.accordionButton { - @apply bg-blue-100; - @apply hover:bg-blue-200; - @apply text-blue-950; -} - -.accordionItem:focus-within .accordionButton, -.accordionButtonSelected { - @apply bg-blue-950; - @apply hover:bg-blue-950; - @apply text-white; -} - -.accordionIcon { - @apply rotate-[-90deg]; -} - -.accordionItem:focus-within .accordionIcon, -.accordionButtonSelected .accordionIcon { - @apply rotate-0; -} diff --git a/src/course/02-lessons/02-Silver/Compound/final/components/ChevronDown.tsx b/src/course/02-lessons/02-Silver/Compound/final/components/ChevronDown.tsx deleted file mode 100644 index a2303f4..0000000 --- a/src/course/02-lessons/02-Silver/Compound/final/components/ChevronDown.tsx +++ /dev/null @@ -1,21 +0,0 @@ -export const ChevronDown = () => ( - - - - - - -); diff --git a/src/course/02-lessons/02-Silver/Compound/final/components/PokemonTeamBuilder.tsx b/src/course/02-lessons/02-Silver/Compound/final/components/PokemonTeamBuilder.tsx new file mode 100644 index 0000000..e74efa2 --- /dev/null +++ b/src/course/02-lessons/02-Silver/Compound/final/components/PokemonTeamBuilder.tsx @@ -0,0 +1,109 @@ +import React, { useState, createContext, useContext } from 'react'; +import classNames from 'classnames'; + +interface IPokemonTeamBuilder { + title: string; + children: React.ReactNode; +} + +interface ITeamSlot { + position: number; + pokemonName?: string; + pokemonLevel?: number; + pokemonType?: string; + onClick?: () => void; +} + +interface TeamContextType { + selectedSlot: string | null; + selectSlot: (slotId: string) => void; +} + +const TeamContext = createContext(null); + +const PokemonTeamBuilder = ({ title, children }: IPokemonTeamBuilder) => { + const [selectedSlot, setSelectedSlot] = useState(null); + + const selectSlot = (slotId: string) => { + setSelectedSlot(selectedSlot === slotId ? null : slotId); + }; + + return ( + +
+

{title}

+
+ {children} +
+
+
+ ); +}; + +const TeamSlot = ({ + position, + pokemonName, + pokemonLevel, + pokemonType, + onClick +}: ITeamSlot) => { + const context = useContext(TeamContext); + if (!context) { + throw new Error('TeamSlot must be used within PokemonTeam'); + } + + const { selectedSlot, selectSlot } = context; + const slotId = `slot-${position}`; + const isSelected = selectedSlot === slotId; + + const handleClick = () => { + if (onClick) { + onClick(); // Call external onClick first (for tracking) + } + selectSlot(slotId); // Then handle selection + }; + + return ( +
+
+
+ Slot {position} +
+ + {pokemonName ? ( +
+
+ {pokemonName} +
+
+ Level {pokemonLevel} +
+
+ {pokemonType} +
+
+ ) : ( +
+
โž•
+
Empty Slot
+
+ )} +
+
+ ); +}; + +// Compound pattern export structure +export const PokemonTeam = Object.assign(PokemonTeamBuilder, { + Slot: TeamSlot +}); \ No newline at end of file diff --git a/src/course/02-lessons/02-Silver/Compound/final/final.tsx b/src/course/02-lessons/02-Silver/Compound/final/final.tsx index e2e5910..e7ba838 100644 --- a/src/course/02-lessons/02-Silver/Compound/final/final.tsx +++ b/src/course/02-lessons/02-Silver/Compound/final/final.tsx @@ -1,71 +1,37 @@ -import { Accordion, AccordionItem } from './components/Accordion'; +import { PokemonTeam } from './components/PokemonTeamBuilder'; export const Final = () => ( - - { - // TODO: Replace with proper analytics tracking - }} - > -

- Per torquent, mus cursus hendrerit id aenean justo auctor - donec. Turpis magna et, egestas dignissim nascetur. Sapien - augue nisl varius diam aliquet. Litora velit, tortor at ante. - Eros lacus faucibus consequat scelerisque proin volutpat. In - pellentesque est curae; dapibus nisl risus sociosqu penatibus. - Lobortis pulvinar scelerisque lacus. Elit vel eros facilisi - dis mauris magna posuere? Cum class viverra bibendum rutrum - odio scelerisque scelerisque libero, nisl est convallis non. - Ac convallis odio suspendisse velit mollis libero. Morbi enim - blandit venenatis lorem! -

-
- -

- Per torquent, mus cursus hendrerit id aenean justo auctor - donec. Turpis magna et, egestas dignissim nascetur. Sapien - augue nisl varius diam aliquet. Litora velit, tortor at ante. - Eros lacus faucibus consequat scelerisque proin volutpat. In - pellentesque est curae; dapibus nisl risus sociosqu penatibus. - Lobortis pulvinar scelerisque lacus. Elit vel eros facilisi - dis mauris magna posuere? Cum class viverra bibendum rutrum - odio scelerisque scelerisque libero, nisl est convallis non. - Ac convallis odio suspendisse velit mollis libero. Morbi enim - blandit venenatis lorem! -

-
- -

- Per torquent, mus cursus hendrerit id aenean justo auctor - donec. Turpis magna et, egestas dignissim nascetur. Sapien - augue nisl varius diam aliquet. Litora velit, tortor at ante. - Eros lacus faucibus consequat scelerisque proin volutpat. In - pellentesque est curae; dapibus nisl risus sociosqu penatibus. - Lobortis pulvinar scelerisque lacus. Elit vel eros facilisi - dis mauris magna posuere? Cum class viverra bibendum rutrum - odio scelerisque scelerisque libero, nisl est convallis non. - Ac convallis odio suspendisse velit mollis libero. Morbi enim - blandit venenatis lorem! -

-
- -

- Per torquent, mus cursus hendrerit id aenean justo auctor - donec. Turpis magna et, egestas dignissim nascetur. Sapien - augue nisl varius diam aliquet. Litora velit, tortor at ante. - Eros lacus faucibus consequat scelerisque proin volutpat. In - pellentesque est curae; dapibus nisl risus sociosqu penatibus. - Lobortis pulvinar scelerisque lacus. Elit vel eros facilisi - dis mauris magna posuere? Cum class viverra bibendum rutrum - odio scelerisque scelerisque libero, nisl est convallis non. - Ac convallis odio suspendisse velit mollis libero. Morbi enim - blandit venenatis lorem! -

-
-
+
+ + { + console.log('TRACK SLOT 1'); + }} + /> + + + + + + +
); diff --git a/src/course/02-lessons/02-Silver/Compound/lesson.mdx b/src/course/02-lessons/02-Silver/Compound/lesson.mdx index 7bbd99d..9c7ce74 100644 --- a/src/course/02-lessons/02-Silver/Compound/lesson.mdx +++ b/src/course/02-lessons/02-Silver/Compound/lesson.mdx @@ -6,23 +6,26 @@ import { Meta } from '@storybook/blocks'; Compound components is an advanced React container pattern that provides a simple and efficient way for multiple components to share states and handle logic โ€” working together. -The compound components pattern provides an expressive and flexible API for communication between a parent component and its children. Also, the compound components pattern enables a parent component to interact and share state with its children implicitly, which makes it suitable for building declarative UI. A good example is the select html element: - -```html - +The compound components pattern provides an expressive and flexible API for communication between a parent component and its children. Also, the compound components pattern enables a parent component to interact and share state with its children implicitly, which makes it suitable for building declarative UI. A good example is a Pokemon team builder: + +```jsx + + + + + + + + ``` -In the code above, the select element manages and shares its state implicitly with the options elements. Consequently, although there is no explicit state declaration, the select element knows what option the user selects. +In the code above, the PokemonTeam component manages and shares its state implicitly with the Slot and Pokemon components. Consequently, although there is no explicit state declaration, the team knows which Pokemon are selected and their positions. -The compound component pattern is useful in building complex React components such as a switch, tab switcher, accordion, dropdowns, tag list, and more. It can be implemented either by using the Context API or the React.cloneElement function. +The compound component pattern is useful in building complex React components such as team builders, accordions, tab switchers, dropdowns, and more. It can be implemented either by using the Context API or the React.cloneElement function. ## Exercise -A requirement has come in to reuse the accordion in another location of our application. The current implementation of the accordion has it's state management implemented only on the page that this component is used on. We need to refactor the component to use the compound design pattern so that it can be re-used on both pages. Head over to the exercise.tsx to continue. +A requirement has come in to reuse the Pokemon team builder in another location of our application. The current implementation of the team builder has its state management implemented only on the page that this component is used on. We need to refactor the component to use the compound design pattern so that it can be re-used on both pages. Head over to the exercise.tsx to continue. ## Feedback From e75e3b0cb18fe386b5d1daabaec16e3d124a3e43 Mon Sep 17 00:00:00 2001 From: code-mattclaffey Date: Thu, 4 Sep 2025 13:38:30 +0100 Subject: [PATCH 14/29] feat: implement pokemone controlled component version --- .../Controlled/exercise/exercise.tsx | 158 ++++++++++++--- .../02-Silver/Controlled/final/final.tsx | 184 +++++++++++++++--- .../02-Silver/Controlled/lesson.mdx | 49 +++-- 3 files changed, 309 insertions(+), 82 deletions(-) diff --git a/src/course/02-lessons/02-Silver/Controlled/exercise/exercise.tsx b/src/course/02-lessons/02-Silver/Controlled/exercise/exercise.tsx index d3878c7..80e5807 100644 --- a/src/course/02-lessons/02-Silver/Controlled/exercise/exercise.tsx +++ b/src/course/02-lessons/02-Silver/Controlled/exercise/exercise.tsx @@ -7,12 +7,21 @@ import { useEffect, useRef, useState } from 'react'; import FocusLock from 'react-focus-lock'; import { Button } from '@shared/components/Button/Button.component'; -interface IModal { +interface IEvolutionModal { isVisible: boolean; onClose: () => void; + onConfirm: () => void; id: string; - title: string; - children: React.ReactNode | React.ReactNode[]; + pokemon: { + name: string; + level: number; + currentSprite: string; + }; + evolution: { + name: string; + sprite: string; + requirement: string; + }; } // For the full guide to making an accessible modal you can follow below to get every instance @@ -20,23 +29,27 @@ interface IModal { // ๐Ÿ’ฃ You can get rid of this eslint error comment when finished. // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars -const Modal = ({ +const EvolutionModal = ({ isVisible, // ๐Ÿ’ฃ You can get rid of this eslint error comment when finished. // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars onClose, + // ๐Ÿ’ฃ You can get rid of this eslint error comment when finished. + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onConfirm, id, - title, - children -}: IModal) => { - // 2a ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Create a useRef and bind the ref to the div on line 58 + pokemon, + evolution +}: IEvolutionModal) => { + // 2a ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Create a useRef and bind the ref to the div on line 70 useEffect(() => { // โœ๐Ÿป When a modal is visible you want to navigate the focus from // the actioner (what caused the modal to open) to the content // โ™ฟ๏ธ It helps the screenreader not get lost on the page - // 2b - ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Check if isVisible is true and the modal.current is defined before setting focus to the modal + // 2b - ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Check if isVisible is true and the modal.current is defined before setting focus to the modal }, [isVisible]); // ๐Ÿ’ฃ You can get rid of this eslint error comment when finished. @@ -61,34 +74,78 @@ const Modal = ({ // 2c - ๐Ÿ’„ Add an object as the second param with flex: isVisible and hidden !isVisible )} role="button" - // 2d - ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Pass the onClose event from the props to the onClick event. + // 2d - ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Pass the onClose event from the props to the onClick event. tabIndex={0} >
@@ -96,21 +153,60 @@ const Modal = ({ }; export const Exercise = () => { - // 1a ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Create a state hook variable with isVisible and setIsVisible + // 1a ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Create a state hook variable with isEvolutionVisible and setIsEvolutionVisible + + // 1b ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Create an onClose event that sets isEvolutionVisible to false - // 1b ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Create an onClose event that sets isVisible to false + // 1c ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Create an onConfirm event that handles evolution and closes modal - // 1c ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Create an onOpen event that sets isVisible to true + // 1d ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Create an onCheckEvolution event that sets isEvolutionVisible to true + + const pokemon = { + name: 'Charmander', + level: 16, + currentSprite: + 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/4.png' + }; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const evolution = { + name: 'Charmeleon', + sprite: + 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/5.png', + requirement: 'Level 16 reached!' + }; return ( - <> - {/* 1d ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Add the onClick={onOpen} event to the button - โœ๐Ÿป This is an example of a Controlled component but in a HTML context. +
+

+ ๐ŸŽฎ Pokemon Evolution System +

+ +
+ {pokemon.name} +

{pokemon.name}

+

Level {pokemon.level}

+

+ Ready to evolve! ๐ŸŒŸ +

+
+ + {/* 1e ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Add the onClick={onCheckEvolution} event to the button + โœ๐Ÿป This is an example of a Controlled component but in a Pokemon context. As a developer, we are providing the button with those props for the button to behave how we want it to behave, otherwise, it does nothing. */} - - {/* 1e ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Check if isVisible (๐Ÿ’… Conditional Render Pattern) to render the Modal */} - {/* Map the isVisible and onClose props to the Modal. The other props can be whatever you want */} - +
+ +
+ + {/* 1f ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Check if isEvolutionVisible (๐Ÿ’… Conditional Render Pattern) to render the EvolutionModal */} + {/* Map the isVisible, onClose, onConfirm props to the EvolutionModal. The other props can be whatever you want */} +
); }; diff --git a/src/course/02-lessons/02-Silver/Controlled/final/final.tsx b/src/course/02-lessons/02-Silver/Controlled/final/final.tsx index 0976052..fff78c7 100644 --- a/src/course/02-lessons/02-Silver/Controlled/final/final.tsx +++ b/src/course/02-lessons/02-Silver/Controlled/final/final.tsx @@ -3,21 +3,31 @@ import { useEffect, useRef, useState } from 'react'; import FocusLock from 'react-focus-lock'; import { Button } from '@shared/components/Button/Button.component'; -interface IModal { +interface IEvolutionModal { isVisible: boolean; onClose: () => void; + onConfirm: () => void; id: string; - title: string; - children: React.ReactNode | React.ReactNode[]; + pokemon: { + name: string; + level: number; + currentSprite: string; + }; + evolution: { + name: string; + sprite: string; + requirement: string; + }; } -const Modal = ({ +const EvolutionModal = ({ isVisible, onClose, + onConfirm, id, - title, - children -}: IModal) => { + pokemon, + evolution +}: IEvolutionModal) => { const modal = useRef(null); useEffect(() => { @@ -44,21 +54,76 @@ const Modal = ({
@@ -66,31 +131,88 @@ const Modal = ({ }; export const Final = () => { - const [isVisible, setIsVisible] = useState(false); + const [isEvolutionVisible, setIsEvolutionVisible] = useState(false); + const [currentPokemon, setCurrentPokemon] = useState({ + name: 'Charmander', + level: 16, + currentSprite: + 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/4.png' + }); + + const evolution = { + name: 'Charmeleon', + sprite: + 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/5.png', + requirement: 'Level 16 reached!' + }; const onClose = () => { - setIsVisible(false); + setIsEvolutionVisible(false); + }; + + const onConfirm = () => { + setCurrentPokemon({ + name: evolution.name, + level: currentPokemon.level, + currentSprite: evolution.sprite + }); + setIsEvolutionVisible(false); }; - const onOpen = () => { - setIsVisible(true); + const onCheckEvolution = () => { + if ( + currentPokemon.level >= 16 && + currentPokemon.name === 'Charmander' + ) { + setIsEvolutionVisible(true); + } }; return ( - <> - - {isVisible && ( - +

+ ๐ŸŽฎ Pokemon Evolution System +

+ +
+ {currentPokemon.name} +

{currentPokemon.name}

+

Level {currentPokemon.level}

+ {currentPokemon.name === 'Charmander' && + currentPokemon.level >= 16 && ( +

+ Ready to evolve! ๐ŸŒŸ +

+ )} +
+ +
+ +
+ + {isEvolutionVisible && ( + )} - +
); }; diff --git a/src/course/02-lessons/02-Silver/Controlled/lesson.mdx b/src/course/02-lessons/02-Silver/Controlled/lesson.mdx index f1226fa..a8cb896 100644 --- a/src/course/02-lessons/02-Silver/Controlled/lesson.mdx +++ b/src/course/02-lessons/02-Silver/Controlled/lesson.mdx @@ -6,48 +6,57 @@ import { Meta } from '@storybook/blocks'; The concept of controlled components involves creating components with highly predictable behavior by managing their state through props. A controlled components behavior changes based on the state passed to it as a prop. -In the example below, the componentโ€™s visibility is controlled by a prop, and it also accepts an onClose prop. In the parent component, invoking the onClose prop will hide the component. Clicking the "open" button in the parent component will then restore the components visibility. +In the example below, a Pokemon evolution modal's visibility is controlled by evolution conditions, and it accepts evolution callbacks. The parent component manages when evolution can occur based on level, stones, or friendship requirements. ```jsx -const Component = ({ isVisible, onClose }) => { +const EvolutionModal = ({ isOpen, pokemon, evolutionData, onConfirm, onCancel }) => { return ( - ); -}; +}; \ No newline at end of file diff --git a/src/course/02-lessons/02-Silver/Portals/exercise/exercise.tsx b/src/course/02-lessons/02-Silver/Portals/exercise/exercise.tsx index 9e87a08..ed48546 100644 --- a/src/course/02-lessons/02-Silver/Portals/exercise/exercise.tsx +++ b/src/course/02-lessons/02-Silver/Portals/exercise/exercise.tsx @@ -1,77 +1,104 @@ import { useState } from 'react'; -import { Modal } from './components/modal'; +import { BattleOverlay } from './components/modal'; import { Button } from '@shared/components/Button/Button.component'; -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 1A - have a look at the current implementation of the modal and then go to components/modal.tsx +// ๐Ÿ‘จ๐Ÿป๐Ÿ’ป 1A - have a look at the current implementation of the battle overlay and then go to components/modal.tsx export const Exercise = () => { - const [isVisible, setIsVisible] = useState(false); - const [isComplete, setIsComplete] = useState(false); + const [isBattleActive, setIsBattleActive] = useState(false); + const [battleResult, setBattleResult] = useState<'won' | 'fled' | null>(null); - const onClose = () => { - setIsVisible(false); + const onCloseBattle = () => { + setIsBattleActive(false); }; - const onOpen = () => { - setIsVisible(true); + const onStartBattle = () => { + setIsBattleActive(true); + setBattleResult(null); }; - const onCheckout = () => { - setIsComplete(true); + const onBattleAction = (action: 'attack' | 'run') => { + if (action === 'attack') { + setBattleResult('won'); + } else { + setBattleResult('fled'); + } + setTimeout(() => { + setIsBattleActive(false); + setBattleResult(null); + }, 2000); }; return ( - // ๐Ÿงช We have z-index 10 on the section and then z-9998 on a div that's purposely there. Our Modal has a z-20 which means: + // ๐Ÿงช We have z-index 10 on the section and then z-9998 on a div that's purposely there. Our BattleOverlay has a z-20 which means: // section z-10 - // modal z-20 (but this means z-20 within the z-10) think of it as a sub layer. - // the bug is 9998 and a css hack for the pay now is 9999 -
-
- {isComplete && ( - <> -

- Payment Successful -

-

Well done you did it!

- - )} + // battle overlay z-20 (but this means z-20 within the z-10) think of it as a sub layer. + // the bug is 9998 and a css hack for the battle buttons is 9999 +
+
+ +
+

๐ŸŒฟ Pokemon World

- {!isComplete && ( - <> -

Payment Page

+
+
+
๐ŸŒฒ
+

Tall Grass

+
+
+
๐Ÿ 
+

Pokemon Center

+
+
+
๐Ÿช
+

Poke Mart

+
+
-

- Please see your selected options from the previous steps - before continuing. +

+

๐ŸŽ’ Trainer Actions

+

+ You're walking through the tall grass. Wild Pokemon might appear!

+ +
+ +
+
-
-

- Delivery Details -

-
-

12 john doe street, Manchester, M12 3RT

-
-
+
+

๐ŸŽฎ Game Status

+

+ {isBattleActive ? 'Battle in progress...' : 'Exploring the world'} +

+ {battleResult && ( +

+ {battleResult === 'won' ? '๐ŸŽ‰ Victory!' : '๐Ÿ’จ Pokemon fled!'} +

+ )} +
+
-
-

- Make Payment -

- -
- - )} - {isVisible && !isComplete && ( - - - + {isBattleActive && ( + )}
); -}; +}; \ No newline at end of file diff --git a/src/course/02-lessons/02-Silver/Portals/final/components/modal.tsx b/src/course/02-lessons/02-Silver/Portals/final/components/modal.tsx index 7a1f8ad..a98c006 100644 --- a/src/course/02-lessons/02-Silver/Portals/final/components/modal.tsx +++ b/src/course/02-lessons/02-Silver/Portals/final/components/modal.tsx @@ -4,21 +4,27 @@ import { createPortal } from 'react-dom'; import FocusLock from 'react-focus-lock'; import { Button } from '@shared/components/Button/Button.component'; -interface IModal { +interface IBattleOverlay { isVisible: boolean; onClose: () => void; id: string; - title: string; - children: React.ReactNode | React.ReactNode[]; + wildPokemon: { + name: string; + level: number; + sprite: string; + }; + onBattleAction: (action: 'attack' | 'run') => void; + battleResult: 'won' | 'fled' | null; } -export const Modal = ({ +export const BattleOverlay = ({ isVisible, onClose, id, - title, - children -}: IModal) => { + wildPokemon, + onBattleAction, + battleResult +}: IBattleOverlay) => { const modal = useRef(null); useEffect(() => { @@ -32,48 +38,90 @@ export const Modal = ({ event.preventDefault(); }; - const handleKeyDown = (event: React.KeyboardEvent) => { - if (event.key === 'Enter' || event.key === ' ') { - onClose(); - } - }; - - if (!document.body) { - return null; - } - - return createPortal( + const battleOverlay = (
-
, - document.body +
); -}; + + return createPortal(battleOverlay, document.body); +}; \ No newline at end of file diff --git a/src/course/02-lessons/02-Silver/Portals/final/final.tsx b/src/course/02-lessons/02-Silver/Portals/final/final.tsx index 04c5305..5700f42 100644 --- a/src/course/02-lessons/02-Silver/Portals/final/final.tsx +++ b/src/course/02-lessons/02-Silver/Portals/final/final.tsx @@ -1,71 +1,98 @@ import { useState } from 'react'; -import { Modal } from './components/modal'; +import { BattleOverlay } from './components/modal'; import { Button } from '@shared/components/Button/Button.component'; export const Final = () => { - const [isVisible, setIsVisible] = useState(false); - const [isComplete, setIsComplete] = useState(false); + const [isBattleActive, setIsBattleActive] = useState(false); + const [battleResult, setBattleResult] = useState<'won' | 'fled' | null>(null); - const onClose = () => { - setIsVisible(false); + const onCloseBattle = () => { + setIsBattleActive(false); }; - const onOpen = () => { - setIsVisible(true); + const onStartBattle = () => { + setIsBattleActive(true); + setBattleResult(null); }; - const onCheckout = () => { - setIsComplete(true); + const onBattleAction = (action: 'attack' | 'run') => { + if (action === 'attack') { + setBattleResult('won'); + } else { + setBattleResult('fled'); + } + setTimeout(() => { + setIsBattleActive(false); + setBattleResult(null); + }, 2000); }; return ( -
-
- {isComplete && ( - <> -

- Payment Successful -

-

Checkout has been successful

- - )} +
+
+ +
+

๐ŸŒฟ Pokemon World

- {!isComplete && ( - <> -

Payment Page

+
+
+
๐ŸŒฒ
+

Tall Grass

+
+
+
๐Ÿ 
+

Pokemon Center

+
+
+
๐Ÿช
+

Poke Mart

+
+
-

- Please see your selected options from the previous steps - before continuing. +

+

๐ŸŽ’ Trainer Actions

+

+ You're walking through the tall grass. Wild Pokemon might appear!

+ +
+ +
+
-
-

- Delivery Details -

-
-

12 john doe street, Manchester, M12 3RT

-
-
+
+

๐ŸŽฎ Game Status

+

+ {isBattleActive ? 'Battle in progress...' : 'Exploring the world'} +

+ {battleResult && ( +

+ {battleResult === 'won' ? '๐ŸŽ‰ Victory!' : '๐Ÿ’จ Pokemon fled!'} +

+ )} +
+
-
-

- Make Payment -

- -
- - )} - {isVisible && !isComplete && ( - - - + {isBattleActive && ( + )}
); -}; +}; \ No newline at end of file diff --git a/src/course/02-lessons/02-Silver/Portals/lesson.mdx b/src/course/02-lessons/02-Silver/Portals/lesson.mdx index 1442280..7b534ff 100644 --- a/src/course/02-lessons/02-Silver/Portals/lesson.mdx +++ b/src/course/02-lessons/02-Silver/Portals/lesson.mdx @@ -4,7 +4,7 @@ import { Meta } from '@storybook/blocks'; # ๐ŸŒŒ Portals Pattern -A React portal lets you render some children into a different part of the DOM. When you call the **createPortal** it will trigger the creation of the portal. When you unmount, the portal removes itself. This is how it looks in React: +A React portal lets you render some children into a different part of the DOM. When you call the **createPortal** it will trigger the creation of the portal. When you unmount, the portal removes itself. This is perfect for Pokemon battle overlays that need to appear above everything else in the game. ```jsx import { createPortal } from 'react-dom'; @@ -12,9 +12,11 @@ import { createPortal } from 'react-dom'; // ...
-

This child is placed in the parent div.

+

This is the main game interface.

{createPortal( -

This child is placed in the document body.

, +
+

A wild Pokemon appeared!

+
, document.body )}
; @@ -25,31 +27,35 @@ Which in html, will translate to: ```html - My react app + Pokemon Game
-

This child is placed in the parent div.

+

This is the main game interface.

-

This child is placed in the document body.

+
+

A wild Pokemon appeared!

+
``` Portal benefits: -- Simplified state management -- No clashes with z-index as the modal is at the root of the DOM. +- Battle overlays render above all game content +- No clashes with z-index as the battle screen is at the root of the DOM +- Simplified state management for battle transitions +- Perfect for full-screen battle interfaces ## Exercise -In the current application when a customer clicks the checkout cta and the payment popup appears, the customer cannot click the pay now button in the modal. +In the current Pokemon game when a trainer encounters a wild Pokemon and the battle overlay appears, the trainer cannot interact with the battle interface properly due to z-index issues with the main game content. ## Feedback Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you. -[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new) +[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new) \ No newline at end of file From 8f1e5800dbedf1396808302ad6ce15b218c25839 Mon Sep 17 00:00:00 2001 From: code-mattclaffey Date: Thu, 4 Sep 2025 14:13:59 +0100 Subject: [PATCH 16/29] feat: implement pokemon style for poly components --- .../Controlled/exercise/exercise.tsx | 22 +-- .../exercise/exercise.tsx | 152 ++++++++++------ .../PolymorphicComponents/final/final.tsx | 167 ++++++++++++------ .../PolymorphicComponents/lesson.mdx | 89 ++++++---- .../Portals/exercise/components/modal.tsx | 51 ++++-- .../02-Silver/Portals/exercise/exercise.tsx | 36 ++-- 6 files changed, 329 insertions(+), 188 deletions(-) diff --git a/src/course/02-lessons/02-Silver/Controlled/exercise/exercise.tsx b/src/course/02-lessons/02-Silver/Controlled/exercise/exercise.tsx index 7592acb..50230cd 100644 --- a/src/course/02-lessons/02-Silver/Controlled/exercise/exercise.tsx +++ b/src/course/02-lessons/02-Silver/Controlled/exercise/exercise.tsx @@ -43,13 +43,13 @@ const EvolutionModal = ({ pokemon, evolution }: IEvolutionModal) => { - // 2a ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Create a useRef and bind the ref to the div on line 70 + // 2a ๐Ÿ’ป Create a useRef and bind the ref to the div on line 70 useEffect(() => { // โœ๐Ÿป When a modal is visible you want to navigate the focus from // the actioner (what caused the modal to open) to the content // โ™ฟ๏ธ It helps the screenreader not get lost on the page - // 2b - ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Check if isVisible is true and the modal.current is defined before setting focus to the modal + // 2b - ๐Ÿ’ป Check if isVisible is true and the modal.current is defined before setting focus to the modal }, [isVisible]); // ๐Ÿ’ฃ You can get rid of this eslint error comment when finished. @@ -74,7 +74,7 @@ const EvolutionModal = ({ // 2c - ๐Ÿ’„ Add an object as the second param with flex: isVisible and hidden !isVisible )} role="button" - // 2d - ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Pass the onClose event from the props to the onClick event. + // 2d - ๐Ÿ’ป Pass the onClose event from the props to the onClick event. tabIndex={0} >
- {/* 2g - ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Add onClick={onConfirm} for evolution confirmation */} + {/* 2g - ๐Ÿ’ป Add onClick={onConfirm} for evolution confirmation */} - {/* 2h - ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Add onClick={onClose} going back to the pattern, we want outside to control the visibility of the modal */} + {/* 2h - ๐Ÿ’ป Add onClick={onClose} going back to the pattern, we want outside to control the visibility of the modal */} @@ -153,13 +153,13 @@ const EvolutionModal = ({ }; export const Exercise = () => { - // 1a ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Create a state hook variable with isEvolutionVisible and setIsEvolutionVisible + // 1a ๐Ÿ’ป Create a state hook variable with isEvolutionVisible and setIsEvolutionVisible - // 1b ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Create an onClose event that sets isEvolutionVisible to false + // 1b ๐Ÿ’ป Create an onClose event that sets isEvolutionVisible to false - // 1c ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Create an onConfirm event that handles evolution and closes modal + // 1c ๐Ÿ’ป Create an onConfirm event that handles evolution and closes modal - // 1d ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Create an onCheckEvolution event that sets isEvolutionVisible to true + // 1d ๐Ÿ’ป Create an onCheckEvolution event that sets isEvolutionVisible to true const pokemon = { name: 'Charmander', @@ -197,7 +197,7 @@ export const Exercise = () => {

- {/* 1e ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Add the onClick={onCheckEvolution} event to the button + {/* 1e ๐Ÿ’ป Add the onClick={onCheckEvolution} event to the button โœ๐Ÿป This is an example of a Controlled component but in a Pokemon context. As a developer, we are providing the button with those props for the button to behave how we want it to behave, otherwise, it does nothing. */} @@ -207,7 +207,7 @@ export const Exercise = () => {
- {/* 1f ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Check if isEvolutionVisible (๐Ÿ’… Conditional Render Pattern) to render the EvolutionModal */} + {/* 1f ๐Ÿ’ป Check if isEvolutionVisible (๐Ÿ’… Conditional Render Pattern) to render the EvolutionModal */} {/* Map the isVisible, onClose, onConfirm props to the EvolutionModal. The other props can be whatever you want */}
); diff --git a/src/course/02-lessons/02-Silver/PolymorphicComponents/exercise/exercise.tsx b/src/course/02-lessons/02-Silver/PolymorphicComponents/exercise/exercise.tsx index f54dfd3..8841186 100644 --- a/src/course/02-lessons/02-Silver/PolymorphicComponents/exercise/exercise.tsx +++ b/src/course/02-lessons/02-Silver/PolymorphicComponents/exercise/exercise.tsx @@ -1,102 +1,146 @@ import { HTMLAttributes } from 'react'; /** - * Exercise: Refactor the Heading component to correctly use the polymorphic pattern. + * Exercise: Refactor the StatusEffect component to correctly use the polymorphic pattern. * * ๐Ÿค” Observations of this file - * In the current component you can see that the as prop is a string so if a developer in a team uses the wrong element they would just get the h2 element. - * Font sizes are clearly defined to the element so there is no flexibility in sizes which can lead to developers pleasing designers but... breaking accessibility or vice versa where designs do not look the same as what was provided. + * In the current component you can see that the as prop is a string so if a developer in a team uses the wrong element they would just get the span element. + * Status styles are clearly defined to the element so there is no flexibility in status effects which can lead to developers pleasing designers but... breaking accessibility or vice versa where designs do not look the same as what was provided. * * We need to tackle this in stages... * * Stage one - Refactoring the component to use Polymorphic style so we remove the switch statement. - * Stage two - decouple the font size to the element - * Stage three - allow for developers to have a size medium breakpoint for special designs. + * Stage two - decouple the status effect to the element + * Stage three - allow for developers to have a severity level for special status effects. * */ -// ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 1.a - Create a type called allowedHTMLElements +// ๐Ÿง‘๐Ÿป๐Ÿ’ป 1.a - Create a type called AllowedElements for 'span' | 'div' | 'button' | 'li' -// ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 2.a - Create a type called FontSizes and it's a union of 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' +// ๐Ÿง‘๐Ÿป๐Ÿ’ป 2.a - Create a type called StatusTypes and it's a union of 'normal' | 'burned' | 'poisoned' | 'paralyzed' | 'frozen' | 'asleep' -interface IHeading extends HTMLAttributes { - // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 1.b - Update the type of string to be the type you defined as part of 1.a +interface IStatusEffect extends HTMLAttributes { + // ๐Ÿง‘๐Ÿป๐Ÿ’ป 1.b - Update the type of string to be the type you defined as part of 1.a as?: string; - // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 2.b - Create a new prop called size?: FontSizes; - // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 3.a - Create a new prop called sizeMd?: FontSizes; + // ๐Ÿง‘๐Ÿป๐Ÿ’ป 2.b - Create a new prop called status?: StatusTypes; + // ๐Ÿง‘๐Ÿป๐Ÿ’ป 3.a - Create a new prop called severity?: 'mild' | 'severe'; children?: React.ReactNode | React.ReactNode[]; } -const Heading = ({ - // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 1.c - add : Element = 'h2' what this will do is redefine the prop to be a capital variable which can be used as a React Component. - as = 'h2', - // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 2.c - Create a new prop called size - // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 3.b - Create a new prop called sizeMd +const StatusEffect = ({ + // ๐Ÿง‘๐Ÿป๐Ÿ’ป 1.c - add : Element = 'span' what this will do is redefine the prop to be a capital variable which can be used as a React Component. + as = 'span', + // ๐Ÿง‘๐Ÿป๐Ÿ’ป 2.c - Create a new prop called status + // ๐Ÿง‘๐Ÿป๐Ÿ’ป 3.b - Create a new prop called severity children, ...rest -}: IHeading) => { - // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 1.d - Create a variable called elementFontSize which uses useMemo to return a string from an object key mapping. For example: useMemo(() => ({ h1: 'text-3xl' }[Element]), [Element]); - // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 2.d - In the useMemo add the size as a dependency and then check if size exists. If it does, return `text-${size}` if not, return what was there previously. Move onto 3.a. +}: IStatusEffect) => { + // ๐Ÿง‘๐Ÿป๐Ÿ’ป 1.d - Create a variable called statusClass which uses useMemo to return a string from an object key mapping. For example: useMemo(() => ({ span: 'text-gray-600' }[Element]), [Element]); + // ๐Ÿง‘๐Ÿป๐Ÿ’ป 2.d - In the useMemo add the status as a dependency and then check if status exists. If it does, return status-specific classes if not, return what was there previously. Move onto 3.a. - // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 3.c - create another useMemo for largeFontSizes where we find an array of md:text-(sm-3xl) and we need to "find" which one in that array "includes" sizeMd props value. + // ๐Ÿง‘๐Ÿป๐Ÿ’ป 3.c - create another useMemo for severityClass where we check if severity exists and return severity-specific classes. // ๐Ÿงช 3.d Head down to the storybook Exercise Component and add a few more variants in. - // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 1.e return the Element with the className={classNames('mb-3 font-semibold', elementFontSize)} don't forget the ...rest + // ๐Ÿง‘๐Ÿป๐Ÿ’ป 1.e return the Element with the className={classNames('px-2 py-1 rounded text-sm', statusClass)} don't forget the ...rest // ๐Ÿ’ฃ 1.f remove the old code below. Move onto step 2.a. if (as) switch (as) { - case 'h1': + case 'span': return ( -

+ {children} -

+ ); - case 'h3': + case 'div': return ( -

+
{children} -

+
); - case 'h4': + case 'button': return ( -

+

+ ); - case 'h5': + case 'li': return ( -
+
  • {children} -
  • - ); - case 'h6': - return ( -
    - {children} -
    + ); default: return ( -

    + {children} -

    + ); } }; export const Exercise = () => ( -
    - Heading One - Heading Two - Heading Three - Heading Four - Heading Five - Heading Six - {/* 3.e ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Implement a heading as h2 and size sm */} - - {/* 3.e ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Implement a heading as h2 and size sm and sizeMd is 3xl */} - - {/* 3.e ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Implement a heading as h2 and sizeMd is 3xl */} -
    +
    +

    + ๐ŸŽฎ Pokemon Status Effects +

    + +
    +
    +

    Battle Status (Spans)

    +
    + Normal + Burned + Poisoned + Paralyzed +
    +
    + +
    +

    Status Alerts (Divs)

    + + Your Pokemon is badly poisoned! + +
    + +
    +

    Heal Actions (Buttons)

    +
    + Heal Burn + Cure Paralysis +
    +
    + +
    +

    + Status List (List Items) +

    +
      + Frozen - Cannot move + Asleep - Skips turn +
    +
    +
    + + {/* 3.e Implement a status effect as span with status burned */} + + {/* 3.e Implement a status effect as div with status poisoned and severity severe */} + + {/* 3.e Implement a status effect as button with severity mild */} +
    ); diff --git a/src/course/02-lessons/02-Silver/PolymorphicComponents/final/final.tsx b/src/course/02-lessons/02-Silver/PolymorphicComponents/final/final.tsx index c52e68e..e38d00d 100644 --- a/src/course/02-lessons/02-Silver/PolymorphicComponents/final/final.tsx +++ b/src/course/02-lessons/02-Silver/PolymorphicComponents/final/final.tsx @@ -1,63 +1,74 @@ import classNames from 'classnames'; import { HTMLAttributes, useMemo } from 'react'; -type AllowedHTMLElements = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; +type AllowedElements = 'span' | 'div' | 'button' | 'li'; -type FontSizes = 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl'; +type StatusTypes = 'normal' | 'burned' | 'poisoned' | 'paralyzed' | 'frozen' | 'asleep'; -// ๐Ÿ’… could use conditional types here to use the correct element but for the sake of the example we can keep heading element. -interface IHeading extends HTMLAttributes { - as?: AllowedHTMLElements; - size?: FontSizes; - sizeMd?: FontSizes; +interface IStatusEffect extends HTMLAttributes { + as?: AllowedElements; + status?: StatusTypes; + severity?: 'mild' | 'severe'; children?: React.ReactNode; } -const Heading = ({ - as: Element = 'h2', - size, - sizeMd, +const StatusEffect = ({ + as: Element = 'span', + status, + severity, children, ...rest -}: IHeading) => { - const elementFontSize = useMemo( +}: IStatusEffect) => { + const statusClass = useMemo( () => - size - ? `text-${size}` + status + ? { + normal: 'bg-gray-200 text-gray-800 border-gray-300', + burned: 'bg-red-200 text-red-800 border-red-300', + poisoned: 'bg-purple-200 text-purple-800 border-purple-300', + paralyzed: 'bg-yellow-200 text-yellow-800 border-yellow-300', + frozen: 'bg-blue-200 text-blue-800 border-blue-300', + asleep: 'bg-indigo-200 text-indigo-800 border-indigo-300' + }[status] : { - h1: 'text-3xl', - h2: 'text-2xl', - h3: 'text-xl', - h4: 'text-lg', - h5: 'text-md', - h6: 'text-sm' + span: 'bg-gray-200 text-gray-800 border-gray-300', + div: 'bg-gray-100 text-gray-700 border-gray-200', + button: 'bg-blue-200 text-blue-800 border-blue-300 hover:bg-blue-300', + li: 'bg-gray-50 text-gray-600 border-gray-200' }[Element], - [Element, size] + [Element, status] ); - const largeFontSize = useMemo( + const severityClass = useMemo( () => { - const sizeMap: Record = { - sm: 'md:text-sm', - md: 'md:text-md', - lg: 'md:text-lg', - xl: 'md:text-xl', - '2xl': 'md:text-2xl', - '3xl': 'md:text-3xl' - }; - return sizeMd ? sizeMap[sizeMd] : undefined; + if (!severity) return ''; + return severity === 'severe' + ? 'font-bold border-2 shadow-md' + : 'opacity-75'; }, - [sizeMd] + [severity] ); + const baseClasses = useMemo(() => { + const base = 'rounded border'; + switch (Element) { + case 'span': + return `${base} px-2 py-1 text-sm inline-block`; + case 'div': + return `${base} p-3`; + case 'button': + return `${base} px-3 py-2 cursor-pointer transition-colors`; + case 'li': + return `${base} p-2 list-none`; + default: + return `${base} px-2 py-1 text-sm`; + } + }, [Element]); + return ( {children} @@ -65,21 +76,65 @@ const Heading = ({ }; export const Final = () => ( -
    - Heading One - Heading Two - Heading Three - Heading Four - Heading Five - Heading Six - - Heading Two (sm size) - - - Heading Two (sm size mobile and md breakpoint 3xl) - - - Heading Two (md breakpoint 3xl) - -
    -); +
    +

    ๐ŸŽฎ Pokemon Status Effects

    + +
    +
    +

    Battle Status (Spans)

    +
    + Normal + Burned + Poisoned + Paralyzed + Frozen + Asleep +
    +
    + +
    +

    Status Alerts (Divs)

    +
    + + Your Pokemon is badly poisoned! It will take damage each turn. + + + Your Pokemon is burned. It takes minor damage each turn. + +
    +
    + +
    +

    Heal Actions (Buttons)

    +
    + Heal Burn + Cure Paralysis + Antidote + Full Heal +
    +
    + +
    +

    Status List (List Items)

    +
      + Frozen - Cannot move until thawed + Asleep - Skips turn until awakened + Fully Paralyzed - Cannot attack +
    +
    + +
    +

    Advanced Examples

    +
    + ๐Ÿ”ฅ Burned + + โ˜ ๏ธ Severely Poisoned - Seek immediate treatment! + + console.log('Healing...')}> + ๐Ÿ’Š Use Potion + +
    +
    +
    +
    +); \ No newline at end of file diff --git a/src/course/02-lessons/02-Silver/PolymorphicComponents/lesson.mdx b/src/course/02-lessons/02-Silver/PolymorphicComponents/lesson.mdx index 1f713d3..03a9983 100644 --- a/src/course/02-lessons/02-Silver/PolymorphicComponents/lesson.mdx +++ b/src/course/02-lessons/02-Silver/PolymorphicComponents/lesson.mdx @@ -4,26 +4,26 @@ import { Meta } from '@storybook/blocks'; # ๐Ÿฆ„ Polymorphic Components Pattern -React excels at building reusable components, but repetition can creep in when components only differ slightlyโ€”like headings or buttons with varied HTML tags. +React excels at building reusable components, but repetition can creep in when components only differ slightlyโ€”like status effects that need different HTML elements based on context. -Consider this: +Consider this Pokemon battle scenario: ```jsx -export const Paragraph = ({ children }) => ( -

    {children}

    +export const BurnedSpan = ({ children }) => ( + {children} ); -export const HeadingOne = ({ children }) => ( -

    {children}

    +export const ParalyzedButton = ({ children, onClick }) => ( + ); -export const HeadingTwo = ({ children }) => ( -

    {children}

    +export const PoisonedDiv = ({ children, turns }) => ( +
    Poisoned for {turns} turns
    ); -export const HeadingThree = ({ children }) => ( -

    {children}

    +export const SleepingListItem = ({ children }) => ( +
  • {children}
  • ); ``` -Each of these components is nearly identical. This isn't scalable and can be difficult to maintain. The **polymorphic component pattern** solves this by allowing you to render different HTML elements using a single component, often via an **as** prop. +Each of these components is nearly identical except for the HTML tag. This isn't scalable and can be difficult to maintain. The **polymorphic component pattern** solves this by allowing you to render different HTML elements using a single component, often via an **as** prop. ## Who else does this? @@ -32,28 +32,39 @@ Most modern UI libraries (like Chakra UI or Radix UI) support polymorphic compon Example: ```JSX - - My heading is a h3 tag but styled like a h4. - + + Burned - Click to heal + + + + Paralyzed + + + + Badly Poisoned + ``` This would render as: ```jsx -

    My heading is a h3 tag but styled like a h4.

    + +Paralyzed +
    Badly Poisoned
    ``` ## Implementation -Hereโ€™s a simple implementation of a polymorphic **Heading** component: +Here's a simple implementation of a polymorphic **StatusEffect** component: ```jsx -export const Heading = ({ - as: Component = 'h2', - size = 'h2', - children +export const StatusEffect = ({ + as: Component = 'span', + status = 'normal', + children, + ...props }) => { - return {children}; + return {children}; }; ``` @@ -63,41 +74,43 @@ export const Heading = ({ This pattern is extremely useful in design systems, especially when building: -- Typography components -- Buttons -- Form elements -- Reusable layout primitives +- Typography components (headings, paragraphs, labels) +- Button variants (buttons, links, spans with click handlers) +- Status indicators (badges, alerts, notifications) +- Form elements that need semantic flexibility +- Navigation components (links, buttons, divs) It ensures your components stay flexible while keeping semantic HTML and accessible markup intact. Here's a quick visual guide: -| Usage | Renders As | Styled As | -| ----------------------------- | ---------- | --------- | -| **Heading** | **h2** | **h2** | -| **Heading as="h3"** | **h3** | **h2** | -| **Heading as="h3" size="h4"** | **h3** | **h4** | +| Usage | Renders As | Status Class | +| ---------------------------------------- | ---------- | ------------ | +| **StatusEffect** | **span** | **normal** | +| **StatusEffect as="button"** | **button** | **normal** | +| **StatusEffect status="burned"** | **span** | **burned** | +| **StatusEffect as="div" status="poisoned"** | **div** | **poisoned** | ## Exercise ### Scenario -The software engineering group are using a Heading component and it works fairly well from an implementation point of view but there is often friction between the design teams and developers around design consistency vs accessibility. +The Pokemon battle system team is using a StatusEffect component and it works fairly well from an implementation point of view but there is often friction between the design teams and developers around design consistency vs accessibility. -The previous developer built a heading component using the right intentions however, the flexibility of the component is a little rigid. +The previous developer built a status effect component using the right intentions however, the flexibility of the component is a little rigid. ### What we are going to do? -In todayโ€™s exercise, weโ€™re going to refactor this component to use the Polymorphic pattern. +In today's exercise, we're going to refactor this component to use the Polymorphic pattern. It should: -1. Render a semantic HTML tag based on the **as** prop BUT we have typescript in place to only allow for the heading tags and p tags. -2. Apply a CSS class based on a **size** prop (defaulting to the tag if **size** is not provided). -3. Fall back to defaults (p, p) if neither is provided. +1. Render a semantic HTML tag based on the **as** prop BUT we have typescript in place to only allow for span, div, button, and li tags. +2. Apply a CSS class based on a **status** prop (defaulting to 'normal' if **status** is not provided). +3. Fall back to defaults (span, normal) if neither is provided. ## Feedback -Feedback is a gift and it helps me make these courses better for you. If you have 5 minutes, Iโ€™d love for you to fill out the feedback form: +Feedback is a gift and it helps me make these courses better for you. If you have 5 minutes, I'd love for you to fill out the feedback form: -[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new) +[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new) \ No newline at end of file diff --git a/src/course/02-lessons/02-Silver/Portals/exercise/components/modal.tsx b/src/course/02-lessons/02-Silver/Portals/exercise/components/modal.tsx index 1d51b8a..7bb98b1 100644 --- a/src/course/02-lessons/02-Silver/Portals/exercise/components/modal.tsx +++ b/src/course/02-lessons/02-Silver/Portals/exercise/components/modal.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import { useEffect, useRef } from 'react'; -// ๐Ÿ‘จ๐Ÿป๐Ÿ’ป 1B - import { createPortal } from 'react-dom'; +// ๐Ÿ’ป 1B - import { createPortal } from 'react-dom'; import FocusLock from 'react-focus-lock'; import { Button } from '@shared/components/Button/Button.component'; @@ -38,7 +38,7 @@ export const BattleOverlay = ({ event.preventDefault(); }; - // ๐Ÿ‘จ๐Ÿป๐Ÿ’ป 1C - call createPortal(battleOverlayCode, document.body); + // ๐Ÿ’ป 1C - call createPortal(battleOverlayCode, document.body); // ๐Ÿงช Test the storybook and look at how you can all of a sudden click the battle buttons // This isn't saying the solution to override z-index is to use portal but more of the sense that if you need something // put at the root of the DOM but do not wish to implement something extremely complex or app level then portal is handy for this. @@ -66,32 +66,37 @@ export const BattleOverlay = ({ >
    -

    +

    โš”๏ธ Wild Pokemon Battle!

    - + {!battleResult ? (
    - {wildPokemon.name}

    A wild {wildPokemon.name} appeared!

    -

    Level {wildPokemon.level}

    +

    + Level {wildPokemon.level} +

    - -
    )}
    -
    - {!battleResult && `What will you do against the wild ${wildPokemon.name}?`} +
    + {!battleResult && + `What will you do against the wild ${wildPokemon.name}?`}
    ); -}; \ No newline at end of file +}; diff --git a/src/course/02-lessons/02-Silver/Portals/exercise/exercise.tsx b/src/course/02-lessons/02-Silver/Portals/exercise/exercise.tsx index ed48546..68a05ab 100644 --- a/src/course/02-lessons/02-Silver/Portals/exercise/exercise.tsx +++ b/src/course/02-lessons/02-Silver/Portals/exercise/exercise.tsx @@ -2,11 +2,13 @@ import { useState } from 'react'; import { BattleOverlay } from './components/modal'; import { Button } from '@shared/components/Button/Button.component'; -// ๐Ÿ‘จ๐Ÿป๐Ÿ’ป 1A - have a look at the current implementation of the battle overlay and then go to components/modal.tsx +// ๐Ÿ’ป 1A - have a look at the current implementation of the battle overlay and then go to components/modal.tsx export const Exercise = () => { const [isBattleActive, setIsBattleActive] = useState(false); - const [battleResult, setBattleResult] = useState<'won' | 'fled' | null>(null); + const [battleResult, setBattleResult] = useState< + 'won' | 'fled' | null + >(null); const onCloseBattle = () => { setIsBattleActive(false); @@ -36,9 +38,11 @@ export const Exercise = () => { // the bug is 9998 and a css hack for the battle buttons is 9999
    - +
    -

    ๐ŸŒฟ Pokemon World

    +

    + ๐ŸŒฟ Pokemon World +

    @@ -56,13 +60,16 @@ export const Exercise = () => {
    -

    ๐ŸŽ’ Trainer Actions

    +

    + ๐ŸŽ’ Trainer Actions +

    - You're walking through the tall grass. Wild Pokemon might appear! + You're walking through the tall grass. Wild Pokemon might + appear!

    - +
    -
    ); -}; \ No newline at end of file +}; From 058349f955c6ee37da111e82958a7d1c71552c11 Mon Sep 17 00:00:00 2001 From: code-mattclaffey Date: Thu, 4 Sep 2025 14:28:41 +0100 Subject: [PATCH 17/29] feat: implement render props pokemon pattern --- src/course/01-introduction/01-Welcome.mdx | 2 + .../RenderProps/exercise/exercise.tsx | 166 ++++++------- .../02-Silver/RenderProps/final/final.tsx | 221 ++++++++++-------- .../02-Silver/RenderProps/lesson.mdx | 43 ++-- 4 files changed, 230 insertions(+), 202 deletions(-) diff --git a/src/course/01-introduction/01-Welcome.mdx b/src/course/01-introduction/01-Welcome.mdx index 56b3a3d..f041380 100644 --- a/src/course/01-introduction/01-Welcome.mdx +++ b/src/course/01-introduction/01-Welcome.mdx @@ -50,6 +50,8 @@ Each lesson is broken down in an exercise file and a final file. The exercise fi - [Compound components pattern](?path=/docs/lessons-๐Ÿฅˆ-Silver-compound-components-pattern-01-lesson--docs) - [Controlled component pattern](?path=/docs/lessons-๐Ÿฅˆ-Silver-controlled-components-pattern-01-lesson--docs) +- [FACC pattern](?path=/docs/lessons-๐Ÿฅˆ-silver-facc-pattern-01-lesson--docs) +- [Render children pattern](?path=/docs/lessons-๐Ÿฅˆ-silver-render-children-pattern-01-lesson--docs) - [Render props pattern](?path=/docs/lessons-๐Ÿฅˆ-Silver-render-props-pattern-01-lesson--docs) - [The Provider pattern](?path=/docs/lessons-๐Ÿฅˆ-Silver-provider-pattern-01-lesson--docs) - [The State Reducer pattern](?path=/docs/lessons-๐Ÿฅˆ-Silver-state-reducer-pattern-01-lesson--docs) diff --git a/src/course/02-lessons/02-Silver/RenderProps/exercise/exercise.tsx b/src/course/02-lessons/02-Silver/RenderProps/exercise/exercise.tsx index 62f783e..4485f12 100644 --- a/src/course/02-lessons/02-Silver/RenderProps/exercise/exercise.tsx +++ b/src/course/02-lessons/02-Silver/RenderProps/exercise/exercise.tsx @@ -1,106 +1,106 @@ -import { ChangeEvent, useState } from 'react'; -import { Input } from '@shared/components/Input/Input.component'; -import { Label } from '@shared/components/Label/Label.component'; -import { ErrorMessage } from '@shared/components/ErrorMessage/ErrorMessage.component'; - -export interface ITextInputFieldProps { - name: string; - id: string; - label: string; - required?: boolean; - errorMessage?: string; +interface ITypeEffectiveness { + attacking: string; + defending: string; + effectiveness: number; + description: string; } /* * Observations - * ๐Ÿ’… The current implementation uses the Controlled Component Pattern - * The UI is already split into small components + * ๐Ÿ’… Type effectiveness calculator is tightly coupled with table display + * Logic and presentation are mixed together * Tasks - * 1A ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป - Refactor the UI layer into its own component and setup the interface for its types to be: - * hasError: boolean; - * errorMessage?: string; - * id: string; - * name: string; - * label: string; - * input: HTMLAttributes & { required?: boolean }; - * - * 1B ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป - Add these new types to the TextInputField - * validate?: (value: string) => boolean; - * children: (props: ITextFieldProps) => React.ReactNode; - * - * 1C ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป - Replace the return of TextInput field with the children prop we have defined. - * ๐Ÿ’… You need to call children and pass down the props you need (the types above are the hint) + * 1A ๐Ÿ’ป - Add render prop to IPokemonTypeCalculatorProps: + * render: (effectiveness: ITypeEffectiveness[]) => React.ReactNode; * - * 1D ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป - In the Exercise component you want to add the UI component in as children. - * ๐Ÿ’… - The children should look like this {(props) => } + * 1B ๐Ÿ’ป - Replace the hardcoded table JSX with render prop call + * 1C ๐Ÿ’ป - In Exercise component, use render prop to display results */ -const validateTextString = (value: string) => - value.trim().length === 0; - -export const TextInputField = ({ - name, - label, - id, - required, - errorMessage -}: ITextInputFieldProps) => { - const [value, setValue] = useState(''); - const [hasError, setHasError] = useState(false); - const [isTouched, setIsTouched] = useState(false); - - const onChange = (event: ChangeEvent) => { - if (required) { - setHasError(validateTextString(event.target.value)); - } +interface IPokemonTypeCalculatorProps { + attackingType: string; +} - setValue(event.target.value); - }; +const typeChart: Record> = { + Fire: { Grass: 2, Water: 0.5, Fire: 0.5, Electric: 1, Ice: 2 }, + Water: { Fire: 2, Grass: 0.5, Water: 0.5, Electric: 1, Ice: 1 }, + Grass: { Water: 2, Fire: 0.5, Grass: 0.5, Electric: 1, Ice: 1 }, + Electric: { Water: 2, Fire: 1, Grass: 0.5, Electric: 0.5, Ice: 1 }, + Ice: { Grass: 2, Fire: 0.5, Water: 0.5, Electric: 1, Ice: 0.5 } +}; - const onFocus = () => { - if (isTouched) { - setHasError(false); - } +const getEffectivenessDescription = (value: number): string => { + if (value === 2) return 'Super Effective'; + if (value === 0.5) return 'Not Very Effective'; + return 'Normal Damage'; +}; - setIsTouched(true); - }; +export const PokemonTypeCalculator = ({ + attackingType +}: IPokemonTypeCalculatorProps) => { + const defendingTypes = Object.keys(typeChart); - const onBlur = () => { - if (value && validateTextString(value)) { - setHasError(true); - } - }; + const effectiveness: ITypeEffectiveness[] = defendingTypes.map( + (defendingType) => ({ + attacking: attackingType, + defending: defendingType, + effectiveness: typeChart[attackingType]?.[defendingType] ?? 1, + description: getEffectivenessDescription( + typeChart[attackingType]?.[defendingType] ?? 1 + ) + }) + ); return ( -
    - - - {errorMessage && hasError && ( - - )} +
    +

    + {attackingType} Type Effectiveness +

    +
    + + + + + + + + + + {effectiveness.map((item, index) => ( + + + + + + ))} + +
    Defending TypeMultiplierEffectiveness
    + {item.defending} + + + {item.effectiveness}x + + + {item.description} +
    +
    ); }; export const Exercise = () => { return ( -
    - - +
    + +
    ); }; diff --git a/src/course/02-lessons/02-Silver/RenderProps/final/final.tsx b/src/course/02-lessons/02-Silver/RenderProps/final/final.tsx index e22879c..5a88c19 100644 --- a/src/course/02-lessons/02-Silver/RenderProps/final/final.tsx +++ b/src/course/02-lessons/02-Silver/RenderProps/final/final.tsx @@ -1,119 +1,138 @@ -import { ChangeEvent, HTMLAttributes, useState } from 'react'; -import { Input } from '@shared/components/Input/Input.component'; -import { Label } from '@shared/components/Label/Label.component'; -import { ErrorMessage } from '@shared/components/ErrorMessage/ErrorMessage.component'; +import { ReactNode } from 'react'; -interface ITextFieldProps { - hasError: boolean; - errorMessage?: string; - id: string; - name: string; - label: string; - input: HTMLAttributes & { required?: boolean }; +interface ITypeEffectiveness { + attacking: string; + defending: string; + effectiveness: number; + description: string; } -interface IFieldProps { - name: string; - id: string; - label: string; - required?: boolean; - errorMessage?: string; - validate?: (value: string) => boolean; - children: (props: ITextFieldProps) => React.ReactNode; +interface IPokemonTypeCalculatorProps { + attackingType: string; + render: (effectiveness: ITypeEffectiveness[]) => ReactNode; } -const validateTextString = (value: string) => - value.trim().length === 0; +const typeChart: Record> = { + Fire: { Grass: 2, Water: 0.5, Fire: 0.5, Electric: 1, Ice: 2 }, + Water: { Fire: 2, Grass: 0.5, Water: 0.5, Electric: 1, Ice: 1 }, + Grass: { Water: 2, Fire: 0.5, Grass: 0.5, Electric: 1, Ice: 1 }, + Electric: { Water: 2, Fire: 1, Grass: 0.5, Electric: 0.5, Ice: 1 }, + Ice: { Grass: 2, Fire: 0.5, Water: 0.5, Electric: 1, Ice: 0.5 } +}; + +const getEffectivenessDescription = (value: number): string => { + if (value === 2) return 'Super Effective'; + if (value === 0.5) return 'Not Very Effective'; + return 'Normal Damage'; +}; -const TextFieldComponent = ({ - hasError, - errorMessage, - input, - id, - name, - label -}: ITextFieldProps) => ( -
    - - - {errorMessage && hasError && ( - - )} +// Table Display Component +const TableDisplay = (effectiveness: ITypeEffectiveness[]) => ( +
    +

    + {effectiveness[0]?.attacking} Type Effectiveness +

    +
    + + + + + + + + + + {effectiveness.map((item, index) => ( + + + + + + ))} + +
    Defending TypeMultiplierEffectiveness
    + {item.defending} + + + {item.effectiveness}x + + + {item.description} +
    +
    ); -export const Field = ({ - name, - label, - id, - required, - errorMessage, - validate, - children -}: IFieldProps) => { - const [value, setValue] = useState(''); - const [hasError, setHasError] = useState(false); - const [isTouched, setIsTouched] = useState(false); - - const onChange = (event: ChangeEvent) => { - if (required && validate) { - setHasError(validate(event.target.value)); - } - - setValue(event.target.value!); - }; - - const onFocus = () => { - if (isTouched) { - setHasError(false); - } +// Card Display Component +const CardDisplay = (effectiveness: ITypeEffectiveness[]) => ( +
    +

    + {effectiveness[0]?.attacking} vs All Types +

    +
    + {effectiveness.map((item, index) => ( +
    +
    {item.defending}
    +
    + {item.effectiveness}x - {item.description} +
    +
    + ))} +
    +
    +); - setIsTouched(true); - }; +export const PokemonTypeCalculator = ({ + attackingType, + render +}: IPokemonTypeCalculatorProps) => { + const defendingTypes = Object.keys(typeChart); - const onBlur = () => { - if (value && validate && validate(value)) { - setHasError(true); - } - }; + const effectiveness: ITypeEffectiveness[] = defendingTypes.map( + (defendingType) => ({ + attacking: attackingType, + defending: defendingType, + effectiveness: typeChart[attackingType]?.[defendingType] ?? 1, + description: getEffectivenessDescription( + typeChart[attackingType]?.[defendingType] ?? 1 + ) + }) + ); - return children({ - name, - label, - id, - errorMessage, - hasError, - input: { - required, - onBlur, - onFocus, - onChange - } - }); + return render(effectiveness); }; export const Final = () => { return ( -
    - - {({ name, label, id, errorMessage, hasError, input }) => ( - - )} - -
    +
    + {/* Table Display using render prop */} + TableDisplay(effectiveness)} + /> + + {/* Card Display using render prop */} + CardDisplay(effectiveness)} + /> +
    ); }; diff --git a/src/course/02-lessons/02-Silver/RenderProps/lesson.mdx b/src/course/02-lessons/02-Silver/RenderProps/lesson.mdx index b72aa24..9867a3f 100644 --- a/src/course/02-lessons/02-Silver/RenderProps/lesson.mdx +++ b/src/course/02-lessons/02-Silver/RenderProps/lesson.mdx @@ -4,35 +4,42 @@ import { Meta } from '@storybook/blocks'; # ๐ŸŽจ Render Props Pattern -Render props is a common pattern you will see in popular NPM packages like [Formik](https://formik.org/) or [React Final form](https://final-form.org/react) and it is very useful for building components that manage the logic and pass information to their children/prop so they can use that logic in the UI layer. +Render props is a pattern where you pass a function as a prop (typically called **render**) that returns a React element. The component calls this function with data, allowing complete control over rendering. -## With children +## Basic Example ```jsx -const HelloWorld = ({ children }) => children({ hello: 'world' }); - -// How you would use it. -{({ hello }) =>

    {hello}

    }
    ; +const PokemonStats = ({ render }) => { + const stats = { hp: 340, attack: 284 }; + return render(stats); +}; + +// Usage + ( +
    + HP: {hp}, Attack: {attack} +
    + )} +/>; ``` -## With props - -```jsx -const HelloWorld = ({ render }) => render({ hello: 'world' }); +## Exercise -// How you would use it. -

    {hello}

    } />; -``` +In this exercise we have a Pokemon type effectiveness calculator that's tightly coupled with a specific display format. Different teams want to use the calculation logic but with their own display components. -## Exercise +Your task is to refactor the component to use the render props pattern with a **render** prop, separating the calculation logic from the display. -In this exercise we are going to create a simplfied version of the Field component which is commonly used within the libraries specified above. What we have in the code is a component which mixes three UI components (Label, TextField, ErrorMessage) with app logic which is tightly coupled together. +Head over to the exercise file and let's begin. -The task: We have multiple teams that want to use that visual element for their applications but they do not align with the app logic as their apps behave differently. They all want the ability to use the UI logic alone so they can handle the app logic their way. +## Why use this pattern? -Our task is to refactor the UI out of the Field component and then pass props into the UI component using the render props pattern. +Render props is valuable for: -Head over to the exercise file and let's begin. +- **Explicit API**: Clear, named render prop instead of overloaded children +- **Reusable Logic**: Share complex calculations across different UI implementations +- **Library Design**: Common pattern in popular libraries like React Router and Formik +- **Flexible Rendering**: Complete control over how data is displayed ## Feedback From 9556cf67f2f740fb1656c95ef772cdba42c94258 Mon Sep 17 00:00:00 2001 From: code-mattclaffey Date: Thu, 4 Sep 2025 14:35:04 +0100 Subject: [PATCH 18/29] feat: implement the render children pattern --- .../exercise/exercise.stories.tsx | 15 ++ .../RenderChildren/exercise/exercise.tsx | 174 +++++++++++++++ .../RenderChildren/final/final.stories.tsx | 15 ++ .../02-Silver/RenderChildren/final/final.tsx | 211 ++++++++++++++++++ .../02-Silver/RenderChildren/lesson.mdx | 47 ++++ 5 files changed, 462 insertions(+) create mode 100644 src/course/02-lessons/02-Silver/RenderChildren/exercise/exercise.stories.tsx create mode 100644 src/course/02-lessons/02-Silver/RenderChildren/exercise/exercise.tsx create mode 100644 src/course/02-lessons/02-Silver/RenderChildren/final/final.stories.tsx create mode 100644 src/course/02-lessons/02-Silver/RenderChildren/final/final.tsx create mode 100644 src/course/02-lessons/02-Silver/RenderChildren/lesson.mdx diff --git a/src/course/02-lessons/02-Silver/RenderChildren/exercise/exercise.stories.tsx b/src/course/02-lessons/02-Silver/RenderChildren/exercise/exercise.stories.tsx new file mode 100644 index 0000000..e383510 --- /dev/null +++ b/src/course/02-lessons/02-Silver/RenderChildren/exercise/exercise.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Exercise } from './exercise'; + +const meta: Meta = { + title: 'Lessons/๐Ÿฅˆ Silver/๐Ÿ”„ Render Children Pattern/Exercise', + component: Exercise, + parameters: { + layout: 'centered', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; \ No newline at end of file diff --git a/src/course/02-lessons/02-Silver/RenderChildren/exercise/exercise.tsx b/src/course/02-lessons/02-Silver/RenderChildren/exercise/exercise.tsx new file mode 100644 index 0000000..ab300d3 --- /dev/null +++ b/src/course/02-lessons/02-Silver/RenderChildren/exercise/exercise.tsx @@ -0,0 +1,174 @@ +import { useState, useEffect } from 'react'; + +interface IPokemon { + id: number; + name: string; + type: string; + hp: number; +} + +/* + * Observations + * ๐Ÿ’… Search component has hardcoded display for all states + * Loading, success, and error displays are tightly coupled + + * Tasks + * 1A ๐Ÿ’ป - Add render props to IPokemonSearchProps: + * renderLoading: () => React.ReactNode; + * renderSuccess: (pokemon: IPokemon[]) => React.ReactNode; + * renderError: (error: string) => React.ReactNode; + * + * 1B ๐Ÿ’ป - Replace hardcoded JSX with render function calls + * 1C ๐Ÿ’ป - In Exercise component, provide render functions for each state +*/ + +interface IPokemonSearchProps { + searchTerm: string; +} + +const mockPokemon: IPokemon[] = [ + { id: 1, name: 'Pikachu', type: 'Electric', hp: 35 }, + { id: 4, name: 'Charmander', type: 'Fire', hp: 39 }, + { id: 7, name: 'Squirtle', type: 'Water', hp: 44 }, + { id: 25, name: 'Pichu', type: 'Electric', hp: 20 } +]; + +export const PokemonSearch = ({ + searchTerm +}: IPokemonSearchProps) => { + const [loading, setLoading] = useState(false); + const [pokemon, setPokemon] = useState([]); + const [error, setError] = useState(null); + + // Simulate API call + const searchPokemon = async (term: string) => { + setLoading(true); + setError(null); + + try { + await new Promise((resolve) => setTimeout(resolve, 1000)); + + if (term === 'error') { + throw new Error('Pokemon not found!'); + } + + const results = mockPokemon.filter((p) => + p.name.toLowerCase().includes(term.toLowerCase()) + ); + setPokemon(results); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + setLoading(false); + } + }; + + // Auto-search when searchTerm changes + useEffect(() => { + if (searchTerm) { + searchPokemon(searchTerm); + } + }, [searchTerm]); + + if (loading) { + return ( +
    +
    +
    +
    +
    +
    +
    +
    + Searching for Pokemon... +
    + ); + } + + if (error) { + return ( +
    +
    + +
    +

    + Search Error +

    +

    {error}

    +
    +
    +
    + ); + } + + return ( +
    +

    + Found {pokemon.length} Pokemon +

    +
      + {pokemon.map((p) => ( +
    • +
      + {p.name} + ({p.type}) +
      + HP: {p.hp} +
    • + ))} +
    +
    + ); +}; + +export const Exercise = () => { + const [searchTerm, setSearchTerm] = useState('pika'); + + return ( +
    + + setSearchTerm(e.target.value)} + placeholder="Search Pokemon..." + className="w-full p-2 border rounded" + aria-describedby="search-instructions" + /> +
    + Type to search for Pokemon by name +
    + +
    + +
    +
    + ); +}; diff --git a/src/course/02-lessons/02-Silver/RenderChildren/final/final.stories.tsx b/src/course/02-lessons/02-Silver/RenderChildren/final/final.stories.tsx new file mode 100644 index 0000000..abdf0e0 --- /dev/null +++ b/src/course/02-lessons/02-Silver/RenderChildren/final/final.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Final } from './final'; + +const meta: Meta = { + title: 'Lessons/๐Ÿฅˆ Silver/๐Ÿ”„ Render Children Pattern/Final', + component: Final, + parameters: { + layout: 'centered', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; \ No newline at end of file diff --git a/src/course/02-lessons/02-Silver/RenderChildren/final/final.tsx b/src/course/02-lessons/02-Silver/RenderChildren/final/final.tsx new file mode 100644 index 0000000..8cdac1c --- /dev/null +++ b/src/course/02-lessons/02-Silver/RenderChildren/final/final.tsx @@ -0,0 +1,211 @@ +import { ReactNode, useState, useEffect } from 'react'; + +interface IPokemon { + id: number; + name: string; + type: string; + hp: number; +} + +interface IPokemonSearchProps { + searchTerm: string; + renderLoading: () => ReactNode | ReactNode[]; + renderSuccess: (pokemon: IPokemon[]) => ReactNode | ReactNode[]; + renderError: (error: string) => ReactNode | ReactNode[]; +} + +const mockPokemon: IPokemon[] = [ + { id: 1, name: 'Pikachu', type: 'Electric', hp: 35 }, + { id: 4, name: 'Charmander', type: 'Fire', hp: 39 }, + { id: 7, name: 'Squirtle', type: 'Water', hp: 44 }, + { id: 25, name: 'Pichu', type: 'Electric', hp: 20 } +]; + +export const PokemonSearch = ({ + searchTerm, + renderLoading, + renderSuccess, + renderError +}: IPokemonSearchProps) => { + const [loading, setLoading] = useState(false); + const [pokemon, setPokemon] = useState([]); + const [error, setError] = useState(null); + + const searchPokemon = async (term: string) => { + setLoading(true); + setError(null); + + try { + await new Promise((resolve) => setTimeout(resolve, 1000)); + + if (term === 'error') { + throw new Error('Pokemon not found!'); + } + + const results = mockPokemon.filter((p) => + p.name.toLowerCase().includes(term.toLowerCase()) + ); + setPokemon(results); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (searchTerm) { + searchPokemon(searchTerm); + } + }, [searchTerm]); + + if (loading) return renderLoading(); + if (error) return renderError(error); + return renderSuccess(pokemon); +}; + +export const Final = () => { + const [searchTerm, setSearchTerm] = useState('pika'); + + return ( +
    + + setSearchTerm(e.target.value)} + placeholder="Search Pokemon..." + className="w-full p-2 border rounded" + aria-describedby="search-instructions-final" + /> +
    + Type to search for Pokemon by name +
    + + {/* Card Display */} + ( +
    +
    +
    +
    +
    +
    +
    +
    + Searching for Pokemon... +
    + )} + renderError={(error) => ( +
    +
    + +
    +

    + Search Error +

    +

    {error}

    +
    +
    +
    + )} + renderSuccess={(pokemon) => ( +
    +

    + Found {pokemon.length} Pokemon +

    +
      + {pokemon.map((p) => ( +
    • +
      + {p.name} + + ({p.type}) + +
      + HP: {p.hp} +
    • + ))} +
    +
    + )} + /> + + {/* List Display */} + ( +
    + + Searching... +
    + )} + renderError={(error) => ( +
    + + {error} +
    + )} + renderSuccess={(pokemon) => ( +
    +
      + {pokemon.map((p) => ( +
    • + + {p.name} ({p.type}) + + HP: {p.hp} +
    • + ))} +
    +
    + )} + /> +
    + ); +}; diff --git a/src/course/02-lessons/02-Silver/RenderChildren/lesson.mdx b/src/course/02-lessons/02-Silver/RenderChildren/lesson.mdx new file mode 100644 index 0000000..59ca77b --- /dev/null +++ b/src/course/02-lessons/02-Silver/RenderChildren/lesson.mdx @@ -0,0 +1,47 @@ +import { Meta } from '@storybook/blocks'; + + + +# ๐Ÿ”„ Render Children Pattern + +Render Children is a pattern where you pass a render function through a custom prop (not children). This gives you explicit control over the API and allows multiple render functions. + +## Basic Example + +```jsx +const PokemonLoader = ({ renderPokemon, renderLoading }) => { + const [pokemon, loading] = usePokemon(); + + if (loading) return renderLoading(); + return renderPokemon(pokemon); +}; + +// Usage +

    {pokemon.name}

    } + renderLoading={() =>

    Loading Pokemon...

    } +/>; +``` + +## Exercise + +In this exercise we have a Pokemon search component that handles loading, success, and error states but only supports one fixed display format. Different teams need different ways to display search results. + +Your task is to refactor the search component to use render children pattern with separate render functions for each state. + +Head over to the exercise file and let's begin. + +## Why use this pattern? + +Render Children is valuable for: + +- **Multiple Render Functions**: Support different render functions for different states +- **Explicit API**: Clear, named props instead of overloaded children +- **Conditional Rendering**: Easy to handle multiple UI states +- **Library Design**: Common in data fetching and state management libraries + +## Feedback + +Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you. + +[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new) From d3b3fe583fbc6ef45eddf0de86096fe2f620086b Mon Sep 17 00:00:00 2001 From: code-mattclaffey Date: Thu, 4 Sep 2025 14:43:44 +0100 Subject: [PATCH 19/29] feat: implement the FACC --- .../Controlled/exercise/exercise.tsx | 4 +- .../FACC/exercise/exercise.stories.tsx | 15 ++ .../02-Silver/FACC/exercise/exercise.tsx | 195 ++++++++++++++++++ .../02-Silver/FACC/final/final.stories.tsx | 15 ++ .../02-lessons/02-Silver/FACC/final/final.tsx | 178 ++++++++++++++++ .../02-lessons/02-Silver/FACC/lesson.mdx | 48 +++++ 6 files changed, 453 insertions(+), 2 deletions(-) create mode 100644 src/course/02-lessons/02-Silver/FACC/exercise/exercise.stories.tsx create mode 100644 src/course/02-lessons/02-Silver/FACC/exercise/exercise.tsx create mode 100644 src/course/02-lessons/02-Silver/FACC/final/final.stories.tsx create mode 100644 src/course/02-lessons/02-Silver/FACC/final/final.tsx create mode 100644 src/course/02-lessons/02-Silver/FACC/lesson.mdx diff --git a/src/course/02-lessons/02-Silver/Controlled/exercise/exercise.tsx b/src/course/02-lessons/02-Silver/Controlled/exercise/exercise.tsx index 50230cd..912c6a8 100644 --- a/src/course/02-lessons/02-Silver/Controlled/exercise/exercise.tsx +++ b/src/course/02-lessons/02-Silver/Controlled/exercise/exercise.tsx @@ -95,7 +95,7 @@ const EvolutionModal = ({ {/* โ™ฟ๏ธ Another requirement is to return focus to the actioner, but FocusLock does that for us when this component unmounts! ๐Ÿฆธ๐Ÿปโ™€๏ธ */}
    - {/* 2f - ๐Ÿ‘จ๐Ÿป๐Ÿ’ปโ™ฟ๏ธ Add id={`evolution_title_${id}`} - this creates the relationship between the title and modal */} + {/* 2f - ๐Ÿ’ป โ™ฟ๏ธ Add id={`evolution_title_${id}`} - this creates the relationship between the title and modal */}

    โœจ Evolution Time! โœจ

    @@ -141,7 +141,7 @@ const EvolutionModal = ({
    - {/* 2i - ๐Ÿ‘จ๐Ÿป๐Ÿ’ปโ™ฟ๏ธ Add id={`evolution_body_${id}`} - this creates the relationship between the content and modal */} + {/* 2i - ๐Ÿ’ป โ™ฟ๏ธ Add id={`evolution_body_${id}`} - this creates the relationship between the content and modal */}
    Your {pokemon.name} is ready to evolve into{' '} {evolution.name}! diff --git a/src/course/02-lessons/02-Silver/FACC/exercise/exercise.stories.tsx b/src/course/02-lessons/02-Silver/FACC/exercise/exercise.stories.tsx new file mode 100644 index 0000000..144db71 --- /dev/null +++ b/src/course/02-lessons/02-Silver/FACC/exercise/exercise.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Exercise } from './exercise'; + +const meta: Meta = { + title: 'Lessons/๐Ÿฅˆ Silver/๐ŸŽฏ FACC Pattern/Exercise', + component: Exercise, + parameters: { + layout: 'centered', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; \ No newline at end of file diff --git a/src/course/02-lessons/02-Silver/FACC/exercise/exercise.tsx b/src/course/02-lessons/02-Silver/FACC/exercise/exercise.tsx new file mode 100644 index 0000000..e0e670a --- /dev/null +++ b/src/course/02-lessons/02-Silver/FACC/exercise/exercise.tsx @@ -0,0 +1,195 @@ +import { useState } from 'react'; + +interface IPokemon { + name: string; + hp: number; + maxHp: number; + attack: number; +} + +interface IBattleState { + playerPokemon: IPokemon; + enemyPokemon: IPokemon; + turn: 'player' | 'enemy'; + battleLog: string[]; + winner: string | null; +} + +/* + * Observations + * ๐Ÿ’… Battle logic is tightly coupled with specific battle display + * The UI is hardcoded within the battle simulator + + * Tasks + * 1A ๐Ÿ’ป - Refactor to use FACC pattern by adding children prop: + * children: (battleState: IBattleState, actions: IBattleActions) => React.ReactNode; + * + * 1B ๐Ÿ’ป - Create IBattleActions interface with: + * attack: () => void; + * resetBattle: () => void; + * + * 1C ๐Ÿ’ป - Replace the JSX return with children function call + * 1D ๐Ÿ’ป - In Exercise component, use FACC to render battle display +*/ + +export const PokemonBattleSimulator = () => { + const [battleState, setBattleState] = useState({ + playerPokemon: { + name: 'Charizard', + hp: 100, + maxHp: 100, + attack: 25 + }, + enemyPokemon: { + name: 'Blastoise', + hp: 100, + maxHp: 100, + attack: 20 + }, + turn: 'player', + battleLog: [], + winner: null + }); + + const attack = () => { + if (battleState.winner) return; + + setBattleState((prev) => { + const newState = { ...prev }; + + if (prev.turn === 'player') { + const damage = prev.playerPokemon.attack; + newState.enemyPokemon.hp = Math.max( + 0, + prev.enemyPokemon.hp - damage + ); + newState.battleLog = [ + ...prev.battleLog, + `${prev.playerPokemon.name} attacks for ${damage} damage!` + ]; + + if (newState.enemyPokemon.hp === 0) { + newState.winner = prev.playerPokemon.name; + } else { + newState.turn = 'enemy'; + } + } else { + const damage = prev.enemyPokemon.attack; + newState.playerPokemon.hp = Math.max( + 0, + prev.playerPokemon.hp - damage + ); + newState.battleLog = [ + ...prev.battleLog, + `${prev.enemyPokemon.name} attacks for ${damage} damage!` + ]; + + if (newState.playerPokemon.hp === 0) { + newState.winner = prev.enemyPokemon.name; + } else { + newState.turn = 'player'; + } + } + + return newState; + }); + }; + + const resetBattle = () => { + setBattleState({ + playerPokemon: { + name: 'Charizard', + hp: 100, + maxHp: 100, + attack: 25 + }, + enemyPokemon: { + name: 'Blastoise', + hp: 100, + maxHp: 100, + attack: 20 + }, + turn: 'player', + battleLog: [], + winner: null + }); + }; + + return ( +
    +

    Pokemon Battle

    + +
    +
    +

    + {battleState.playerPokemon.name} +

    +
    +
    +
    +

    + {battleState.playerPokemon.hp}/ + {battleState.playerPokemon.maxHp} HP +

    +
    + +
    +

    + {battleState.enemyPokemon.name} +

    +
    +
    +
    +

    + {battleState.enemyPokemon.hp}/ + {battleState.enemyPokemon.maxHp} HP +

    +
    +
    + +
    + + +
    + + {battleState.winner && ( +
    +

    {battleState.winner} wins!

    +
    + )} + +
    + {battleState.battleLog.map((log, index) => ( +

    + {log} +

    + ))} +
    +
    + ); +}; + +export const Exercise = () => { + return ; +}; diff --git a/src/course/02-lessons/02-Silver/FACC/final/final.stories.tsx b/src/course/02-lessons/02-Silver/FACC/final/final.stories.tsx new file mode 100644 index 0000000..ce4c663 --- /dev/null +++ b/src/course/02-lessons/02-Silver/FACC/final/final.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Final } from './final'; + +const meta: Meta = { + title: 'Lessons/๐Ÿฅˆ Silver/๐ŸŽฏ FACC Pattern/Final', + component: Final, + parameters: { + layout: 'centered', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; \ No newline at end of file diff --git a/src/course/02-lessons/02-Silver/FACC/final/final.tsx b/src/course/02-lessons/02-Silver/FACC/final/final.tsx new file mode 100644 index 0000000..6e6af3c --- /dev/null +++ b/src/course/02-lessons/02-Silver/FACC/final/final.tsx @@ -0,0 +1,178 @@ +import { ReactNode, useState } from 'react'; + +interface IPokemon { + name: string; + hp: number; + maxHp: number; + attack: number; +} + +interface IBattleState { + playerPokemon: IPokemon; + enemyPokemon: IPokemon; + turn: 'player' | 'enemy'; + battleLog: string[]; + winner: string | null; +} + +interface IBattleActions { + attack: () => void; + resetBattle: () => void; +} + +interface IPokemonBattleSimulatorProps { + children: (battleState: IBattleState, actions: IBattleActions) => ReactNode; +} + +// Classic Battle Display +const ClassicBattleDisplay = (battleState: IBattleState, actions: IBattleActions) => ( +
    +

    Pokemon Battle

    + +
    +
    +

    {battleState.playerPokemon.name}

    +
    +
    +
    +

    {battleState.playerPokemon.hp}/{battleState.playerPokemon.maxHp} HP

    +
    + +
    +

    {battleState.enemyPokemon.name}

    +
    +
    +
    +

    {battleState.enemyPokemon.hp}/{battleState.enemyPokemon.maxHp} HP

    +
    +
    + +
    + + +
    + + {battleState.winner && ( +
    +

    {battleState.winner} wins!

    +
    + )} + +
    + {battleState.battleLog.map((log, index) => ( +

    {log}

    + ))} +
    +
    +); + +// Minimal Battle Display +const MinimalBattleDisplay = (battleState: IBattleState, actions: IBattleActions) => ( +
    +
    + {battleState.playerPokemon.name}: {battleState.playerPokemon.hp} + VS + {battleState.enemyPokemon.name}: {battleState.enemyPokemon.hp} +
    + + {battleState.winner ? ( +
    +

    {battleState.winner} wins!

    + +
    + ) : ( + + )} +
    +); + +export const PokemonBattleSimulator = ({ children }: IPokemonBattleSimulatorProps) => { + const [battleState, setBattleState] = useState({ + playerPokemon: { name: 'Charizard', hp: 100, maxHp: 100, attack: 25 }, + enemyPokemon: { name: 'Blastoise', hp: 100, maxHp: 100, attack: 20 }, + turn: 'player', + battleLog: [], + winner: null + }); + + const attack = () => { + if (battleState.winner) return; + + setBattleState(prev => { + const newState = { ...prev }; + + if (prev.turn === 'player') { + const damage = prev.playerPokemon.attack; + newState.enemyPokemon.hp = Math.max(0, prev.enemyPokemon.hp - damage); + newState.battleLog = [...prev.battleLog, `${prev.playerPokemon.name} attacks for ${damage} damage!`]; + + if (newState.enemyPokemon.hp === 0) { + newState.winner = prev.playerPokemon.name; + } else { + newState.turn = 'enemy'; + } + } else { + const damage = prev.enemyPokemon.attack; + newState.playerPokemon.hp = Math.max(0, prev.playerPokemon.hp - damage); + newState.battleLog = [...prev.battleLog, `${prev.enemyPokemon.name} attacks for ${damage} damage!`]; + + if (newState.playerPokemon.hp === 0) { + newState.winner = prev.enemyPokemon.name; + } else { + newState.turn = 'player'; + } + } + + return newState; + }); + }; + + const resetBattle = () => { + setBattleState({ + playerPokemon: { name: 'Charizard', hp: 100, maxHp: 100, attack: 25 }, + enemyPokemon: { name: 'Blastoise', hp: 100, maxHp: 100, attack: 20 }, + turn: 'player', + battleLog: [], + winner: null + }); + }; + + return children(battleState, { attack, resetBattle }); +}; + +export const Final = () => { + return ( +
    + {/* Classic Display using FACC */} + + {(battleState, actions) => ClassicBattleDisplay(battleState, actions)} + + + {/* Minimal Display using FACC */} + + {(battleState, actions) => MinimalBattleDisplay(battleState, actions)} + +
    + ); +}; \ No newline at end of file diff --git a/src/course/02-lessons/02-Silver/FACC/lesson.mdx b/src/course/02-lessons/02-Silver/FACC/lesson.mdx new file mode 100644 index 0000000..ea24b0c --- /dev/null +++ b/src/course/02-lessons/02-Silver/FACC/lesson.mdx @@ -0,0 +1,48 @@ +import { Meta } from '@storybook/blocks'; + + + +# ๐ŸŽฏ FACC (Function as Child Component) Pattern + +FACC is a pattern where you pass a function as the children prop. The component calls this function with data, allowing complete control over rendering while sharing logic. + +## Basic Example + +```jsx +const PokemonData = ({ children }) => { + const pokemon = { name: 'Pikachu', type: 'Electric' }; + return children(pokemon); +}; + +// Usage + + {(pokemon) => ( +

    + {pokemon.name} is {pokemon.type} type! +

    + )} +
    ; +``` + +## Exercise + +In this exercise we have a Pokemon battle simulator that's tightly coupled with a specific battle display. Different teams want to use the battle logic but with their own custom battle interfaces. + +Your task is to refactor the battle simulator to use FACC pattern, separating the battle logic from the display components. + +Head over to the exercise file and let's begin. + +## Why use this pattern? + +FACC is valuable for: + +- **Clean API**: Uses the natural children prop instead of custom render props +- **Flexible Rendering**: Complete control over how data is displayed +- **Logic Reuse**: Share complex state management across different UIs +- **Component Libraries**: Create headless components that work with any design system + +## Feedback + +Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you. + +[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new) From 962cf312d881af0b608c9a5e84064f1184d5d766 Mon Sep 17 00:00:00 2001 From: code-mattclaffey Date: Thu, 4 Sep 2025 16:21:51 +0100 Subject: [PATCH 20/29] feat: implement uncontrolled components --- src/course/01-introduction/01-Welcome.mdx | 1 + .../exercise/exercise.stories.tsx | 15 ++ .../Uncontrolled/exercise/exercise.tsx | 153 ++++++++++++++++ .../Uncontrolled/final/final.stories.tsx | 15 ++ .../02-Silver/Uncontrolled/final/final.tsx | 165 ++++++++++++++++++ .../02-Silver/Uncontrolled/lesson.mdx | 70 ++++++++ 6 files changed, 419 insertions(+) create mode 100644 src/course/02-lessons/02-Silver/Uncontrolled/exercise/exercise.stories.tsx create mode 100644 src/course/02-lessons/02-Silver/Uncontrolled/exercise/exercise.tsx create mode 100644 src/course/02-lessons/02-Silver/Uncontrolled/final/final.stories.tsx create mode 100644 src/course/02-lessons/02-Silver/Uncontrolled/final/final.tsx create mode 100644 src/course/02-lessons/02-Silver/Uncontrolled/lesson.mdx diff --git a/src/course/01-introduction/01-Welcome.mdx b/src/course/01-introduction/01-Welcome.mdx index f041380..4fa9512 100644 --- a/src/course/01-introduction/01-Welcome.mdx +++ b/src/course/01-introduction/01-Welcome.mdx @@ -50,6 +50,7 @@ Each lesson is broken down in an exercise file and a final file. The exercise fi - [Compound components pattern](?path=/docs/lessons-๐Ÿฅˆ-Silver-compound-components-pattern-01-lesson--docs) - [Controlled component pattern](?path=/docs/lessons-๐Ÿฅˆ-Silver-controlled-components-pattern-01-lesson--docs) +- [Uncontrolled components pattern](?path=/docs/lessons-๐Ÿฅˆ-silver-uncontrolled-components-01-lesson--docs) - [FACC pattern](?path=/docs/lessons-๐Ÿฅˆ-silver-facc-pattern-01-lesson--docs) - [Render children pattern](?path=/docs/lessons-๐Ÿฅˆ-silver-render-children-pattern-01-lesson--docs) - [Render props pattern](?path=/docs/lessons-๐Ÿฅˆ-Silver-render-props-pattern-01-lesson--docs) diff --git a/src/course/02-lessons/02-Silver/Uncontrolled/exercise/exercise.stories.tsx b/src/course/02-lessons/02-Silver/Uncontrolled/exercise/exercise.stories.tsx new file mode 100644 index 0000000..9ef78b2 --- /dev/null +++ b/src/course/02-lessons/02-Silver/Uncontrolled/exercise/exercise.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Exercise } from './exercise'; + +const meta: Meta = { + title: 'Lessons/๐Ÿฅˆ Silver/๐ŸŽฎ Uncontrolled Components/Exercise', + component: Exercise, + parameters: { + layout: 'centered', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; \ No newline at end of file diff --git a/src/course/02-lessons/02-Silver/Uncontrolled/exercise/exercise.tsx b/src/course/02-lessons/02-Silver/Uncontrolled/exercise/exercise.tsx new file mode 100644 index 0000000..28c7e78 --- /dev/null +++ b/src/course/02-lessons/02-Silver/Uncontrolled/exercise/exercise.tsx @@ -0,0 +1,153 @@ +import { useState } from 'react'; + +interface IPokemonTeam { + trainerName: string; + teamName: string; + pokemon1: string; + pokemon2: string; + pokemon3: string; +} + +/* + * Observations + * ๐Ÿ’… Form uses controlled components with lots of state management + * Every input change triggers a re-render + * Lots of boilerplate for simple form handling + + * Tasks + * 1A ๐Ÿ‘จ๐Ÿป๐Ÿ’ป - Replace useState with useRef for each form field + * 1B ๐Ÿ‘จ๐Ÿป๐Ÿ’ป - Remove onChange handlers and use defaultValue instead of value + * 1C ๐Ÿ‘จ๐Ÿป๐Ÿ’ป - Update handleSubmit to read values from refs + * 1D ๐Ÿ‘จ๐Ÿป๐Ÿ’ป - Remove all state-related code +*/ + +export const PokemonTeamRegistration = () => { + const [trainerName, setTrainerName] = useState(''); + const [teamName, setTeamName] = useState(''); + const [pokemon1, setPokemon1] = useState(''); + const [pokemon2, setPokemon2] = useState(''); + const [pokemon3, setPokemon3] = useState(''); + const [submittedTeam, setSubmittedTeam] = useState(null); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + const team: IPokemonTeam = { + trainerName, + teamName, + pokemon1, + pokemon2, + pokemon3 + }; + + setSubmittedTeam(team); + + // Reset form + setTrainerName(''); + setTeamName(''); + setPokemon1(''); + setPokemon2(''); + setPokemon3(''); + }; + + return ( +
    +

    Pokemon Team Registration

    + +
    +
    + + setTrainerName(e.target.value)} + className="w-full p-2 border rounded" + required + /> +
    + +
    + + setTeamName(e.target.value)} + className="w-full p-2 border rounded" + required + /> +
    + +
    + + setPokemon1(e.target.value)} + className="w-full p-2 border rounded" + placeholder="e.g., Pikachu" + required + /> +
    + +
    + + setPokemon2(e.target.value)} + className="w-full p-2 border rounded" + placeholder="e.g., Charizard" + required + /> +
    + +
    + + setPokemon3(e.target.value)} + className="w-full p-2 border rounded" + placeholder="e.g., Blastoise" + required + /> +
    + + +
    + + {submittedTeam && ( +
    +

    Team Registered!

    +

    Trainer: {submittedTeam.trainerName}

    +

    Team: {submittedTeam.teamName}

    +

    Pokemon: {submittedTeam.pokemon1}, {submittedTeam.pokemon2}, {submittedTeam.pokemon3}

    +
    + )} +
    + ); +}; + +export const Exercise = () => { + return ; +}; \ No newline at end of file diff --git a/src/course/02-lessons/02-Silver/Uncontrolled/final/final.stories.tsx b/src/course/02-lessons/02-Silver/Uncontrolled/final/final.stories.tsx new file mode 100644 index 0000000..3fce477 --- /dev/null +++ b/src/course/02-lessons/02-Silver/Uncontrolled/final/final.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Final } from './final'; + +const meta: Meta = { + title: 'Lessons/๐Ÿฅˆ Silver/๐ŸŽฎ Uncontrolled Components/Final', + component: Final, + parameters: { + layout: 'centered', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; \ No newline at end of file diff --git a/src/course/02-lessons/02-Silver/Uncontrolled/final/final.tsx b/src/course/02-lessons/02-Silver/Uncontrolled/final/final.tsx new file mode 100644 index 0000000..202b86b --- /dev/null +++ b/src/course/02-lessons/02-Silver/Uncontrolled/final/final.tsx @@ -0,0 +1,165 @@ +import { useState } from 'react'; + +interface IPokemonTeam { + trainerName: string; + teamName: string; + pokemon1: string; + pokemon2: string; + pokemon3: string; +} + +export const PokemonTeamRegistration = () => { + // Only keeping state for the submitted team display + const [submittedTeam, setSubmittedTeam] = + useState(null); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + // Using FormData - still uncontrolled! + const formData = new FormData(e.currentTarget); + const team: IPokemonTeam = { + trainerName: formData.get('trainerName') as string, + teamName: formData.get('teamName') as string, + pokemon1: formData.get('pokemon1') as string, + pokemon2: formData.get('pokemon2') as string, + pokemon3: formData.get('pokemon3') as string + }; + + setSubmittedTeam(team); + + // Reset form using built-in method + e.currentTarget.reset(); + }; + + return ( +
    +

    + Pokemon Team Registration +

    + +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + + +
    + + {submittedTeam && ( +
    +

    + Team Registered! +

    +

    + Trainer: {submittedTeam.trainerName} +

    +

    + Team: {submittedTeam.teamName} +

    +

    + Pokemon: {submittedTeam.pokemon1},{' '} + {submittedTeam.pokemon2}, {submittedTeam.pokemon3} +

    +
    + )} +
    + ); +}; + +export const Final = () => { + return ( +
    + +
    + ); +}; diff --git a/src/course/02-lessons/02-Silver/Uncontrolled/lesson.mdx b/src/course/02-lessons/02-Silver/Uncontrolled/lesson.mdx new file mode 100644 index 0000000..72362cc --- /dev/null +++ b/src/course/02-lessons/02-Silver/Uncontrolled/lesson.mdx @@ -0,0 +1,70 @@ +import { Meta } from '@storybook/blocks'; + + + +# ๐ŸŽฎ Uncontrolled Components + +Uncontrolled components manage their own state internally using refs instead of React state. The DOM itself becomes the "source of truth" for the form data, making them useful for simple forms or when integrating with non-React code. + +## Basic Example + +```jsx +// Using refs +const PokemonNameForm = () => { + const nameRef = useRef(); + + const handleSubmit = (e) => { + e.preventDefault(); + console.log('Pokemon name:', nameRef.current.value); + }; + + return ( +
    + + +
    + ); +}; + +// Using FormData (modern approach) +const PokemonNameForm = () => { + const handleSubmit = (e) => { + e.preventDefault(); + const formData = new FormData(e.target); + console.log('Pokemon name:', formData.get('pokemonName')); + }; + + return ( +
    + + +
    + ); +}; +``` + +## Exercise + +In this exercise we have a Pokemon team registration form that uses controlled components with lots of state management. For simple forms like this, uncontrolled components can reduce complexity and boilerplate code. + +Your task is to refactor the form to use uncontrolled components with refs, removing the need for state management while maintaining the same functionality. + +Head over to the exercise file and let's begin. + +## When to use this pattern? + +**Use uncontrolled components for:** +- **Simple Forms**: Basic forms without complex validation +- **Performance**: When you want to avoid re-renders on input changes +- **Third-party Integration**: Easier integration with non-React libraries +- **Default Values**: When you just need initial form values + +**Avoid uncontrolled components when:** +- **Real-time Validation**: You need to validate as users type +- **Dynamic UI**: Form fields depend on other field values +- **Complex UX**: Conditional enabling/disabling of form elements +- **Immediate Feedback**: You need to show live character counts, formatting, etc. + +## Feedback + +[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new) \ No newline at end of file From a6a6f766e420affa87de7918f403e594d43b8f72 Mon Sep 17 00:00:00 2001 From: code-mattclaffey Date: Thu, 4 Sep 2025 20:15:41 +0100 Subject: [PATCH 21/29] feat: implement suspense --- package-lock.json | 1734 ++++++++++------- package.json | 10 +- src/course/01-introduction/01-Welcome.mdx | 1 + .../Suspense/exercise/exercise.stories.tsx | 15 + .../03-Gold/Suspense/exercise/exercise.tsx | 185 ++ .../final/components/BlastoiseDetails.tsx | 41 + .../final/components/CharizardDetails.tsx | 41 + .../final/components/PikachuDetails.tsx | 41 + .../03-Gold/Suspense/final/final.stories.tsx | 15 + .../03-Gold/Suspense/final/final.tsx | 106 + .../02-lessons/03-Gold/Suspense/lesson.mdx | 47 + .../03-Gold/Suspense/utils/PokemonLoader.tsx | 23 + .../03-Gold/Suspense/utils/delay.ts | 2 + 13 files changed, 1579 insertions(+), 682 deletions(-) create mode 100644 src/course/02-lessons/03-Gold/Suspense/exercise/exercise.stories.tsx create mode 100644 src/course/02-lessons/03-Gold/Suspense/exercise/exercise.tsx create mode 100644 src/course/02-lessons/03-Gold/Suspense/final/components/BlastoiseDetails.tsx create mode 100644 src/course/02-lessons/03-Gold/Suspense/final/components/CharizardDetails.tsx create mode 100644 src/course/02-lessons/03-Gold/Suspense/final/components/PikachuDetails.tsx create mode 100644 src/course/02-lessons/03-Gold/Suspense/final/final.stories.tsx create mode 100644 src/course/02-lessons/03-Gold/Suspense/final/final.tsx create mode 100644 src/course/02-lessons/03-Gold/Suspense/lesson.mdx create mode 100644 src/course/02-lessons/03-Gold/Suspense/utils/PokemonLoader.tsx create mode 100644 src/course/02-lessons/03-Gold/Suspense/utils/delay.ts diff --git a/package-lock.json b/package-lock.json index 39050e8..cdde24a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,9 @@ "dependencies": { "@vercel/analytics": "^1.3.1", "classnames": "^2.5.1", - "react": "^18.3.1", + "react": "^19.0.0", "react-code-blocks": "^0.1.6", - "react-dom": "^18.3.1", + "react-dom": "^19.0.0", "react-focus-lock": "^2.12.1" }, "devDependencies": { @@ -29,8 +29,8 @@ "@storybook/test": "^8.4.7", "@storybook/test-runner": "^0.19.0", "@storybook/theming": "^8.4.7", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", "@typescript-eslint/eslint-plugin": "^7.13.1", "@typescript-eslint/parser": "^7.13.1", "@vitejs/plugin-react": "^4.3.1", @@ -46,7 +46,7 @@ "storybook": "^8.4.7", "tailwindcss": "^3.4.5", "typescript": "^5.2.2", - "vite": "^5.3.1" + "vite": "^7.1.4" } }, "node_modules/@adobe/css-tools": { @@ -97,30 +97,32 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", - "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", - "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, + "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helpers": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/template": "^7.24.7", - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.3", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -145,29 +147,32 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", - "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.24.7", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", - "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "browserslist": "^4.22.2", + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -180,71 +185,45 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", - "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", - "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", - "dev": true, - "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", - "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "dev": true, - "dependencies": { - "@babel/types": "^7.24.7" - }, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", - "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -254,35 +233,11 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", - "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", - "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "dev": true, - "dependencies": { - "@babel/types": "^7.24.7" - }, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -308,36 +263,37 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", - "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", - "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", + "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", - "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.28.2" }, "bin": { "parser": "bin/babel-parser.js" @@ -524,12 +480,13 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", - "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -539,12 +496,13 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", - "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -578,30 +536,28 @@ } }, "node_modules/@babel/traverse": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", - "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7", - "debug": "^4.3.1", - "globals": "^11.1.0" + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -666,6 +622,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "license": "MIT", "dependencies": { "@emotion/memoize": "^0.8.1" } @@ -673,379 +630,455 @@ "node_modules/@emotion/memoize": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" }, "node_modules/@emotion/unitless": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", - "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -1096,10 +1129,11 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1172,10 +1206,11 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2073,51 +2108,15 @@ "node": ">=8" } }, - "node_modules/@joshwooding/vite-plugin-react-docgen-typescript": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.4.2.tgz", - "integrity": "sha512-feQ+ntr+8hbVudnsTUapiMN9q8T90XA1d5jn9QzY09sNoj4iD9wi0PY1vsBFTda4ZjEaxRK9S81oarR2nj7TFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "magic-string": "^0.27.0", - "react-docgen-typescript": "^2.2.2" - }, - "peerDependencies": { - "typescript": ">= 4.3.x", - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@joshwooding/vite-plugin-react-docgen-typescript/node_modules/magic-string": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", - "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.13" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -2129,15 +2128,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -2146,10 +2136,11 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -2217,6 +2208,13 @@ "node": ">=14" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/pluginutils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", @@ -2240,247 +2238,294 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.1.tgz", - "integrity": "sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.0.tgz", + "integrity": "sha512-lVgpeQyy4fWN5QYebtW4buT/4kn4p4IJ+kDNB4uYNT5b8c8DLJDg6titg20NIg7E8RWwdWZORW6vUFfrLyG3KQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.29.1.tgz", - "integrity": "sha512-CaRfrV0cd+NIIcVVN/jx+hVLN+VRqnuzLRmfmlzpOzB87ajixsN/+9L5xNmkaUUvEbI5BmIKS+XTwXsHEb65Ew==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.0.tgz", + "integrity": "sha512-2O73dR4Dc9bp+wSYhviP6sDziurB5/HCym7xILKifWdE9UsOe2FtNcM+I4xZjKrfLJnq5UR8k9riB87gauiQtw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.29.1.tgz", - "integrity": "sha512-2ORr7T31Y0Mnk6qNuwtyNmy14MunTAMx06VAPI6/Ju52W10zk1i7i5U3vlDRWjhOI5quBcrvhkCHyF76bI7kEw==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.0.tgz", + "integrity": "sha512-vwSXQN8T4sKf1RHr1F0s98Pf8UPz7pS6P3LG9NSmuw0TVh7EmaE+5Ny7hJOZ0M2yuTctEsHHRTMi2wuHkdS6Hg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.29.1.tgz", - "integrity": "sha512-j/Ej1oanzPjmN0tirRd5K2/nncAhS9W6ICzgxV+9Y5ZsP0hiGhHJXZ2JQ53iSSjj8m6cRY6oB1GMzNn2EUt6Ng==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.0.tgz", + "integrity": "sha512-cQp/WG8HE7BCGyFVuzUg0FNmupxC+EPZEwWu2FCGGw5WDT1o2/YlENbm5e9SMvfDFR6FRhVCBePLqj0o8MN7Vw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.29.1.tgz", - "integrity": "sha512-91C//G6Dm/cv724tpt7nTyP+JdN12iqeXGFM1SqnljCmi5yTXriH7B1r8AD9dAZByHpKAumqP1Qy2vVNIdLZqw==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.0.tgz", + "integrity": "sha512-UR1uTJFU/p801DvvBbtDD7z9mQL8J80xB0bR7DqW7UGQHRm/OaKzp4is7sQSdbt2pjjSS72eAtRh43hNduTnnQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.29.1.tgz", - "integrity": "sha512-hEioiEQ9Dec2nIRoeHUP6hr1PSkXzQaCUyqBDQ9I9ik4gCXQZjJMIVzoNLBRGet+hIUb3CISMh9KXuCcWVW/8w==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.0.tgz", + "integrity": "sha512-G/DKyS6PK0dD0+VEzH/6n/hWDNPDZSMBmqsElWnCRGrYOb2jC0VSupp7UAHHQ4+QILwkxSMaYIbQ72dktp8pKA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.29.1.tgz", - "integrity": "sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.0.tgz", + "integrity": "sha512-u72Mzc6jyJwKjJbZZcIYmd9bumJu7KNmHYdue43vT1rXPm2rITwmPWF0mmPzLm9/vJWxIRbao/jrQmxTO0Sm9w==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.29.1.tgz", - "integrity": "sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.0.tgz", + "integrity": "sha512-S4UefYdV0tnynDJV1mdkNawp0E5Qm2MtSs330IyHgaccOFrwqsvgigUD29uT+B/70PDY1eQ3t40+xf6wIvXJyg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.29.1.tgz", - "integrity": "sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.0.tgz", + "integrity": "sha512-1EhkSvUQXJsIhk4msxP5nNAUWoB4MFDHhtc4gAYvnqoHlaL9V3F37pNHabndawsfy/Tp7BPiy/aSa6XBYbaD1g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.29.1.tgz", - "integrity": "sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.0.tgz", + "integrity": "sha512-EtBDIZuDtVg75xIPIK1l5vCXNNCIRM0OBPUG+tbApDuJAy9mKago6QxX+tfMzbCI6tXEhMuZuN1+CU8iDW+0UQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.29.1.tgz", - "integrity": "sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.0.tgz", + "integrity": "sha512-BGYSwJdMP0hT5CCmljuSNx7+k+0upweM2M4YGfFBjnFSZMHOLYR0gEEj/dxyYJ6Zc6AiSeaBY8dWOa11GF/ppQ==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.29.1.tgz", - "integrity": "sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.0.tgz", + "integrity": "sha512-I1gSMzkVe1KzAxKAroCJL30hA4DqSi+wGc5gviD0y3IL/VkvcnAqwBf4RHXHyvH66YVHxpKO8ojrgc4SrWAnLg==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.29.1.tgz", - "integrity": "sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.0.tgz", + "integrity": "sha512-bSbWlY3jZo7molh4tc5dKfeSxkqnf48UsLqYbUhnkdnfgZjgufLS/NTA8PcP/dnvct5CCdNkABJ56CbclMRYCA==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.29.1.tgz", - "integrity": "sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g==", + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.0.tgz", + "integrity": "sha512-LSXSGumSURzEQLT2e4sFqFOv3LWZsEF8FK7AAv9zHZNDdMnUPYH3t8ZlaeYYZyTXnsob3htwTKeWtBIkPV27iQ==", "cpu": [ - "s390x" + "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.29.1.tgz", - "integrity": "sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ==", + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.0.tgz", + "integrity": "sha512-CxRKyakfDrsLXiCyucVfVWVoaPA4oFSpPpDwlMcDFQvrv3XY6KEzMtMZrA+e/goC8xxp2WSOxHQubP8fPmmjOQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.0.tgz", + "integrity": "sha512-8PrJJA7/VU8ToHVEPu14FzuSAqVKyo5gg/J8xUerMbyNkWkO9j2ExBho/68RnJsMGNJq4zH114iAttgm7BZVkA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.29.1.tgz", - "integrity": "sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.0.tgz", + "integrity": "sha512-SkE6YQp+CzpyOrbw7Oc4MgXFvTw2UIBElvAvLCo230pyxOLmYwRPwZ/L5lBe/VW/qT1ZgND9wJfOsdy0XptRvw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.0.tgz", + "integrity": "sha512-PZkNLPfvXeIOgJWA804zjSFH7fARBBCpCXxgkGDRjjAhRLOR8o0IGS01ykh5GYfod4c2yiiREuDM8iZ+pVsT+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.29.1.tgz", - "integrity": "sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.0.tgz", + "integrity": "sha512-q7cIIdFvWQoaCbLDUyUc8YfR3Jh2xx3unO8Dn6/TTogKjfwrax9SyfmGGK6cQhKtjePI7jRfd7iRYcxYs93esg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.29.1.tgz", - "integrity": "sha512-rYRe5S0FcjlOBZQHgbTKNrqxCBUmgDJem/VQTCcTnA2KCabYSWQDrytOzX7avb79cAAweNmMUb/Zw18RNd4mng==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.0.tgz", + "integrity": "sha512-XzNOVg/YnDOmFdDKcxxK410PrcbcqZkBmz+0FicpW5jtjKQxcW1BZJEQOF0NJa6JO7CZhett8GEtRN/wYLYJuw==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.29.1.tgz", - "integrity": "sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.0.tgz", + "integrity": "sha512-xMmiWRR8sp72Zqwjgtf3QbZfF1wdh8X2ABu3EaozvZcyHJeU0r+XAnXdKgs4cCAp6ORoYoCygipYP1mjmbjrsg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -2609,6 +2654,72 @@ "storybook": "^8.4.7" } }, + "node_modules/@storybook/addon-docs/node_modules/@storybook/blocks": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-8.4.7.tgz", + "integrity": "sha512-+QH7+JwXXXIyP3fRCxz/7E2VZepAanXJM7G8nbR3wWsqWgrRp4Wra6MvybxAYCxU7aNfJX5c+RW84SNikFpcIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/csf": "^0.1.11", + "@storybook/icons": "^1.2.12", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^8.4.7" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@storybook/addon-docs/node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@storybook/addon-docs/node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/@storybook/addon-docs/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/@storybook/addon-essentials": { "version": "8.4.7", "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-8.4.7.tgz", @@ -2795,12 +2906,12 @@ } }, "node_modules/@storybook/blocks": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-8.4.7.tgz", - "integrity": "sha512-+QH7+JwXXXIyP3fRCxz/7E2VZepAanXJM7G8nbR3wWsqWgrRp4Wra6MvybxAYCxU7aNfJX5c+RW84SNikFpcIA==", + "version": "8.6.14", + "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-8.6.14.tgz", + "integrity": "sha512-rBMHAfA39AGHgkrDze4RmsnQTMw1ND5fGWobr9pDcJdnDKWQWNRD7Nrlxj0gFlN3n4D9lEZhWGdFrCbku7FVAQ==", "dev": true, + "license": "MIT", "dependencies": { - "@storybook/csf": "^0.1.11", "@storybook/icons": "^1.2.12", "ts-dedent": "^2.0.0" }, @@ -2809,9 +2920,9 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^8.4.7" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "storybook": "^8.6.14" }, "peerDependenciesMeta": { "react": { @@ -2822,26 +2933,6 @@ } } }, - "node_modules/@storybook/builder-vite": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-8.4.7.tgz", - "integrity": "sha512-LovyXG5VM0w7CovI/k56ZZyWCveQFVDl0m7WwetpmMh2mmFJ+uPQ35BBsgTvTfc8RHi+9Q3F58qP1MQSByXi9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/csf-plugin": "8.4.7", - "browser-assert": "^1.2.1", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "^8.4.7", - "vite": "^4.0.0 || ^5.0.0 || ^6.0.0" - } - }, "node_modules/@storybook/channels": { "version": "8.1.10", "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-8.1.10.tgz", @@ -2873,9 +2964,9 @@ } }, "node_modules/@storybook/components": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/components/-/components-8.4.7.tgz", - "integrity": "sha512-uyJIcoyeMWKAvjrG9tJBUCKxr2WZk+PomgrgrUwejkIfXMO76i6jw9BwLa0NZjYdlthDv30r9FfbYZyeNPmF0g==", + "version": "8.6.14", + "resolved": "https://registry.npmjs.org/@storybook/components/-/components-8.6.14.tgz", + "integrity": "sha512-HNR2mC5I4Z5ek8kTrVZlIY/B8gJGs5b3XdZPBPBopTIN6U/YHXiDyOjY3JlaS4fSG1fVhp/Qp1TpMn1w/9m1pw==", "dev": true, "license": "MIT", "funding": { @@ -2946,20 +3037,6 @@ "url": "https://opencollective.com/storybook" } }, - "node_modules/@storybook/core/node_modules/@storybook/theming": { - "version": "8.6.14", - "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.6.14.tgz", - "integrity": "sha512-r4y+LsiB37V5hzpQo+BM10PaCsp7YlZ0YcZzQP1OCkPlYXmUAFy2VvDKaFRpD8IeNPKug2u4iFm/laDEbs03dg==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" - } - }, "node_modules/@storybook/csf": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.1.13.tgz", @@ -3026,37 +3103,217 @@ "dev": true }, "node_modules/@storybook/icons": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-1.3.0.tgz", - "integrity": "sha512-Nz/UzeYQdUZUhacrPyfkiiysSjydyjgg/p0P9HxB4p/WaJUUjMAcaoaLgy3EXx61zZJ3iD36WPuDkZs5QYrA0A==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-1.4.0.tgz", + "integrity": "sha512-Td73IeJxOyalzvjQL+JXx72jlIYHgs+REaHiREOqfpo3A2AYYG71AUbcv+lg7mEDIweKVCxsMQ0UKo634c8XeA==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.0.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta" + } + }, + "node_modules/@storybook/instrumenter": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/instrumenter/-/instrumenter-8.4.7.tgz", + "integrity": "sha512-k6NSD3jaRCCHAFtqXZ7tw8jAzD/yTEWXGya+REgZqq5RCkmJ+9S4Ytp/6OhQMPtPFX23gAuJJzTQVLcCr+gjRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@vitest/utils": "^2.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.4.7" + } + }, + "node_modules/@storybook/manager-api": { + "version": "8.6.14", + "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-8.6.14.tgz", + "integrity": "sha512-ez0Zihuy17udLbfHZQXkGqwtep0mSGgHcNzGN7iZrMP1m+VmNo+7aGCJJdvXi7+iU3yq8weXSQFWg5DqWgLS7g==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" + } + }, + "node_modules/@storybook/preview-api": { + "version": "8.6.14", + "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.6.14.tgz", + "integrity": "sha512-2GhcCd4dNMrnD7eooEfvbfL4I83qAqEyO0CO7JQAmIO6Rxb9BsOLLI/GD5HkvQB73ArTJ+PT50rfaO820IExOQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" + } + }, + "node_modules/@storybook/react": { + "version": "8.6.14", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-8.6.14.tgz", + "integrity": "sha512-BOepx5bBFwl/CPI+F+LnmMmsG1wQYmrX/UQXgUbHQUU9Tj7E2ndTnNbpIuSLc8IrM03ru+DfwSg1Co3cxWtT+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/components": "8.6.14", + "@storybook/global": "^5.0.0", + "@storybook/manager-api": "8.6.14", + "@storybook/preview-api": "8.6.14", + "@storybook/react-dom-shim": "8.6.14", + "@storybook/theming": "8.6.14" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "@storybook/test": "8.6.14", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^8.6.14", + "typescript": ">= 4.2.x" + }, + "peerDependenciesMeta": { + "@storybook/test": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@storybook/react-dom-shim": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-8.4.7.tgz", + "integrity": "sha512-6bkG2jvKTmWrmVzCgwpTxwIugd7Lu+2btsLAqhQSzDyIj2/uhMNp8xIMr/NBDtLgq3nomt9gefNa9xxLwk/OMg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^8.4.7" + } + }, + "node_modules/@storybook/react-vite": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-8.4.7.tgz", + "integrity": "sha512-iiY9iLdMXhDnilCEVxU6vQsN72pW3miaf0WSenOZRyZv3HdbpgOxI0qapOS0KCyRUnX9vTlmrSPTMchY4cAeOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@joshwooding/vite-plugin-react-docgen-typescript": "0.4.2", + "@rollup/pluginutils": "^5.0.2", + "@storybook/builder-vite": "8.4.7", + "@storybook/react": "8.4.7", + "find-up": "^5.0.0", + "magic-string": "^0.30.0", + "react-docgen": "^7.0.0", + "resolve": "^1.22.8", + "tsconfig-paths": "^4.2.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^8.4.7", + "vite": "^4.0.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/@storybook/react-vite/node_modules/@joshwooding/vite-plugin-react-docgen-typescript": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.4.2.tgz", + "integrity": "sha512-feQ+ntr+8hbVudnsTUapiMN9q8T90XA1d5jn9QzY09sNoj4iD9wi0PY1vsBFTda4ZjEaxRK9S81oarR2nj7TFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.27.0", + "react-docgen-typescript": "^2.2.2" + }, + "peerDependencies": { + "typescript": ">= 4.3.x", + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@storybook/react-vite/node_modules/@joshwooding/vite-plugin-react-docgen-typescript/node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@storybook/react-vite/node_modules/@storybook/builder-vite": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-8.4.7.tgz", + "integrity": "sha512-LovyXG5VM0w7CovI/k56ZZyWCveQFVDl0m7WwetpmMh2mmFJ+uPQ35BBsgTvTfc8RHi+9Q3F58qP1MQSByXi9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/csf-plugin": "8.4.7", + "browser-assert": "^1.2.1", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.4.7", + "vite": "^4.0.0 || ^5.0.0 || ^6.0.0" } }, - "node_modules/@storybook/instrumenter": { + "node_modules/@storybook/react-vite/node_modules/@storybook/components": { "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/instrumenter/-/instrumenter-8.4.7.tgz", - "integrity": "sha512-k6NSD3jaRCCHAFtqXZ7tw8jAzD/yTEWXGya+REgZqq5RCkmJ+9S4Ytp/6OhQMPtPFX23gAuJJzTQVLcCr+gjRg==", + "resolved": "https://registry.npmjs.org/@storybook/components/-/components-8.4.7.tgz", + "integrity": "sha512-uyJIcoyeMWKAvjrG9tJBUCKxr2WZk+PomgrgrUwejkIfXMO76i6jw9BwLa0NZjYdlthDv30r9FfbYZyeNPmF0g==", "dev": true, "license": "MIT", - "dependencies": { - "@storybook/global": "^5.0.0", - "@vitest/utils": "^2.1.1" - }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.7" + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, - "node_modules/@storybook/manager-api": { + "node_modules/@storybook/react-vite/node_modules/@storybook/manager-api": { "version": "8.4.7", "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-8.4.7.tgz", "integrity": "sha512-ELqemTviCxAsZ5tqUz39sDmQkvhVAvAgiplYy9Uf15kO0SP2+HKsCMzlrm2ue2FfkUNyqbDayCPPCB0Cdn/mpQ==", @@ -3070,7 +3327,7 @@ "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, - "node_modules/@storybook/preview-api": { + "node_modules/@storybook/react-vite/node_modules/@storybook/preview-api": { "version": "8.4.7", "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.4.7.tgz", "integrity": "sha512-0QVQwHw+OyZGHAJEXo6Knx+6/4er7n2rTDE5RYJ9F2E2Lg42E19pfdLlq2Jhoods2Xrclo3wj6GWR//Ahi39Eg==", @@ -3084,7 +3341,7 @@ "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, - "node_modules/@storybook/react": { + "node_modules/@storybook/react-vite/node_modules/@storybook/react": { "version": "8.4.7", "resolved": "https://registry.npmjs.org/@storybook/react/-/react-8.4.7.tgz", "integrity": "sha512-nQ0/7i2DkaCb7dy0NaT95llRVNYWQiPIVuhNfjr1mVhEP7XD090p0g7eqUmsx8vfdHh2BzWEo6CoBFRd3+EXxw==", @@ -3121,10 +3378,10 @@ } } }, - "node_modules/@storybook/react-dom-shim": { + "node_modules/@storybook/react-vite/node_modules/@storybook/theming": { "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-8.4.7.tgz", - "integrity": "sha512-6bkG2jvKTmWrmVzCgwpTxwIugd7Lu+2btsLAqhQSzDyIj2/uhMNp8xIMr/NBDtLgq3nomt9gefNa9xxLwk/OMg==", + "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.4.7.tgz", + "integrity": "sha512-99rgLEjf7iwfSEmdqlHkSG3AyLcK0sfExcr0jnc6rLiAkBhzuIsvcHjjUwkR210SOCgXqBPW0ZA6uhnuyppHLw==", "dev": true, "license": "MIT", "funding": { @@ -3132,31 +3389,15 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^8.4.7" + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, - "node_modules/@storybook/react-vite": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-8.4.7.tgz", - "integrity": "sha512-iiY9iLdMXhDnilCEVxU6vQsN72pW3miaf0WSenOZRyZv3HdbpgOxI0qapOS0KCyRUnX9vTlmrSPTMchY4cAeOg==", + "node_modules/@storybook/react/node_modules/@storybook/react-dom-shim": { + "version": "8.6.14", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-8.6.14.tgz", + "integrity": "sha512-0hixr3dOy3f3M+HBofp3jtMQMS+sqzjKNgl7Arfuj3fvjmyXOks/yGjDImySR4imPtEllvPZfhiQNlejheaInw==", "dev": true, "license": "MIT", - "dependencies": { - "@joshwooding/vite-plugin-react-docgen-typescript": "0.4.2", - "@rollup/pluginutils": "^5.0.2", - "@storybook/builder-vite": "8.4.7", - "@storybook/react": "8.4.7", - "find-up": "^5.0.0", - "magic-string": "^0.30.0", - "react-docgen": "^7.0.0", - "resolve": "^1.22.8", - "tsconfig-paths": "^4.2.0" - }, - "engines": { - "node": ">=18.0.0" - }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" @@ -3164,8 +3405,7 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^8.4.7", - "vite": "^4.0.0 || ^5.0.0 || ^6.0.0" + "storybook": "^8.6.14" } }, "node_modules/@storybook/test": { @@ -3229,9 +3469,9 @@ } }, "node_modules/@storybook/theming": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.4.7.tgz", - "integrity": "sha512-99rgLEjf7iwfSEmdqlHkSG3AyLcK0sfExcr0jnc6rLiAkBhzuIsvcHjjUwkR210SOCgXqBPW0ZA6uhnuyppHLw==", + "version": "8.6.14", + "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.6.14.tgz", + "integrity": "sha512-r4y+LsiB37V5hzpQo+BM10PaCsp7YlZ0YcZzQP1OCkPlYXmUAFy2VvDKaFRpD8IeNPKug2u4iFm/laDEbs03dg==", "dev": true, "license": "MIT", "funding": { @@ -3776,10 +4016,11 @@ } }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" }, "node_modules/@types/express": { "version": "4.17.21", @@ -3880,20 +4121,15 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "18.19.38", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.38.tgz", - "integrity": "sha512-SApYXUF7si4JJ+lO2o6X60OPOnA6wPpbiB09GMCkQ+JAwpa9hxUVG8p7GzA08TKQn5OhzK57rj1wFj+185YsGg==", + "version": "24.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", + "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~7.10.0" } }, - "node_modules/@types/prop-types": { - "version": "15.7.12", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "devOptional": true - }, "node_modules/@types/qs": { "version": "6.9.15", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", @@ -3907,22 +4143,23 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.3.3", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", - "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "version": "19.1.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz", + "integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==", "devOptional": true, + "license": "MIT", "dependencies": { - "@types/prop-types": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", - "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", + "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", "dev": true, - "dependencies": { - "@types/react": "*" + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" } }, "node_modules/@types/resolve": { @@ -3967,7 +4204,8 @@ "node_modules/@types/stylis": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", - "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==" + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", + "license": "MIT" }, "node_modules/@types/unist": { "version": "3.0.3", @@ -4218,22 +4456,24 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", - "integrity": "sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/core": "^7.24.5", - "@babel/plugin-transform-react-jsx-self": "^7.24.5", - "@babel/plugin-transform-react-jsx-source": "^7.24.1", + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", - "react-refresh": "^0.14.2" + "react-refresh": "^0.17.0" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "node_modules/@vitest/expect": { @@ -4856,10 +5096,11 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -4883,9 +5124,9 @@ "dev": true }, "node_modules/browserslist": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", - "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "version": "4.25.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", + "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", "dev": true, "funding": [ { @@ -4901,11 +5142,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001629", - "electron-to-chromium": "^1.4.796", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.16" + "caniuse-lite": "^1.0.30001737", + "electron-to-chromium": "^1.5.211", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -5037,14 +5279,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001636", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz", - "integrity": "sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==", + "version": "1.0.30001739", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz", + "integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==", "dev": true, "funding": [ { @@ -5059,7 +5302,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/ccount": { "version": "2.0.1", @@ -5496,6 +5740,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", "engines": { "node": ">=4" } @@ -5504,6 +5749,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", "dependencies": { "camelize": "^1.0.0", "css-color-keywords": "^1.0.0", @@ -5726,7 +5972,8 @@ "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" }, "node_modules/devlop": { "version": "1.1.0", @@ -5881,10 +6128,11 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.808", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.808.tgz", - "integrity": "sha512-0ItWyhPYnww2VOuCGF4s1LTfbrdAV2ajy/TN+ZTuhR23AHI6rWHCrBXJ/uxoXOvRRqw8qjYVrG81HFI7x/2wdQ==", - "dev": true + "version": "1.5.214", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.214.tgz", + "integrity": "sha512-TpvUNdha+X3ybfU78NoQatKvQEm1oq3lf2QbnmCEdw+Bd9RuIAY+hJTvq1avzHM0f7EJfnH3vbCnbzKzisc/9Q==", + "dev": true, + "license": "ISC" }, "node_modules/emittery": { "version": "0.13.1", @@ -5952,6 +6200,22 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", @@ -5959,41 +6223,45 @@ "dev": true }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" } }, "node_modules/esbuild-register": { @@ -6010,10 +6278,11 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -6296,10 +6565,11 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7016,13 +7286,16 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "dev": true, + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -7234,10 +7507,11 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7295,15 +7569,6 @@ "which": "bin/which" } }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -10295,15 +10560,16 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, + "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { @@ -10510,6 +10776,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^3.0.2" } @@ -11537,15 +11804,16 @@ } }, "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -11578,10 +11846,11 @@ } }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" }, "node_modules/normalize-path": { "version": "3.0.0", @@ -12240,9 +12509,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "funding": [ { "type": "opencollective", @@ -12257,10 +12526,11 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -12573,25 +12843,24 @@ } }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-clientside-effect": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz", - "integrity": "sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.8.tgz", + "integrity": "sha512-ma2FePH0z3px2+WOu6h+YycZcEvFmmxIlAb62cF52bG86eMySciO/EQZeQMXd07kPCYB0a1dWDT5J+KE9mCDUw==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.13" }, "peerDependencies": { - "react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "node_modules/react-code-blocks": { @@ -12617,18 +12886,19 @@ "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, "node_modules/react-confetti": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/react-confetti/-/react-confetti-6.1.0.tgz", - "integrity": "sha512-7Ypx4vz0+g8ECVxr88W9zhcQpbeujJAVqL14ZnXJ3I23mOI9/oBVTQ3dkJhUmB0D6XOtCZEM6N0Gm9PMngkORw==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/react-confetti/-/react-confetti-6.4.0.tgz", + "integrity": "sha512-5MdGUcqxrTU26I2EU7ltkWPwxvucQTuqMm8dUz72z2YMqTD6s9vMcDUysk7n9jnC+lXuCPeJJ7Knf98VEYE9Rg==", "dev": true, + "license": "MIT", "dependencies": { "tween-functions": "^1.2.0" }, "engines": { - "node": ">=10.18" + "node": ">=16" }, "peerDependencies": { - "react": "^16.3.0 || ^17.0.1 || ^18.0.0" + "react": "^16.3.0 || ^17.0.1 || ^18.0.0 || ^19.0.0" } }, "node_modules/react-docgen": { @@ -12653,9 +12923,9 @@ } }, "node_modules/react-docgen-typescript": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.2.2.tgz", - "integrity": "sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.4.0.tgz", + "integrity": "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -12669,15 +12939,15 @@ "dev": true }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", + "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.26.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.1.1" } }, "node_modules/react-focus-lock": { @@ -12710,25 +12980,26 @@ "license": "MIT" }, "node_modules/react-refresh": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-syntax-highlighter": { - "version": "15.6.1", - "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", - "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==", + "version": "15.6.6", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz", + "integrity": "sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", - "prismjs": "^1.27.0", + "prismjs": "^1.30.0", "refractor": "^3.6.0" }, "peerDependencies": { @@ -13044,12 +13315,13 @@ } }, "node_modules/rollup": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.29.1.tgz", - "integrity": "sha512-RaJ45M/kmJUzSWDs1Nnd5DdV4eerC98idtUOVr6FfKcgxqvjwHmxc5upLF9qZU9EpsVzzhleFahrT3shLuJzIw==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.0.tgz", + "integrity": "sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw==", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -13059,25 +13331,27 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.29.1", - "@rollup/rollup-android-arm64": "4.29.1", - "@rollup/rollup-darwin-arm64": "4.29.1", - "@rollup/rollup-darwin-x64": "4.29.1", - "@rollup/rollup-freebsd-arm64": "4.29.1", - "@rollup/rollup-freebsd-x64": "4.29.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.29.1", - "@rollup/rollup-linux-arm-musleabihf": "4.29.1", - "@rollup/rollup-linux-arm64-gnu": "4.29.1", - "@rollup/rollup-linux-arm64-musl": "4.29.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.29.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.29.1", - "@rollup/rollup-linux-riscv64-gnu": "4.29.1", - "@rollup/rollup-linux-s390x-gnu": "4.29.1", - "@rollup/rollup-linux-x64-gnu": "4.29.1", - "@rollup/rollup-linux-x64-musl": "4.29.1", - "@rollup/rollup-win32-arm64-msvc": "4.29.1", - "@rollup/rollup-win32-ia32-msvc": "4.29.1", - "@rollup/rollup-win32-x64-msvc": "4.29.1", + "@rollup/rollup-android-arm-eabi": "4.50.0", + "@rollup/rollup-android-arm64": "4.50.0", + "@rollup/rollup-darwin-arm64": "4.50.0", + "@rollup/rollup-darwin-x64": "4.50.0", + "@rollup/rollup-freebsd-arm64": "4.50.0", + "@rollup/rollup-freebsd-x64": "4.50.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.50.0", + "@rollup/rollup-linux-arm-musleabihf": "4.50.0", + "@rollup/rollup-linux-arm64-gnu": "4.50.0", + "@rollup/rollup-linux-arm64-musl": "4.50.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.50.0", + "@rollup/rollup-linux-ppc64-gnu": "4.50.0", + "@rollup/rollup-linux-riscv64-gnu": "4.50.0", + "@rollup/rollup-linux-riscv64-musl": "4.50.0", + "@rollup/rollup-linux-s390x-gnu": "4.50.0", + "@rollup/rollup-linux-x64-gnu": "4.50.0", + "@rollup/rollup-linux-x64-musl": "4.50.0", + "@rollup/rollup-openharmony-arm64": "4.50.0", + "@rollup/rollup-win32-arm64-msvc": "4.50.0", + "@rollup/rollup-win32-ia32-msvc": "4.50.0", + "@rollup/rollup-win32-x64-msvc": "4.50.0", "fsevents": "~2.3.2" } }, @@ -13158,12 +13432,10 @@ } }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" }, "node_modules/semver": { "version": "7.7.2", @@ -13210,7 +13482,8 @@ "node_modules/shallowequal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" }, "node_modules/shebang-command": { "version": "2.0.0", @@ -13552,16 +13825,17 @@ } }, "node_modules/styled-components": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.11.tgz", - "integrity": "sha512-Ui0jXPzbp1phYij90h12ksljKGqF8ncGx+pjrNPsSPhbUUjWT2tD1FwGo2LF6USCnbrsIhNngDfodhxbegfEOA==", + "version": "6.1.19", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.19.tgz", + "integrity": "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==", + "license": "MIT", "dependencies": { "@emotion/is-prop-valid": "1.2.2", "@emotion/unitless": "0.8.1", "@types/stylis": "4.2.5", "css-to-react-native": "3.2.0", "csstype": "3.1.3", - "postcss": "8.4.38", + "postcss": "8.4.49", "shallowequal": "1.1.0", "stylis": "4.3.2", "tslib": "2.6.2" @@ -13581,12 +13855,14 @@ "node_modules/styled-components/node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" }, "node_modules/stylis": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", - "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==" + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "license": "MIT" }, "node_modules/sucrase": { "version": "3.35.0", @@ -13777,10 +14053,11 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -13831,6 +14108,54 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "dev": true }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tinyrainbow": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", @@ -13955,7 +14280,8 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz", "integrity": "sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==", - "dev": true + "dev": true, + "license": "BSD" }, "node_modules/type-check": { "version": "0.4.0", @@ -14013,10 +14339,11 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "dev": true, + "license": "MIT" }, "node_modules/unified": { "version": "11.0.5", @@ -14122,9 +14449,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { @@ -14140,9 +14467,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -14161,9 +14489,10 @@ } }, "node_modules/use-callback-ref": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", - "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, @@ -14171,8 +14500,8 @@ "node": ">=10" }, "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -14181,14 +14510,16 @@ } }, "node_modules/use-callback-ref/node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/use-sidecar": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", - "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" @@ -14197,8 +14528,8 @@ "node": ">=10" }, "peerDependencies": { - "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -14207,9 +14538,10 @@ } }, "node_modules/use-sidecar/node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/util": { "version": "0.12.5", @@ -14289,21 +14621,24 @@ } }, "node_modules/vite": { - "version": "5.4.19", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", - "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.4.tgz", + "integrity": "sha512-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.14" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -14312,19 +14647,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -14345,13 +14686,50 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true } } }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vite/node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -14367,8 +14745,9 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -14679,7 +15058,8 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/yaml": { "version": "2.4.5", diff --git a/package.json b/package.json index 1b198e4..42b0557 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,9 @@ "dependencies": { "@vercel/analytics": "^1.3.1", "classnames": "^2.5.1", - "react": "^18.3.1", + "react": "^19.0.0", "react-code-blocks": "^0.1.6", - "react-dom": "^18.3.1", + "react-dom": "^19.0.0", "react-focus-lock": "^2.12.1" }, "devDependencies": { @@ -35,8 +35,8 @@ "@storybook/test": "^8.4.7", "@storybook/test-runner": "^0.19.0", "@storybook/theming": "^8.4.7", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", "@typescript-eslint/eslint-plugin": "^7.13.1", "@typescript-eslint/parser": "^7.13.1", "@vitejs/plugin-react": "^4.3.1", @@ -52,6 +52,6 @@ "storybook": "^8.4.7", "tailwindcss": "^3.4.5", "typescript": "^5.2.2", - "vite": "^5.3.1" + "vite": "^7.1.4" } } diff --git a/src/course/01-introduction/01-Welcome.mdx b/src/course/01-introduction/01-Welcome.mdx index 4fa9512..0f6a6f8 100644 --- a/src/course/01-introduction/01-Welcome.mdx +++ b/src/course/01-introduction/01-Welcome.mdx @@ -62,6 +62,7 @@ Each lesson is broken down in an exercise file and a final file. The exercise fi #### ๐Ÿฅ‡ Gold - [Higher order component](?path=/docs/lessons-๐Ÿฅ‡-gold-higher-order-components-pattern-01-lesson--docs) +- [Suspense & lazy loading pattern](?path=/docs/lessons-๐Ÿฅ‡-gold-suspense-lazy-loading-01-lesson--docs) ## FAQs diff --git a/src/course/02-lessons/03-Gold/Suspense/exercise/exercise.stories.tsx b/src/course/02-lessons/03-Gold/Suspense/exercise/exercise.stories.tsx new file mode 100644 index 0000000..e0cc250 --- /dev/null +++ b/src/course/02-lessons/03-Gold/Suspense/exercise/exercise.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Exercise } from './exercise'; + +const meta: Meta = { + title: 'Lessons/๐Ÿฅ‡ Gold/โณ Suspense & Lazy Loading/Exercise', + component: Exercise, + parameters: { + layout: 'centered', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; \ No newline at end of file diff --git a/src/course/02-lessons/03-Gold/Suspense/exercise/exercise.tsx b/src/course/02-lessons/03-Gold/Suspense/exercise/exercise.tsx new file mode 100644 index 0000000..f3bbdb4 --- /dev/null +++ b/src/course/02-lessons/03-Gold/Suspense/exercise/exercise.tsx @@ -0,0 +1,185 @@ +import { useState } from 'react'; +// Utilities provided for you: +// import { delay } from '../utils/delay'; +// import { PokemonLoader } from '../utils/PokemonLoader'; + +/* + * Observations + * ๐Ÿ’… All Pokemon components are imported upfront + * Large initial bundle size even if user doesn't view all Pokemon + * No loading states for heavy components + + * Available utilities (already provided): + * - import { delay } from '../utils/delay'; + * - import { PokemonLoader } from '../utils/PokemonLoader'; + + * Tasks + * 1A ๐Ÿ‘จ๐Ÿป๐Ÿ’ป - Move Pokemon components to separate files with default exports + * 1B ๐Ÿ‘จ๐Ÿป๐Ÿ’ป - Add use(delay(ms)) to each component with cached promises + * 1C ๐Ÿ‘จ๐Ÿป๐Ÿ’ป - Convert imports to React.lazy() dynamic imports + * 1D ๐Ÿ‘จ๐Ÿป๐Ÿ’ป - Wrap Pokemon components with Suspense using PokemonLoader + * 1E ๐Ÿ‘จ๐Ÿป๐Ÿ’ป - Test that components show loading states and resolve correctly +*/ + +// Heavy Pokemon detail components (simulating large components) +const PikachuDetails = () => ( +
    +

    + Pikachu +

    +
    +
    +

    Stats

    +

    HP: 35

    +

    Attack: 55

    +

    Defense: 40

    +

    Speed: 90

    +
    +
    +

    Abilities

    +

    Static

    +

    Lightning Rod (Hidden)

    +
    +
    +
    +

    Description

    +

    + This Pokemon has electricity-storing pouches on its cheeks. + These appear to become electrically charged during the night + while Pikachu sleeps. +

    +
    +
    +); + +const CharizardDetails = () => ( +
    +

    + Charizard +

    +
    +
    +

    Stats

    +

    HP: 78

    +

    Attack: 84

    +

    Defense: 78

    +

    Speed: 100

    +
    +
    +

    Abilities

    +

    Blaze

    +

    Solar Power (Hidden)

    +
    +
    +
    +

    Description

    +

    + Charizard flies around the sky in search of powerful + opponents. It breathes fire of such great heat that it melts + anything. +

    +
    +
    +); + +const BlastoiseDetails = () => ( +
    +

    + Blastoise +

    +
    +
    +

    Stats

    +

    HP: 79

    +

    Attack: 83

    +

    Defense: 100

    +

    Speed: 78

    +
    +
    +

    Abilities

    +

    Torrent

    +

    Rain Dish (Hidden)

    +
    +
    +
    +

    Description

    +

    + Blastoise has water spouts that protrude from its shell. The + water spouts are very accurate and can punch through thick + steel. +

    +
    +
    +); + +export const PokemonEncyclopedia = () => { + const [selectedPokemon, setSelectedPokemon] = useState< + string | null + >(null); + + const renderPokemonDetails = () => { + switch (selectedPokemon) { + case 'pikachu': + return ; + case 'charizard': + return ; + case 'blastoise': + return ; + default: + return ( +
    +

    + Select a Pokemon to view details +

    +
    + ); + } + }; + + return ( +
    +

    + Pokemon Encyclopedia +

    + +
    + + + +
    + + {renderPokemonDetails()} +
    + ); +}; + +export const Exercise = () => { + return ; +}; diff --git a/src/course/02-lessons/03-Gold/Suspense/final/components/BlastoiseDetails.tsx b/src/course/02-lessons/03-Gold/Suspense/final/components/BlastoiseDetails.tsx new file mode 100644 index 0000000..0073121 --- /dev/null +++ b/src/course/02-lessons/03-Gold/Suspense/final/components/BlastoiseDetails.tsx @@ -0,0 +1,41 @@ +import { use } from 'react'; +import { delay } from '../../utils/delay'; + +// Cache the promise to prevent infinite re-creation +const blastoiseDataPromise = delay(1000); + +const BlastoiseDetails = () => { + use(blastoiseDataPromise); + + return ( +
    +

    + Blastoise +

    +
    +
    +

    Stats

    +

    HP: 79

    +

    Attack: 83

    +

    Defense: 100

    +

    Speed: 78

    +
    +
    +

    Abilities

    +

    Torrent

    +

    Rain Dish (Hidden)

    +
    +
    +
    +

    Description

    +

    + Blastoise has water spouts that protrude from its shell. The + water spouts are very accurate and can punch through thick + steel. +

    +
    +
    + ); +}; + +export default BlastoiseDetails; diff --git a/src/course/02-lessons/03-Gold/Suspense/final/components/CharizardDetails.tsx b/src/course/02-lessons/03-Gold/Suspense/final/components/CharizardDetails.tsx new file mode 100644 index 0000000..d407505 --- /dev/null +++ b/src/course/02-lessons/03-Gold/Suspense/final/components/CharizardDetails.tsx @@ -0,0 +1,41 @@ +import { use } from 'react'; +import { delay } from '../../utils/delay'; + +// Cache the promise to prevent infinite re-creation +const charizardDataPromise = delay(1800); + +const CharizardDetails = () => { + use(charizardDataPromise); + + return ( +
    +

    + Charizard +

    +
    +
    +

    Stats

    +

    HP: 78

    +

    Attack: 84

    +

    Defense: 78

    +

    Speed: 100

    +
    +
    +

    Abilities

    +

    Blaze

    +

    Solar Power (Hidden)

    +
    +
    +
    +

    Description

    +

    + Charizard flies around the sky in search of powerful + opponents. It breathes fire of such great heat that it melts + anything. +

    +
    +
    + ); +}; + +export default CharizardDetails; diff --git a/src/course/02-lessons/03-Gold/Suspense/final/components/PikachuDetails.tsx b/src/course/02-lessons/03-Gold/Suspense/final/components/PikachuDetails.tsx new file mode 100644 index 0000000..cdd7db5 --- /dev/null +++ b/src/course/02-lessons/03-Gold/Suspense/final/components/PikachuDetails.tsx @@ -0,0 +1,41 @@ +import { use } from 'react'; +import { delay } from '../../utils/delay'; + +// Cache the promise to prevent infinite re-creation +const pikachuDataPromise = delay(1200); + +const PikachuDetails = () => { + use(pikachuDataPromise); + + return ( +
    +

    + Pikachu +

    +
    +
    +

    Stats

    +

    HP: 35

    +

    Attack: 55

    +

    Defense: 40

    +

    Speed: 90

    +
    +
    +

    Abilities

    +

    Static

    +

    Lightning Rod (Hidden)

    +
    +
    +
    +

    Description

    +

    + This Pokemon has electricity-storing pouches on its cheeks. + These appear to become electrically charged during the night + while Pikachu sleeks. +

    +
    +
    + ); +}; + +export default PikachuDetails; diff --git a/src/course/02-lessons/03-Gold/Suspense/final/final.stories.tsx b/src/course/02-lessons/03-Gold/Suspense/final/final.stories.tsx new file mode 100644 index 0000000..d5a8398 --- /dev/null +++ b/src/course/02-lessons/03-Gold/Suspense/final/final.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Final } from './final'; + +const meta: Meta = { + title: 'Lessons/๐Ÿฅ‡ Gold/โณ Suspense & Lazy Loading/Final', + component: Final, + parameters: { + layout: 'centered', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; \ No newline at end of file diff --git a/src/course/02-lessons/03-Gold/Suspense/final/final.tsx b/src/course/02-lessons/03-Gold/Suspense/final/final.tsx new file mode 100644 index 0000000..cbcbb6c --- /dev/null +++ b/src/course/02-lessons/03-Gold/Suspense/final/final.tsx @@ -0,0 +1,106 @@ +import { Suspense, lazy, useState } from 'react'; +import { PokemonLoader } from '../utils/PokemonLoader'; + +// Lazy load Pokemon components - only loaded when needed +const PikachuDetails = lazy( + () => import('./components/PikachuDetails') +); +const CharizardDetails = lazy( + () => import('./components/CharizardDetails') +); +const BlastoiseDetails = lazy( + () => import('./components/BlastoiseDetails') +); + +export const PokemonEncyclopedia = () => { + const [selectedPokemon, setSelectedPokemon] = useState< + string | null + >(null); + + const renderPokemonDetails = () => { + if (!selectedPokemon) { + return ( +
    +

    + Select a Pokemon to view details +

    +
    + ); + } + + // Each component is wrapped in Suspense for individual loading states + switch (selectedPokemon) { + case 'pikachu': + return ( + }> + + + ); + case 'charizard': + return ( + }> + + + ); + case 'blastoise': + return ( + }> + + + ); + default: + return null; + } + }; + + return ( +
    +

    + Pokemon Encyclopedia +

    + +
    + + + +
    + + {renderPokemonDetails()} +
    + ); +}; + +export const Final = () => { + return ( +
    + +
    + ); +}; diff --git a/src/course/02-lessons/03-Gold/Suspense/lesson.mdx b/src/course/02-lessons/03-Gold/Suspense/lesson.mdx new file mode 100644 index 0000000..fb1f59b --- /dev/null +++ b/src/course/02-lessons/03-Gold/Suspense/lesson.mdx @@ -0,0 +1,47 @@ +import { Meta } from '@storybook/blocks'; + + + +# โณ Suspense & Lazy Loading + +Suspense allows you to declaratively handle loading states while React.lazy enables code splitting by dynamically importing components only when needed. This improves initial bundle size and loading performance. + +## Basic Example + +```jsx +import { Suspense, lazy } from 'react'; + +// Lazy load component +const PokemonDetails = lazy(() => import('./PokemonDetails')); + +const App = () => ( + Loading Pokemon...
    }> + + +); +``` + +## Exercise + +In this exercise we have a Pokemon encyclopedia app that imports all Pokemon detail components upfront, creating a large initial bundle. Users might only view a few Pokemon, making this inefficient. + +Your task is to implement lazy loading with Suspense to split the code and only load Pokemon components when they're actually needed. + +Head over to the exercise file and let's begin. + +## When to use this pattern? + +**Use Suspense & Lazy Loading for:** +- **Large Components**: Heavy components that aren't immediately needed +- **Route-based Splitting**: Different pages/routes in your app +- **Conditional Features**: Components shown based on user actions +- **Performance**: Reducing initial bundle size + +**Avoid when:** +- **Small Components**: Overhead isn't worth it for tiny components +- **Critical Path**: Components needed immediately on page load +- **Frequent Toggling**: Components that show/hide rapidly + +## Feedback + +[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new) \ No newline at end of file diff --git a/src/course/02-lessons/03-Gold/Suspense/utils/PokemonLoader.tsx b/src/course/02-lessons/03-Gold/Suspense/utils/PokemonLoader.tsx new file mode 100644 index 0000000..82d3580 --- /dev/null +++ b/src/course/02-lessons/03-Gold/Suspense/utils/PokemonLoader.tsx @@ -0,0 +1,23 @@ +// Loading fallback component for Pokemon data +export const PokemonLoader = () => ( +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Loading Pokemon data...

    +
    +); \ No newline at end of file diff --git a/src/course/02-lessons/03-Gold/Suspense/utils/delay.ts b/src/course/02-lessons/03-Gold/Suspense/utils/delay.ts new file mode 100644 index 0000000..5d30227 --- /dev/null +++ b/src/course/02-lessons/03-Gold/Suspense/utils/delay.ts @@ -0,0 +1,2 @@ +// Utility function to create a delay promise +export const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); \ No newline at end of file From 7a04b94fba973e77edf60634d725540afde9c41e Mon Sep 17 00:00:00 2001 From: code-mattclaffey Date: Thu, 4 Sep 2025 20:27:29 +0100 Subject: [PATCH 22/29] feat: implement the headless component pattern --- src/course/01-introduction/01-Welcome.mdx | 1 + .../exercise/exercise.stories.tsx | 15 + .../HeadlessComponents/exercise/exercise.tsx | 162 +++++++++++ .../final/final.stories.tsx | 15 + .../HeadlessComponents/final/final.tsx | 259 ++++++++++++++++++ .../03-Gold/HeadlessComponents/lesson.mdx | 61 +++++ .../02-lessons/03-Gold/Suspense/lesson.mdx | 6 +- 7 files changed, 518 insertions(+), 1 deletion(-) create mode 100644 src/course/02-lessons/03-Gold/HeadlessComponents/exercise/exercise.stories.tsx create mode 100644 src/course/02-lessons/03-Gold/HeadlessComponents/exercise/exercise.tsx create mode 100644 src/course/02-lessons/03-Gold/HeadlessComponents/final/final.stories.tsx create mode 100644 src/course/02-lessons/03-Gold/HeadlessComponents/final/final.tsx create mode 100644 src/course/02-lessons/03-Gold/HeadlessComponents/lesson.mdx diff --git a/src/course/01-introduction/01-Welcome.mdx b/src/course/01-introduction/01-Welcome.mdx index 0f6a6f8..10c2523 100644 --- a/src/course/01-introduction/01-Welcome.mdx +++ b/src/course/01-introduction/01-Welcome.mdx @@ -63,6 +63,7 @@ Each lesson is broken down in an exercise file and a final file. The exercise fi - [Higher order component](?path=/docs/lessons-๐Ÿฅ‡-gold-higher-order-components-pattern-01-lesson--docs) - [Suspense & lazy loading pattern](?path=/docs/lessons-๐Ÿฅ‡-gold-suspense-lazy-loading-01-lesson--docs) +- [Headless components pattern](?path=/docs/lessons-๐Ÿฅ‡-gold-headless-components-01-lesson--docs) ## FAQs diff --git a/src/course/02-lessons/03-Gold/HeadlessComponents/exercise/exercise.stories.tsx b/src/course/02-lessons/03-Gold/HeadlessComponents/exercise/exercise.stories.tsx new file mode 100644 index 0000000..d381ce3 --- /dev/null +++ b/src/course/02-lessons/03-Gold/HeadlessComponents/exercise/exercise.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Exercise } from './exercise'; + +const meta: Meta = { + title: 'Lessons/๐Ÿฅ‡ Gold/๐ŸŽญ Headless Components/Exercise', + component: Exercise, + parameters: { + layout: 'centered', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; \ No newline at end of file diff --git a/src/course/02-lessons/03-Gold/HeadlessComponents/exercise/exercise.tsx b/src/course/02-lessons/03-Gold/HeadlessComponents/exercise/exercise.tsx new file mode 100644 index 0000000..f69e8dd --- /dev/null +++ b/src/course/02-lessons/03-Gold/HeadlessComponents/exercise/exercise.tsx @@ -0,0 +1,162 @@ +import { useState } from 'react'; + +interface IPokemon { + id: number; + name: string; + type: string; + level: number; + caught: boolean; +} + +/* + * Observations + * ๐Ÿ’… Pokemon inventory logic is tightly coupled with card UI + * Filtering, sorting, and state management mixed with presentation + * Hard to reuse logic for different UI designs + + * Tasks + * 1A ๐Ÿ’ป - Extract inventory logic into usePokemonInventory hook + * 1B ๐Ÿ’ป - Return state and actions from the headless hook + * 1C ๐Ÿ’ป - Create CardView component that uses the headless logic + * 1D ๐Ÿ’ป - Create ListView component with same logic, different UI + * 1E ๐Ÿ’ป - Test both components work with shared functionality +*/ + +const mockPokemon: IPokemon[] = [ + { + id: 1, + name: 'Pikachu', + type: 'Electric', + level: 25, + caught: true + }, + { + id: 4, + name: 'Charmander', + type: 'Fire', + level: 12, + caught: false + }, + { id: 7, name: 'Squirtle', type: 'Water', level: 18, caught: true }, + { + id: 25, + name: 'Pichu', + type: 'Electric', + level: 8, + caught: false + }, + { + id: 150, + name: 'Mewtwo', + type: 'Psychic', + level: 70, + caught: true + } +]; + +export const PokemonInventory = () => { + const [pokemon, setPokemon] = useState(mockPokemon); + const [filter, setFilter] = useState('all'); + const [sortBy, setSortBy] = useState('name'); + + const toggleCaught = (id: number) => { + setPokemon((prev) => + prev.map((p) => (p.id === id ? { ...p, caught: !p.caught } : p)) + ); + }; + + const filteredPokemon = pokemon.filter((p) => { + if (filter === 'caught') return p.caught; + if (filter === 'wild') return !p.caught; + return true; + }); + + const sortedPokemon = [...filteredPokemon].sort((a, b) => { + if (sortBy === 'level') return b.level - a.level; + if (sortBy === 'type') return a.type.localeCompare(b.type); + return a.name.localeCompare(b.name); + }); + + return ( +
    +

    Pokemon Inventory

    + + {/* Controls */} +
    +
    + + +
    + +
    + + +
    +
    + + {/* Pokemon Cards */} +
    + {sortedPokemon.map((p) => ( +
    +
    +

    {p.name}

    + + {p.caught ? 'Caught' : 'Wild'} + +
    + +
    +

    Type: {p.type}

    +

    Level: {p.level}

    +
    + + +
    + ))} +
    + +
    + Showing {sortedPokemon.length} of {pokemon.length} Pokemon +
    +
    + ); +}; + +export const Exercise = () => { + return ; +}; diff --git a/src/course/02-lessons/03-Gold/HeadlessComponents/final/final.stories.tsx b/src/course/02-lessons/03-Gold/HeadlessComponents/final/final.stories.tsx new file mode 100644 index 0000000..48d1659 --- /dev/null +++ b/src/course/02-lessons/03-Gold/HeadlessComponents/final/final.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Final } from './final'; + +const meta: Meta = { + title: 'Lessons/๐Ÿฅ‡ Gold/๐ŸŽญ Headless Components/Final', + component: Final, + parameters: { + layout: 'centered', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; \ No newline at end of file diff --git a/src/course/02-lessons/03-Gold/HeadlessComponents/final/final.tsx b/src/course/02-lessons/03-Gold/HeadlessComponents/final/final.tsx new file mode 100644 index 0000000..1c45312 --- /dev/null +++ b/src/course/02-lessons/03-Gold/HeadlessComponents/final/final.tsx @@ -0,0 +1,259 @@ +import { useState } from 'react'; + +interface IPokemon { + id: number; + name: string; + type: string; + level: number; + caught: boolean; +} + +const mockPokemon: IPokemon[] = [ + { + id: 1, + name: 'Pikachu', + type: 'Electric', + level: 25, + caught: true + }, + { + id: 4, + name: 'Charmander', + type: 'Fire', + level: 12, + caught: false + }, + { id: 7, name: 'Squirtle', type: 'Water', level: 18, caught: true }, + { + id: 25, + name: 'Pichu', + type: 'Electric', + level: 8, + caught: false + }, + { + id: 150, + name: 'Mewtwo', + type: 'Psychic', + level: 70, + caught: true + } +]; + +// Headless component - logic only, no UI +const usePokemonInventory = () => { + const [pokemon, setPokemon] = useState(mockPokemon); + const [filter, setFilter] = useState('all'); + const [sortBy, setSortBy] = useState('name'); + + const toggleCaught = (id: number) => { + setPokemon((prev) => + prev.map((p) => (p.id === id ? { ...p, caught: !p.caught } : p)) + ); + }; + + const filteredPokemon = pokemon.filter((p) => { + if (filter === 'caught') return p.caught; + if (filter === 'wild') return !p.caught; + return true; + }); + + const sortedPokemon = [...filteredPokemon].sort((a, b) => { + if (sortBy === 'level') return b.level - a.level; + if (sortBy === 'type') return a.type.localeCompare(b.type); + return a.name.localeCompare(b.name); + }); + + return { + // State + pokemon: sortedPokemon, + totalCount: pokemon.length, + filter, + sortBy, + + // Actions + setFilter, + setSortBy, + toggleCaught + }; +}; + +// Card View UI Component +const CardView = () => { + const { + pokemon, + totalCount, + filter, + sortBy, + setFilter, + setSortBy, + toggleCaught + } = usePokemonInventory(); + + return ( +
    +

    Pokemon Cards

    + +
    + + + +
    + +
    + {pokemon.map((p) => ( +
    +
    +

    {p.name}

    + + {p.caught ? 'Caught' : 'Wild'} + +
    + +
    +

    Type: {p.type}

    +

    Level: {p.level}

    +
    + + +
    + ))} +
    + +
    + Showing {pokemon.length} of {totalCount} Pokemon +
    +
    + ); +}; + +// List View UI Component +const ListView = () => { + const { + pokemon, + totalCount, + filter, + sortBy, + setFilter, + setSortBy, + toggleCaught + } = usePokemonInventory(); + + return ( +
    +

    Pokemon List

    + +
    + + + +
    + +
    + + + + + + + + + + + + {pokemon.map((p) => ( + + + + + + + + ))} + +
    NameTypeLevelStatusAction
    {p.name}{p.type}{p.level} + + {p.caught ? 'Caught' : 'Wild'} + + + +
    +
    + +
    + Showing {pokemon.length} of {totalCount} Pokemon +
    +
    + ); +}; + +export const Final = () => { + return ( +
    + + +
    + ); +}; diff --git a/src/course/02-lessons/03-Gold/HeadlessComponents/lesson.mdx b/src/course/02-lessons/03-Gold/HeadlessComponents/lesson.mdx new file mode 100644 index 0000000..508777f --- /dev/null +++ b/src/course/02-lessons/03-Gold/HeadlessComponents/lesson.mdx @@ -0,0 +1,61 @@ +import { Meta } from '@storybook/blocks'; + + + +# ๐ŸŽญ Headless Components + +Headless components provide logic and behavior without any UI. They separate business logic from presentation, allowing complete control over styling while reusing complex functionality across different designs. + +## Basic Example + +```jsx +// Headless component - logic only +const usePokemonBattle = () => { + const [hp, setHp] = useState(100); + const attack = () => setHp((prev) => Math.max(0, prev - 20)); + const heal = () => setHp((prev) => Math.min(100, prev + 30)); + + return { hp, attack, heal, isDefeated: hp === 0 }; +}; + +// UI components use the headless logic +const BattleCard = () => { + const { hp, attack, heal, isDefeated } = usePokemonBattle(); + return ( +
    +
    HP: {hp}
    + + +
    + ); +}; +``` + +## Exercise + +In this exercise we have a Pokemon inventory system with tightly coupled UI and logic. Different teams need the same inventory functionality but with completely different designs - mobile cards, desktop tables, and admin dashboards. + +Your task is to extract the inventory logic into a headless component, then create multiple UI implementations that use the same underlying functionality. + +Head over to the exercise file and let's begin. + +## When to use this pattern? + +**Use headless components for:** + +- **Complex Logic**: State machines, data fetching, form validation +- **Multiple UIs**: Same logic needed across different designs +- **Design Systems**: Consistent behavior with flexible presentation +- **Testing**: Easier to test logic separately from UI + +**Avoid when:** + +- **Simple Components**: Basic UI with minimal logic +- **Single Use**: Logic only needed in one place +- **Tight Coupling**: UI and logic are inherently connected + +## Feedback + +Feedback is a gift and it helps me make these course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you. + +[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new) diff --git a/src/course/02-lessons/03-Gold/Suspense/lesson.mdx b/src/course/02-lessons/03-Gold/Suspense/lesson.mdx index fb1f59b..40b6849 100644 --- a/src/course/02-lessons/03-Gold/Suspense/lesson.mdx +++ b/src/course/02-lessons/03-Gold/Suspense/lesson.mdx @@ -32,16 +32,20 @@ Head over to the exercise file and let's begin. ## When to use this pattern? **Use Suspense & Lazy Loading for:** + - **Large Components**: Heavy components that aren't immediately needed - **Route-based Splitting**: Different pages/routes in your app - **Conditional Features**: Components shown based on user actions - **Performance**: Reducing initial bundle size **Avoid when:** + - **Small Components**: Overhead isn't worth it for tiny components - **Critical Path**: Components needed immediately on page load - **Frequent Toggling**: Components that show/hide rapidly ## Feedback -[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new) \ No newline at end of file +Feedback is a gift and it helps me make these course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you. + +[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new) From 482ca85c0bd19ecb2ea3ae694e9aa455c6720167 Mon Sep 17 00:00:00 2001 From: code-mattclaffey Date: Thu, 4 Sep 2025 20:45:15 +0100 Subject: [PATCH 23/29] feat: improve the HOC lesson --- .../01-Bronze/ConditionalRendering/lesson.mdx | 13 ++ .../02-lessons/01-Bronze/Hooks/lesson.mdx | 13 ++ .../01-Bronze/PropsCombination/lesson.mdx | 13 ++ .../02-lessons/02-Silver/Compound/lesson.mdx | 13 ++ .../02-lessons/02-Silver/Provider/lesson.mdx | 15 ++ .../exercise/exercise.tsx | 130 ++++++++++-------- .../HigherOrderComponents/final/final.tsx | 115 ++++++++-------- .../03-Gold/HigherOrderComponents/lesson.mdx | 74 +++++----- 8 files changed, 239 insertions(+), 147 deletions(-) diff --git a/src/course/02-lessons/01-Bronze/ConditionalRendering/lesson.mdx b/src/course/02-lessons/01-Bronze/ConditionalRendering/lesson.mdx index cc53d7b..0b239a5 100644 --- a/src/course/02-lessons/01-Bronze/ConditionalRendering/lesson.mdx +++ b/src/course/02-lessons/01-Bronze/ConditionalRendering/lesson.mdx @@ -88,6 +88,19 @@ const PokemonExplorer = () => { In the first exercise we are going to look into building a Pokemon trainer status system that shows different content based on whether the trainer has earned gym badges. Go to the exercise.tsx inside the ConditionalRendering folder and start the exercise. Once completed, the Tests will show as passed in the storybook "Interactions" addon section. +## When to use this pattern? + +**Use conditional rendering for:** +- **Dynamic UI**: Show/hide components based on state or props +- **User Permissions**: Display content based on user roles +- **Loading States**: Show spinners while data loads +- **Error Handling**: Display error messages when needed + +**Avoid when:** +- **Complex Logic**: Use separate components for complex conditions +- **Performance**: Avoid heavy computations in render conditions +- **Readability**: Don't nest too many ternary operators + ## Feedback Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you. diff --git a/src/course/02-lessons/01-Bronze/Hooks/lesson.mdx b/src/course/02-lessons/01-Bronze/Hooks/lesson.mdx index 8a760f4..dfd6e6b 100644 --- a/src/course/02-lessons/01-Bronze/Hooks/lesson.mdx +++ b/src/course/02-lessons/01-Bronze/Hooks/lesson.mdx @@ -99,6 +99,19 @@ You'll create a hook that manages capture attempts, pokeball inventory, success Head over to the exercise and let's get started. +## When to use this pattern? + +**Use hooks for:** +- **State Management**: Replace class component state with useState +- **Side Effects**: Handle API calls, subscriptions with useEffect +- **Logic Reuse**: Extract common logic into custom hooks +- **Modern React**: Preferred approach for new components + +**Avoid when:** +- **Legacy Code**: Existing class components work fine +- **Simple Components**: Pure presentational components don't need hooks +- **Over-abstraction**: Don't create hooks for single-use logic + ## Feedback Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you. diff --git a/src/course/02-lessons/01-Bronze/PropsCombination/lesson.mdx b/src/course/02-lessons/01-Bronze/PropsCombination/lesson.mdx index 237ff54..0978a63 100644 --- a/src/course/02-lessons/01-Bronze/PropsCombination/lesson.mdx +++ b/src/course/02-lessons/01-Bronze/PropsCombination/lesson.mdx @@ -56,6 +56,19 @@ You'll be refactoring a Pokemon trading card component that currently has too ma Head over to the exercise file and let's begin. +## When to use this pattern? + +**Use props combination for:** +- **Related Data**: Group logically related props together +- **Reducing Clutter**: When components have many individual props +- **API Design**: Creating cleaner component interfaces +- **Maintainability**: Easier to add/remove related properties + +**Avoid when:** +- **Simple Components**: Few props don't need grouping +- **Unrelated Props**: Don't force unrelated props together +- **Performance**: Grouping can cause unnecessary re-renders + ## Feedback Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you. diff --git a/src/course/02-lessons/02-Silver/Compound/lesson.mdx b/src/course/02-lessons/02-Silver/Compound/lesson.mdx index 9c7ce74..ec92035 100644 --- a/src/course/02-lessons/02-Silver/Compound/lesson.mdx +++ b/src/course/02-lessons/02-Silver/Compound/lesson.mdx @@ -27,6 +27,19 @@ The compound component pattern is useful in building complex React components su A requirement has come in to reuse the Pokemon team builder in another location of our application. The current implementation of the team builder has its state management implemented only on the page that this component is used on. We need to refactor the component to use the compound design pattern so that it can be re-used on both pages. Head over to the exercise.tsx to continue. +## When to use this pattern? + +**Use compound components for:** +- **Complex UI**: Multi-part components like accordions, tabs, modals +- **Flexible API**: When you want declarative, composable interfaces +- **State Sharing**: Components that need to share state implicitly +- **Reusable Libraries**: Building component libraries with flexible APIs + +**Avoid when:** +- **Simple Components**: Basic components don't need compound patterns +- **Performance**: Can add complexity if not needed +- **Learning Curve**: May be confusing for junior developers + ## Feedback Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you. diff --git a/src/course/02-lessons/02-Silver/Provider/lesson.mdx b/src/course/02-lessons/02-Silver/Provider/lesson.mdx index 7d89fc6..d1c5b71 100644 --- a/src/course/02-lessons/02-Silver/Provider/lesson.mdx +++ b/src/course/02-lessons/02-Silver/Provider/lesson.mdx @@ -34,6 +34,21 @@ const Page = () => { In this lesson we are going to implement the PokemonManager with the react content pattern and pull that information from a hook in our component. Head over to the exercise.tsx file to get started. +## When to use this pattern? + +**Use Provider pattern for:** + +- **Global State**: App-wide data like user authentication, themes +- **Avoiding Prop Drilling**: When props pass through many components +- **Shared Logic**: Common functionality across component trees +- **Configuration**: App settings that many components need + +**Avoid when:** + +- **Local State**: Component-specific state should stay local +- **Simple Apps**: Small apps don't need global state management +- **Performance**: Context changes re-render all consumers + ## Feedback Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you. diff --git a/src/course/02-lessons/03-Gold/HigherOrderComponents/exercise/exercise.tsx b/src/course/02-lessons/03-Gold/HigherOrderComponents/exercise/exercise.tsx index 40df327..20ae3ee 100644 --- a/src/course/02-lessons/03-Gold/HigherOrderComponents/exercise/exercise.tsx +++ b/src/course/02-lessons/03-Gold/HigherOrderComponents/exercise/exercise.tsx @@ -1,63 +1,77 @@ -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 2A Import IPokemon, IPokemonManagerActions, IPokemonManageState, withPokemon from './store'; - -/** - * Exercise: Implement a Higher Order Component - * - * What will we be doing? - * We will be creating a Higher order component which will pass pokemon data - * Using the same pattern that Redux used to do with their connect function - * - * Navigate your way to withPokemon to start. - */ - -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 2B - Interfaces -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 2B.a Setup an interface called IMapStateToPropsComponentOneResponse. -// This will just have pokemons: IPokemon[]; in the interface. - -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 2B.b Setup an interface called IActionsComponentOneResponse. -// This will just have fetchPokemons: (total: number) => Promise; in the interface. - -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 2B.c Setup an interface called IComponentOneProps which extends IMapStateToPropsComponentOneResponse & IActionsComponentOneResponse. -// This will just have title: string; in the interface. - -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 2C - Setting up the mapStateToProps & mapActionsToProps -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 2C.a Setup a function called mapStateToProps which has a parameter state: IPokemonManagerState -// it should return the IMapStateToPropsComponentOneResponse interface - -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 2C.b Setup a function called mapActionsToProps which has a parameter actions: IPokemonManagerActions -// it should return the IActionsComponentOneResponse interface - -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 2D - Creating the component -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 2D.a Create a Component and it should have this params { pokemons, title, fetchPokemons }: IComponentProps -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 2D.b Make a useEffect with no dependencies and fetchPokemons - go wild with the total... why not. -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป๐Ÿ’„ 2D.c Return this markup -{ - /*
    -

    {title}

    -
    - {pokemons.map((pokemon) => ( -
    - {pokemon.name} -
    - ))} -
    -
    */ +// No imports needed for this exercise + +interface IPokemon { + id: number; + name: string; + type: string; + level: number; } -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 2E - Applying the HOC -// I want you to call: -// const Exercise = withPokemons< -// IMapStateToPropsComponentOneResponse, // We are defining a generic here -// IActionsComponentOneResponse // We are defining a generic here -// >( -// mapStateToProps, -// mapActionsToProps -// )(Component); +/* + * Observations + * ๐Ÿ’… Type-based styling logic is repeated inline + * Hard to reuse styling across different components + * Mixing presentation logic with component logic + + * Tasks + * 1A ๐Ÿ’ป - Create withPokemonType HOC that adds type-based styling + * 1B ๐Ÿ’ป - Apply HOC to PokemonCard component + * 1C ๐Ÿ’ป - Use enhanced component to display Pokemon with type styling + * 1D ๐Ÿ’ป - Test that different Pokemon types get different styling +*/ + +// Sample Pokemon data +const samplePokemon: IPokemon[] = [ + { id: 1, name: 'Pikachu', type: 'Electric', level: 25 }, + { id: 4, name: 'Charmander', type: 'Fire', level: 12 }, + { id: 7, name: 'Squirtle', type: 'Water', level: 18 } +]; + +// Basic Pokemon Card Component +const PokemonCard = ({ pokemon }: { pokemon: IPokemon }) => ( +
    +

    {pokemon.name}

    +

    Type: {pokemon.type}

    +

    Level: {pokemon.level}

    +
    +); + +// Component that shows Pokemon with inline type styling (needs refactoring) +const PokemonShowcase = () => { + // Inline type styling (should be extracted to HOC) + const getTypeStyles = (type: string) => { + const styles = { + Electric: 'bg-yellow-100 border-yellow-300', + Fire: 'bg-red-100 border-red-300', + Water: 'bg-blue-100 border-blue-300' + }; + return ( + styles[type as keyof typeof styles] || + 'bg-gray-100 border-gray-300' + ); + }; + + return ( +
    + {samplePokemon.map((pokemon) => ( +
    + +
    + ))} +
    + ); +}; export const Exercise = () => { - return null; + return ( +
    +

    + Pokemon Cards (Before HOC) +

    + +
    + ); }; diff --git a/src/course/02-lessons/03-Gold/HigherOrderComponents/final/final.tsx b/src/course/02-lessons/03-Gold/HigherOrderComponents/final/final.tsx index fa790fd..efe1a8f 100644 --- a/src/course/02-lessons/03-Gold/HigherOrderComponents/final/final.tsx +++ b/src/course/02-lessons/03-Gold/HigherOrderComponents/final/final.tsx @@ -1,69 +1,72 @@ -import { useEffect } from 'react'; -import { IPokemonManagerActions, withPokemons } from './withPokemon'; -import { - IPokemon, - IPokemonManagerState -} from '@shared/modules/PokemonManager/PokemonManager'; +import { ComponentType } from 'react'; -interface IMapStateToPropsComponentOneResponse { - pokemons: IPokemon[]; +interface IPokemon { + id: number; + name: string; + type: string; + level: number; } -interface IActionsComponentOneResponse { - fetchPokemons: (total: number) => Promise; -} +// Sample Pokemon data +const samplePokemon: IPokemon[] = [ + { id: 1, name: 'Pikachu', type: 'Electric', level: 25 }, + { id: 4, name: 'Charmander', type: 'Fire', level: 12 }, + { id: 7, name: 'Squirtle', type: 'Water', level: 18 } +]; -interface IComponentOneProps - extends IMapStateToPropsComponentOneResponse, - IActionsComponentOneResponse { - title: string; -} +// HOC: Add Pokemon type-based styling +const withPokemonType =

    ( + Component: ComponentType

    +) => { + return (props: P & { type?: string }) => { + const getTypeStyles = (type?: string) => { + const styles = { + Electric: 'bg-yellow-100 border-yellow-300', + Fire: 'bg-red-100 border-red-300', + Water: 'bg-blue-100 border-blue-300' + }; + return ( + styles[type as keyof typeof styles] || + 'bg-gray-100 border-gray-300' + ); + }; -const mapStateToProps = ( - state: IPokemonManagerState -): IMapStateToPropsComponentOneResponse => ({ - pokemons: state.pokemons -}); + return ( +

    + +
    + ); + }; +}; -const mapActionsToProps = ( - actions: IPokemonManagerActions -): IActionsComponentOneResponse => ({ - fetchPokemons: actions.fetchPokemons -}); +// Basic Pokemon Card Component +const PokemonCard = ({ pokemon }: { pokemon: IPokemon }) => ( +
    +

    {pokemon.name}

    +

    Type: {pokemon.type}

    +

    Level: {pokemon.level}

    +
    +); -const Component = ({ - pokemons, - title, - fetchPokemons -}: IComponentOneProps) => { - useEffect(() => { - fetchPokemons(12); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); +// Enhanced component using HOC +const StyledPokemonCard = withPokemonType(PokemonCard); +export const Final = () => { return ( -
    -

    {title}

    -
    - {pokemons.map((pokemon) => ( -
    - {pokemon.name} -
    +
    +

    Pokemon Cards with HOC

    + +
    + {samplePokemon.map((pokemon) => ( + ))}
    -
    +
    ); }; - -export const Final = withPokemons< - IMapStateToPropsComponentOneResponse, - IActionsComponentOneResponse, - { title: string } ->( - mapStateToProps, - mapActionsToProps -)(Component); diff --git a/src/course/02-lessons/03-Gold/HigherOrderComponents/lesson.mdx b/src/course/02-lessons/03-Gold/HigherOrderComponents/lesson.mdx index e4df04d..d095c50 100644 --- a/src/course/02-lessons/03-Gold/HigherOrderComponents/lesson.mdx +++ b/src/course/02-lessons/03-Gold/HigherOrderComponents/lesson.mdx @@ -2,55 +2,63 @@ import { Meta } from '@storybook/blocks'; -# ๐ŸŽ† Higher Order Components Pattern +# ๐ŸŽ† Higher Order Components (HOC) -In some cases you may want to have some logic that is consistent across your application. You could use hooks but this requires implementing some logic of code within each component you apply the hook which isn't sustainable as a higher order component. +Higher Order Components are functions that take a component and return a new component with additional functionality. They're perfect for cross-cutting concerns like authentication, logging, or data fetching that you want to apply to multiple components. -## What does it look like syntactically - -If you are familiar with functional programming this looks very similar to the "currying" pattern. +## Basic Example ```jsx -// HigherOrderComponent.ts -export const withHigherOrderComponent = (Component) => (props) => { - const { isAuth } = useAuthentication(); - - if (!isAuth) { - return

    Not authenticated

    ; - } - - return ; +// HOC that adds Pokemon type styling +const withPokemonType = (Component) => (props) => { + const getTypeColor = (type) => { + const colors = { + Fire: 'bg-red-100 border-red-300', + Water: 'bg-blue-100 border-blue-300', + Electric: 'bg-yellow-100 border-yellow-300' + }; + return colors[type] || 'bg-gray-100 border-gray-300'; + }; + + return ( +
    + +
    + ); }; -// Component.tsx -import { withHigherOrderComponent } from 'HigherOrderComponent.ts'; - -const Component = ({ foo }) =>

    {foo}

    ; - -export default withHigherOrderComponent(Component); +// Usage +const PokemonCard = ({ name, type }) =>

    {name}

    ; +const StyledPokemonCard = withPokemonType(PokemonCard); -// App.tsx +// Renders with type-specific styling +; +``` -import Component from 'Component.tsx'; +## Exercise -const App = () => { - return ; -}; -``` +In this exercise we have Pokemon components that need common functionality like loading states, error handling, and type-based styling. Instead of duplicating this logic, we'll create HOCs to wrap components with these features. -> In React it is good practice to use the **with** at the start as the name associates to a higher order component. +Your task is to create HOCs that add loading states, error boundaries, and Pokemon type styling to any component. -Now let's breakdown what is actually going on here. You have the **withHigherOrderComponent** and it takes a Component as a prop and then you return another function with props which will normally be the props that the component will need that do not get provided by the higher order component. +Head over to the exercise file and let's begin. -## When would this be useful? +## When to use this pattern? -The logic in the higher order component is consolidated into one area which makes things more consistent across your application vs writing that line over and over again in every use case you need. +**Use HOCs for:** -## Exercise +- **Cross-cutting Concerns**: Authentication, logging, analytics +- **Code Reuse**: Same functionality across multiple components +- **Legacy Support**: Wrapping class components with modern features +- **Third-party Integration**: Adding external library features -In this exercise we are going to go a bit wild and implement a very high level version of the Redux connect. The higher order component will pull information from a class which stores our data and then we will relay that data into the component. +**Avoid when:** -> Blast from the past this if you have done this with old Redux... sorry ๐Ÿ˜‰ +- **Simple Logic**: Use hooks for simpler state management +- **Modern React**: Prefer hooks and composition over HOCs +- **Complex Props**: HOCs can make prop flow confusing ## Feedback From 8484f9acc3f085475631a6ddb214f2ca27b3a4dd Mon Sep 17 00:00:00 2001 From: code-mattclaffey Date: Thu, 4 Sep 2025 20:47:11 +0100 Subject: [PATCH 24/29] chore: update some files --- .../exercise/components/ErrorBoundary.tsx | 0 .../exercise/components/Fallback.tsx | 0 .../exercise/exercise.stories.tsx | 0 .../ErrorBoundary/exercise/exercise.tsx | 0 .../final/components/ErrorBoundary.tsx | 0 .../final/components/Fallback.tsx | 0 .../ErrorBoundary/final/final.stories.tsx | 0 .../03-Gold/ErrorBoundary/final/final.tsx | 0 .../03-Gold/ErrorBoundary/lesson.mdx | 0 .../exercise/withPokemon.tsx | 59 ------------ .../final/withPokemon.tsx | 39 -------- src/shared/hooks/usePokedex.ts | 95 ------------------- 12 files changed, 193 deletions(-) rename src/course/{02- lessons => 02-lessons}/03-Gold/ErrorBoundary/exercise/components/ErrorBoundary.tsx (100%) rename src/course/{02- lessons => 02-lessons}/03-Gold/ErrorBoundary/exercise/components/Fallback.tsx (100%) rename src/course/{02- lessons => 02-lessons}/03-Gold/ErrorBoundary/exercise/exercise.stories.tsx (100%) rename src/course/{02- lessons => 02-lessons}/03-Gold/ErrorBoundary/exercise/exercise.tsx (100%) rename src/course/{02- lessons => 02-lessons}/03-Gold/ErrorBoundary/final/components/ErrorBoundary.tsx (100%) rename src/course/{02- lessons => 02-lessons}/03-Gold/ErrorBoundary/final/components/Fallback.tsx (100%) rename src/course/{02- lessons => 02-lessons}/03-Gold/ErrorBoundary/final/final.stories.tsx (100%) rename src/course/{02- lessons => 02-lessons}/03-Gold/ErrorBoundary/final/final.tsx (100%) rename src/course/{02- lessons => 02-lessons}/03-Gold/ErrorBoundary/lesson.mdx (100%) delete mode 100644 src/course/02-lessons/03-Gold/HigherOrderComponents/exercise/withPokemon.tsx delete mode 100644 src/course/02-lessons/03-Gold/HigherOrderComponents/final/withPokemon.tsx delete mode 100644 src/shared/hooks/usePokedex.ts diff --git a/src/course/02- lessons/03-Gold/ErrorBoundary/exercise/components/ErrorBoundary.tsx b/src/course/02-lessons/03-Gold/ErrorBoundary/exercise/components/ErrorBoundary.tsx similarity index 100% rename from src/course/02- lessons/03-Gold/ErrorBoundary/exercise/components/ErrorBoundary.tsx rename to src/course/02-lessons/03-Gold/ErrorBoundary/exercise/components/ErrorBoundary.tsx diff --git a/src/course/02- lessons/03-Gold/ErrorBoundary/exercise/components/Fallback.tsx b/src/course/02-lessons/03-Gold/ErrorBoundary/exercise/components/Fallback.tsx similarity index 100% rename from src/course/02- lessons/03-Gold/ErrorBoundary/exercise/components/Fallback.tsx rename to src/course/02-lessons/03-Gold/ErrorBoundary/exercise/components/Fallback.tsx diff --git a/src/course/02- lessons/03-Gold/ErrorBoundary/exercise/exercise.stories.tsx b/src/course/02-lessons/03-Gold/ErrorBoundary/exercise/exercise.stories.tsx similarity index 100% rename from src/course/02- lessons/03-Gold/ErrorBoundary/exercise/exercise.stories.tsx rename to src/course/02-lessons/03-Gold/ErrorBoundary/exercise/exercise.stories.tsx diff --git a/src/course/02- lessons/03-Gold/ErrorBoundary/exercise/exercise.tsx b/src/course/02-lessons/03-Gold/ErrorBoundary/exercise/exercise.tsx similarity index 100% rename from src/course/02- lessons/03-Gold/ErrorBoundary/exercise/exercise.tsx rename to src/course/02-lessons/03-Gold/ErrorBoundary/exercise/exercise.tsx diff --git a/src/course/02- lessons/03-Gold/ErrorBoundary/final/components/ErrorBoundary.tsx b/src/course/02-lessons/03-Gold/ErrorBoundary/final/components/ErrorBoundary.tsx similarity index 100% rename from src/course/02- lessons/03-Gold/ErrorBoundary/final/components/ErrorBoundary.tsx rename to src/course/02-lessons/03-Gold/ErrorBoundary/final/components/ErrorBoundary.tsx diff --git a/src/course/02- lessons/03-Gold/ErrorBoundary/final/components/Fallback.tsx b/src/course/02-lessons/03-Gold/ErrorBoundary/final/components/Fallback.tsx similarity index 100% rename from src/course/02- lessons/03-Gold/ErrorBoundary/final/components/Fallback.tsx rename to src/course/02-lessons/03-Gold/ErrorBoundary/final/components/Fallback.tsx diff --git a/src/course/02- lessons/03-Gold/ErrorBoundary/final/final.stories.tsx b/src/course/02-lessons/03-Gold/ErrorBoundary/final/final.stories.tsx similarity index 100% rename from src/course/02- lessons/03-Gold/ErrorBoundary/final/final.stories.tsx rename to src/course/02-lessons/03-Gold/ErrorBoundary/final/final.stories.tsx diff --git a/src/course/02- lessons/03-Gold/ErrorBoundary/final/final.tsx b/src/course/02-lessons/03-Gold/ErrorBoundary/final/final.tsx similarity index 100% rename from src/course/02- lessons/03-Gold/ErrorBoundary/final/final.tsx rename to src/course/02-lessons/03-Gold/ErrorBoundary/final/final.tsx diff --git a/src/course/02- lessons/03-Gold/ErrorBoundary/lesson.mdx b/src/course/02-lessons/03-Gold/ErrorBoundary/lesson.mdx similarity index 100% rename from src/course/02- lessons/03-Gold/ErrorBoundary/lesson.mdx rename to src/course/02-lessons/03-Gold/ErrorBoundary/lesson.mdx diff --git a/src/course/02-lessons/03-Gold/HigherOrderComponents/exercise/withPokemon.tsx b/src/course/02-lessons/03-Gold/HigherOrderComponents/exercise/withPokemon.tsx deleted file mode 100644 index b33eaa1..0000000 --- a/src/course/02-lessons/03-Gold/HigherOrderComponents/exercise/withPokemon.tsx +++ /dev/null @@ -1,59 +0,0 @@ -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 1A import IPokemonManagerState, PokemonManager from './PokemonManager - -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 1B Setup an interface called IPokemonManagerActions -// It will contain just fetchPokemons: (total: number) => Promise; in the interface - -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 1C Creating the HOC -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 1C.a export const withPokemons. -// โœ๐Ÿป In typescript you can use something called generics which helps you set specific types -// For specific scenarios. In our scenario, we want to have two Generic types where we specify -// What data we need from state and what actions we need from state. This will make more sense -// When we set it up in the exercise file. Example: -// export const Func = ( -// prop: T -// ) => {} -// Func(); -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป 1C.b We need our props to look like this -// export const withPokemons = ( -// mapper: (state: IPokemonManagerState) => TMapperResponse, -// actions: (actions: IPokemonManagerActions) => TActionResponse -// ) => {} - -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป1D - Returning the return -// So the whole point of the HOC is to pass a Component into the first or second called For example: -// withHOC(Component) or withHOC(funcA)(Component); -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป1D.a We need to Return a function which looks like this: -// (Component: React.FC) => {} -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป1D.b And then we want to return another function -// (props: any) - we could type this better but typescript isn't the core of this lesson. - -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป1E - Managing state from a class -// We need to: -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป1E.a - Create a useState with the initial value of new PokemonManager().getState() -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป1E.b - Create a variable with useMemo(() => new PokemonManager(), []); -// โœ๐Ÿป This is so we can prevent it from calling itself everytime the component re-rendered. -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป1E.c - return the component with the following: -// return ( -// { -// await pokemonManager.fetchPokemons(total); -// setPokemonManagerState(pokemonManager.getState()); -// } -// })} -// /> -// ); - -// ๐ŸŽ‰ Finished with this file. -// Take a step back and reflect what you have just done because this was more than just a HOC. -// What you did was: -/** - * ๐ŸŽ‰ You made a complicated higher order component which did a double return based on the props it took. - * ๐ŸŽ‰ We pretty much wrote the high level thinking of the old way of doing redux connect - * ๐ŸŽ‰ You delve into TypeScript Generics and made this HOC more flexible. - * ๐ŸŽ‰ You used static logic within an ES6 class and used react purely for state management. - */ - -// ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Let's get this wired up into the presentational layer. Head over to exercise.tsx. diff --git a/src/course/02-lessons/03-Gold/HigherOrderComponents/final/withPokemon.tsx b/src/course/02-lessons/03-Gold/HigherOrderComponents/final/withPokemon.tsx deleted file mode 100644 index 5095bca..0000000 --- a/src/course/02-lessons/03-Gold/HigherOrderComponents/final/withPokemon.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { useMemo, useState } from 'react'; -import { - IPokemonManagerState, - PokemonManager -} from '@shared/modules/PokemonManager/PokemonManager'; - -export interface IPokemonManagerActions { - fetchPokemons: (total: number) => Promise; -} - -export const withPokemons = < - TMapperResponse, - TActionResponse, - TProps ->( - mapper: (state: IPokemonManagerState) => TMapperResponse, - actions: (actions: IPokemonManagerActions) => TActionResponse -) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (Component: any) => (props: TProps) => { - const pokemonManager = useMemo(() => new PokemonManager(), []); - const [pokemonManagerState, setPokemonManagerState] = useState( - pokemonManager.getState() - ); - - return ( - { - await pokemonManager.fetchPokemons(total); - setPokemonManagerState(pokemonManager.getState()); - } - })} - /> - ); - }; -}; diff --git a/src/shared/hooks/usePokedex.ts b/src/shared/hooks/usePokedex.ts deleted file mode 100644 index 11035e3..0000000 --- a/src/shared/hooks/usePokedex.ts +++ /dev/null @@ -1,95 +0,0 @@ -// define the api types like a generic - -import { useEffect, useReducer } from 'react'; - -export type TPokemonCardsApiResponse = { - id: string; - name: string; - images: { - small: string; - }; -}; - -export type TPokemonTypesApiResponse = string; - -type TTypesApi = { - path: 'types'; - skip?: boolean; - queryParams?: string; - fail?: boolean; -}; - -type TCardsApi = { - path: 'cards'; - queryParams?: string; - skip?: boolean; - fail?: boolean; -}; - -interface IUsePokedexState { - data?: TResponse; - isError?: boolean; - isLoading?: boolean; -} - -const usePokedexReducer = ( - state: IUsePokedexState, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - action: { type: string; payload?: any } -): IUsePokedexState => { - switch (action.type) { - case 'SUCCESS': - return { ...state, isLoading: false, data: action.payload }; - case 'LOADING': - return { ...state, isLoading: true }; - case 'ERROR': - return { ...state, isLoading: false }; - default: - return state; - } -}; - -export const usePokedex = ({ - path, - queryParams = '', - skip = false, - fail = false -}: TCardsApi | TTypesApi): IUsePokedexState => { - const [state, dispatch] = useReducer(usePokedexReducer, { - isError: false, - isLoading: false, - data: undefined - }); - - useEffect(() => { - if (skip) return; - - const getData = async () => { - try { - dispatch({ type: 'LOADING' }); - const response = await fetch( - `https://api.pokemontcg.io/v2/${path}?${queryParams}` - ); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const json = await response.json(); - - if (fail) { - throw new Error('Error'); - } - - dispatch({ type: 'SUCCESS', payload: json.data }); - } catch (e) { - dispatch({ type: 'ERROR' }); - console.error('Failed to fetch Pokemon data:', e instanceof Error ? e.message : 'Unknown error'); - } - }; - - getData(); - }, [skip, path, queryParams]); - - return state; -}; From 36a3ad29ea493b698c5a01dab27966f39e1ded35 Mon Sep 17 00:00:00 2001 From: code-mattclaffey Date: Thu, 4 Sep 2025 21:07:03 +0100 Subject: [PATCH 25/29] chore: tidy up config --- .eslintrc.cjs => config/.eslintrc.cjs | 0 config/postcss.config.cjs | 7 ++ config/tailwind.config.cjs | 21 ++++++ vite.config.ts => config/vite.config.ts | 4 +- package.json | 8 +-- postcss.config.cjs | 8 +-- src/shared/hooks/usePokedex.ts | 95 +++++++++++++++++++++++++ tailwind.config.cjs | 22 +----- tsconfig.app.json | 31 -------- tsconfig.json | 30 +++++--- tsconfig.node.json | 16 ----- 11 files changed, 154 insertions(+), 88 deletions(-) rename .eslintrc.cjs => config/.eslintrc.cjs (100%) create mode 100644 config/postcss.config.cjs create mode 100644 config/tailwind.config.cjs rename vite.config.ts => config/vite.config.ts (62%) create mode 100644 src/shared/hooks/usePokedex.ts delete mode 100644 tsconfig.app.json delete mode 100644 tsconfig.node.json diff --git a/.eslintrc.cjs b/config/.eslintrc.cjs similarity index 100% rename from .eslintrc.cjs rename to config/.eslintrc.cjs diff --git a/config/postcss.config.cjs b/config/postcss.config.cjs new file mode 100644 index 0000000..a609618 --- /dev/null +++ b/config/postcss.config.cjs @@ -0,0 +1,7 @@ +/* eslint-disable no-undef */ +module.exports = { + plugins: { + tailwindcss: { config: './config/tailwind.config.cjs' }, + autoprefixer: {}, + }, +} diff --git a/config/tailwind.config.cjs b/config/tailwind.config.cjs new file mode 100644 index 0000000..fe723b7 --- /dev/null +++ b/config/tailwind.config.cjs @@ -0,0 +1,21 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{js,jsx,ts,tsx,mdx}'], + darkMode: ['class', '[data-mode="dark"]'], + theme: { + extend: { + colors: { + modal: { + bg: 'rgba(0, 0, 0, 0.3)' + }, + code: { + 950: '#1E1E3F', + 750: '#2D2B55', + 600: '#a03fc0', + 500: '#A599E9' + } + } + } + }, + plugins: [] +}; diff --git a/vite.config.ts b/config/vite.config.ts similarity index 62% rename from vite.config.ts rename to config/vite.config.ts index 6549182..7185270 100644 --- a/vite.config.ts +++ b/config/vite.config.ts @@ -1,7 +1,9 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()] + plugins: [react()], + root: resolve(__dirname, '..') }); diff --git a/package.json b/package.json index 42b0557..0e4a63a 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,10 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives", - "preview": "vite preview", + "dev": "vite --config config/vite.config.ts", + "build": "tsc && vite build --config config/vite.config.ts", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --config config/.eslintrc.cjs", + "preview": "vite preview --config config/vite.config.ts", "storybook": "storybook dev -p 6006", "build-storybook": "npm run build && storybook build -o dist/storybook", "test": "test-storybook", diff --git a/postcss.config.cjs b/postcss.config.cjs index 586f6d9..9e0729d 100644 --- a/postcss.config.cjs +++ b/postcss.config.cjs @@ -1,7 +1 @@ -/* eslint-disable no-undef */ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} +module.exports = require('./config/postcss.config.cjs'); \ No newline at end of file diff --git a/src/shared/hooks/usePokedex.ts b/src/shared/hooks/usePokedex.ts new file mode 100644 index 0000000..11035e3 --- /dev/null +++ b/src/shared/hooks/usePokedex.ts @@ -0,0 +1,95 @@ +// define the api types like a generic + +import { useEffect, useReducer } from 'react'; + +export type TPokemonCardsApiResponse = { + id: string; + name: string; + images: { + small: string; + }; +}; + +export type TPokemonTypesApiResponse = string; + +type TTypesApi = { + path: 'types'; + skip?: boolean; + queryParams?: string; + fail?: boolean; +}; + +type TCardsApi = { + path: 'cards'; + queryParams?: string; + skip?: boolean; + fail?: boolean; +}; + +interface IUsePokedexState { + data?: TResponse; + isError?: boolean; + isLoading?: boolean; +} + +const usePokedexReducer = ( + state: IUsePokedexState, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + action: { type: string; payload?: any } +): IUsePokedexState => { + switch (action.type) { + case 'SUCCESS': + return { ...state, isLoading: false, data: action.payload }; + case 'LOADING': + return { ...state, isLoading: true }; + case 'ERROR': + return { ...state, isLoading: false }; + default: + return state; + } +}; + +export const usePokedex = ({ + path, + queryParams = '', + skip = false, + fail = false +}: TCardsApi | TTypesApi): IUsePokedexState => { + const [state, dispatch] = useReducer(usePokedexReducer, { + isError: false, + isLoading: false, + data: undefined + }); + + useEffect(() => { + if (skip) return; + + const getData = async () => { + try { + dispatch({ type: 'LOADING' }); + const response = await fetch( + `https://api.pokemontcg.io/v2/${path}?${queryParams}` + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const json = await response.json(); + + if (fail) { + throw new Error('Error'); + } + + dispatch({ type: 'SUCCESS', payload: json.data }); + } catch (e) { + dispatch({ type: 'ERROR' }); + console.error('Failed to fetch Pokemon data:', e instanceof Error ? e.message : 'Unknown error'); + } + }; + + getData(); + }, [skip, path, queryParams]); + + return state; +}; diff --git a/tailwind.config.cjs b/tailwind.config.cjs index fe723b7..79e7785 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -1,21 +1 @@ -/** @type {import('tailwindcss').Config} */ -export default { - content: ['./src/**/*.{js,jsx,ts,tsx,mdx}'], - darkMode: ['class', '[data-mode="dark"]'], - theme: { - extend: { - colors: { - modal: { - bg: 'rgba(0, 0, 0, 0.3)' - }, - code: { - 950: '#1E1E3F', - 750: '#2D2B55', - 600: '#a03fc0', - 500: '#A599E9' - } - } - } - }, - plugins: [] -}; +module.exports = require('./config/tailwind.config.cjs'); \ No newline at end of file diff --git a/tsconfig.app.json b/tsconfig.app.json deleted file mode 100644 index 23ed661..0000000 --- a/tsconfig.app.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "paths": { - "@shared/*": ["./src/shared/*"] - } - }, - "include": ["src"], - "ignore": ["src/**/*.mdx"] -} diff --git a/tsconfig.json b/tsconfig.json index ea9d0cd..1ef66de 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,25 @@ { - "files": [], - "references": [ - { - "path": "./tsconfig.app.json" - }, - { - "path": "./tsconfig.node.json" + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "paths": { + "@shared/*": ["./src/shared/*"] } - ] + }, + "include": ["src", "config/vite.config.ts"], + "exclude": ["src/**/*.mdx"] } diff --git a/tsconfig.node.json b/tsconfig.node.json deleted file mode 100644 index f89e416..0000000 --- a/tsconfig.node.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true, - "strict": true, - "noEmit": true, - "paths": { - "@shared/*": ["./src/shared/*"] - } - }, - "include": ["vite.config.ts"] -} From b1c66f16750b9c4702c0b8b5d0fe16c203239c59 Mon Sep 17 00:00:00 2001 From: code-mattclaffey Date: Thu, 4 Sep 2025 21:09:26 +0100 Subject: [PATCH 26/29] docs: update reamde --- README.md | 152 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 127 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 0b154a3..f7821c9 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,143 @@ -# React + TypeScript + Vite +# ๐ŸŽฎ React Design Patterns -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +A comprehensive interactive course teaching React design patterns through Pokemon-themed examples. Learn advanced React patterns with hands-on exercises and real-world applications. -Currently, two official plugins are available: +## ๐Ÿš€ What's Inside -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +This repository contains a complete React design patterns course built with: +- **React 19** with TypeScript +- **Storybook** for interactive lessons and exercises +- **Tailwind CSS** for styling +- **Vite** for fast development +- **Pokemon theme** to make learning engaging -## Expanding the ESLint configuration +## ๐Ÿ“š Course Structure -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: +### ๐Ÿฅ‰ Bronze Level (Fundamentals) +- **Conditional Rendering** - Dynamic UI based on state +- **Props Combination** - Grouping related props for cleaner APIs +- **React Hooks** - Modern state management and side effects +- **Presentational & Container** - Separating UI from business logic +- **Slots Pattern** - Flexible component composition -- Configure the top-level `parserOptions` property like this: +### ๐Ÿฅˆ Silver Level (Intermediate) +- **Compound Components** - Building flexible, composable APIs +- **Controlled Components** - Managing form state externally +- **Uncontrolled Components** - Letting components manage their own state +- **FACC Pattern** - Function as Child Components +- **Render Children** - Advanced component composition +- **Render Props** - Sharing logic between components +- **Provider Pattern** - Global state management with Context +- **State Reducer** - Complex state logic management +- **Portals** - Rendering outside component hierarchy +- **Polymorphic Components** - Flexible component APIs -```js -export default { - // other rules... - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - project: ['./tsconfig.json', './tsconfig.node.json'], - tsconfigRootDir: __dirname, - }, -} +### ๐Ÿฅ‡ Gold Level (Advanced) +- **Higher Order Components** - Component enhancement and reuse +- **Suspense & Lazy Loading** - Code splitting and async components +- **Headless Components** - Logic without UI for maximum flexibility + +## ๐Ÿ› ๏ธ Getting Started + +### Prerequisites +- Node.js 18+ +- npm or yarn + +### Installation + +```bash +# Clone the repository +git clone https://github.com/code-mattclaffey/react-design-patterns.git + +# Navigate to project +cd react-design-patterns + +# Install dependencies +npm install + +# Start Storybook (recommended) +npm run storybook + +# Or start development server +npm run dev ``` -- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` -- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list +### Using Storybook (Recommended) +The course is designed to be experienced through Storybook: -## Troubleshooting +```bash +npm run storybook +``` -When you install if you get this error: +Navigate through the lessons in the sidebar: +1. **Introduction** - Course overview and setup +2. **Lessons** - Interactive exercises organized by difficulty +3. **Each lesson includes:** + - Theory and examples + - Exercise files with guided tasks + - Final implementations + - When to use each pattern + +## ๐Ÿ“ Project Structure + +``` +src/ +โ”œโ”€โ”€ course/ +โ”‚ โ”œโ”€โ”€ 01-introduction/ # Course welcome and overview +โ”‚ โ””โ”€โ”€ 02-lessons/ +โ”‚ โ”œโ”€โ”€ 01-Bronze/ # Fundamental patterns +โ”‚ โ”œโ”€โ”€ 02-Silver/ # Intermediate patterns +โ”‚ โ””โ”€โ”€ 03-Gold/ # Advanced patterns +โ”œโ”€โ”€ shared/ +โ”‚ โ”œโ”€โ”€ components/ # Reusable UI components +โ”‚ โ”œโ”€โ”€ hooks/ # Custom React hooks +โ”‚ โ””โ”€โ”€ modules/ # Business logic modules +config/ # Build and linting configuration +``` + +## ๐ŸŽฏ Learning Approach + +Each lesson follows a consistent structure: +1. **Problem Introduction** - Real-world scenario +2. **Pattern Explanation** - Theory with code examples +3. **Hands-on Exercise** - Guided implementation +4. **Final Solution** - Complete working example +5. **When to Use** - Practical guidance for real projects + +## ๐Ÿงช Available Scripts ```bash -Error: Cannot find module './node_modules/browser-assert/lib/assert.js'. Please verify that the package.json has a valid "main" entry +# Development +npm run dev # Start Vite dev server +npm run storybook # Start Storybook (recommended) + +# Building +npm run build # Build for production +npm run build-storybook # Build Storybook for deployment + +# Quality +npm run lint # Run ESLint +npm run test # Run Storybook tests ``` -Just remove node modules and reinstall. +## ๐Ÿค Contributing + +Contributions are welcome! Please: +1. Fork the repository +2. Create a feature branch +3. Follow the existing code style +4. Add tests for new patterns +5. Submit a pull request + +## ๐Ÿ“ Feedback + +Found an issue or have suggestions? Please [open an issue](https://github.com/code-mattclaffey/react-design-patterns/issues/new) on GitHub. + +## ๐Ÿ“„ License + +This project is open source and available under the [MIT License](LICENSE). + +--- + +**Happy learning! ๐Ÿš€** Start your React design patterns journey with `npm run storybook` From 72bb52c9f6d91ee95597f772daffde702eac9e47 Mon Sep 17 00:00:00 2001 From: code-mattclaffey Date: Thu, 4 Sep 2025 21:50:53 +0100 Subject: [PATCH 27/29] feat: implement better landing page --- index.html | 4 ++ src/App.tsx | 30 +------- src/app/LandingPage.tsx | 19 +++++ src/app/components/CallToAction.tsx | 39 +++++++++++ src/app/components/CourseStructure.tsx | 52 ++++++++++++++ src/app/components/Features.tsx | 37 ++++++++++ src/app/components/Footer.tsx | 97 ++++++++++++++++++++++++++ src/app/components/Header.tsx | 94 +++++++++++++++++++++++++ src/app/components/Hero.tsx | 60 ++++++++++++++++ src/app/content/courseData.ts | 83 ++++++++++++++++++++++ src/app/content/index.ts | 1 + src/app/icons/DotPattern.tsx | 9 +++ src/app/icons/index.ts | 1 + 13 files changed, 498 insertions(+), 28 deletions(-) create mode 100644 src/app/LandingPage.tsx create mode 100644 src/app/components/CallToAction.tsx create mode 100644 src/app/components/CourseStructure.tsx create mode 100644 src/app/components/Features.tsx create mode 100644 src/app/components/Footer.tsx create mode 100644 src/app/components/Header.tsx create mode 100644 src/app/components/Hero.tsx create mode 100644 src/app/content/courseData.ts create mode 100644 src/app/content/index.ts create mode 100644 src/app/icons/DotPattern.tsx create mode 100644 src/app/icons/index.ts diff --git a/index.html b/index.html index 65687d8..6401738 100644 --- a/index.html +++ b/index.html @@ -7,6 +7,10 @@ name="viewport" content="width=device-width, initial-scale=1.0" /> + React Design Patterns diff --git a/src/App.tsx b/src/App.tsx index eb4bbde..25b26ae 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,36 +1,10 @@ import { Analytics } from '@vercel/analytics/react'; +import { LandingPage } from './app/LandingPage'; const App = () => { return ( <> -
    -

    - โš›๏ธ React Design Patterns -

    -

    - Welcome to React Design Patterns ๐Ÿ‘‹๐Ÿป!

    This - course educates developers on best practices for writing - React components, utilizing patterns and providing practical - guides with real-world examples. -

    - - Get started - -

    - Made with โค๏ธ by Matt Claffey{' '} - - @code-mattclaffey - -

    -
    + ); diff --git a/src/app/LandingPage.tsx b/src/app/LandingPage.tsx new file mode 100644 index 0000000..d99e933 --- /dev/null +++ b/src/app/LandingPage.tsx @@ -0,0 +1,19 @@ +import { Header } from './components/Header'; +import { Hero } from './components/Hero'; +import { CourseStructure } from './components/CourseStructure'; +import { CallToAction } from './components/CallToAction'; +import { Footer } from './components/Footer'; +import { Features } from './components/Features'; + +export const LandingPage = () => { + return ( +
    +
    + + + + +
    +
    + ); +}; diff --git a/src/app/components/CallToAction.tsx b/src/app/components/CallToAction.tsx new file mode 100644 index 0000000..5f3b86f --- /dev/null +++ b/src/app/components/CallToAction.tsx @@ -0,0 +1,39 @@ +export const CallToAction = () => { + return ( +
    +
    +

    + Ready to Master React Patterns? +

    +

    + Join thousands of developers who have leveled up their React skills with our interactive course. +

    + + + +
    +
    +
    15+
    +
    Design Patterns
    +
    +
    +
    50+
    +
    Interactive Examples
    +
    +
    +
    100%
    +
    Free & Open Source
    +
    +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/src/app/components/CourseStructure.tsx b/src/app/components/CourseStructure.tsx new file mode 100644 index 0000000..f87fcb3 --- /dev/null +++ b/src/app/components/CourseStructure.tsx @@ -0,0 +1,52 @@ +import { courseLevels } from '../content'; + +export const CourseStructure = () => { + return ( +
    +
    +
    +

    + Progressive Learning Path +

    +

    + Master React patterns step by step, from fundamental concepts to advanced techniques +

    +
    + +
    + {courseLevels.map((level, index) => ( +
    +
    +
    + {level.emoji} +
    +

    + {level.title} +

    +

    {level.subtitle}

    +
    + +
      + {level.patterns.map((pattern) => ( +
    • +
      + {pattern} +
    • + ))} +
    + +
    + + {String(index + 1).padStart(2, '0')} + +
    +
    + ))} +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/src/app/components/Features.tsx b/src/app/components/Features.tsx new file mode 100644 index 0000000..3d2f722 --- /dev/null +++ b/src/app/components/Features.tsx @@ -0,0 +1,37 @@ +import { features } from '../content'; + +export const Features = () => { + return ( +
    +
    +
    +

    + Why This Course? +

    +

    + Learn React design patterns the right way with interactive examples and real-world context +

    +
    + +
    + {features.map((feature) => ( +
    +
    + {feature.icon} +
    +

    + {feature.title} +

    +

    + {feature.description} +

    +
    + ))} +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/src/app/components/Footer.tsx b/src/app/components/Footer.tsx new file mode 100644 index 0000000..05edf96 --- /dev/null +++ b/src/app/components/Footer.tsx @@ -0,0 +1,97 @@ +export const Footer = () => { + return ( + <> + + + ); +}; diff --git a/src/app/components/Header.tsx b/src/app/components/Header.tsx new file mode 100644 index 0000000..127dc48 --- /dev/null +++ b/src/app/components/Header.tsx @@ -0,0 +1,94 @@ +import { useState } from 'react'; + +export const Header = () => { + const [isMenuOpen, setIsMenuOpen] = useState(false); + + return ( +
    + +
    + ); +}; diff --git a/src/app/components/Hero.tsx b/src/app/components/Hero.tsx new file mode 100644 index 0000000..249b8cd --- /dev/null +++ b/src/app/components/Hero.tsx @@ -0,0 +1,60 @@ +export const Hero = () => { + return ( +
    +
    +
    +
    + + ๐ŸŽฎ Pokemon-Themed Learning + +
    + +

    + Master React Design Patterns +

    + +

    + Learn advanced React patterns through interactive + Pokemon-themed exercises. From fundamentals to + expert-level techniques. +

    + + + +
    +
    +
    + 15+ Patterns +
    +
    +
    + + Interactive Exercises + +
    +
    +
    + Real-world Examples +
    +
    +
    +
    +
    + ); +}; diff --git a/src/app/content/courseData.ts b/src/app/content/courseData.ts new file mode 100644 index 0000000..45a0c2c --- /dev/null +++ b/src/app/content/courseData.ts @@ -0,0 +1,83 @@ +export const courseLevels = [ + { + title: 'Bronze Level', + subtitle: 'Fundamentals', + emoji: '๐Ÿฅ‰', + color: 'from-amber-500 to-orange-600', + bgColor: 'bg-amber-50', + borderColor: 'border-amber-200', + patterns: [ + 'Conditional Rendering', + 'Props Combination', + 'React Hooks', + 'Presentational & Container', + 'Slots Pattern' + ] + }, + { + title: 'Silver Level', + subtitle: 'Intermediate', + emoji: '๐Ÿฅˆ', + color: 'from-slate-400 to-slate-600', + bgColor: 'bg-slate-50', + borderColor: 'border-slate-200', + patterns: [ + 'Compound Components', + 'Controlled Components', + 'Uncontrolled Components', + 'FACC Pattern', + 'Render Children', + 'Render Props', + 'Provider Pattern', + 'State Reducer', + 'Portals', + 'Polymorphic Components' + ] + }, + { + title: 'Gold Level', + subtitle: 'Advanced', + emoji: '๐Ÿฅ‡', + color: 'from-yellow-400 to-yellow-600', + bgColor: 'bg-yellow-50', + borderColor: 'border-yellow-200', + patterns: [ + 'Higher Order Components', + 'Suspense & Lazy Loading', + 'Headless Components' + ] + } +]; + +export const features = [ + { + icon: '๐ŸŽฎ', + title: 'Pokemon-Themed Learning', + description: 'Learn React patterns through engaging Pokemon examples that make complex concepts memorable and fun.' + }, + { + icon: '๐Ÿ“š', + title: 'Interactive Storybook', + description: 'Hands-on exercises with live code examples. See patterns in action and experiment with real implementations.' + }, + { + icon: '๐ŸŽฏ', + title: 'Progressive Difficulty', + description: 'Start with fundamentals and advance to expert-level patterns. Each lesson builds on previous knowledge.' + }, + { + icon: '๐Ÿ”ง', + title: 'Real-World Applications', + description: 'Learn when and how to apply each pattern with practical guidance for production applications.' + }, + { + icon: 'โšก', + title: 'Modern React 19', + description: 'Built with the latest React features including hooks, Suspense, and concurrent rendering patterns.' + }, + { + icon: '๐ŸŽจ', + title: 'Beautiful UI Examples', + description: 'Styled with Tailwind CSS to show how patterns work in polished, production-ready interfaces.' + } +]; \ No newline at end of file diff --git a/src/app/content/index.ts b/src/app/content/index.ts new file mode 100644 index 0000000..03e0d48 --- /dev/null +++ b/src/app/content/index.ts @@ -0,0 +1 @@ +export { courseLevels, features } from './courseData'; \ No newline at end of file diff --git a/src/app/icons/DotPattern.tsx b/src/app/icons/DotPattern.tsx new file mode 100644 index 0000000..95c48aa --- /dev/null +++ b/src/app/icons/DotPattern.tsx @@ -0,0 +1,9 @@ +export const DotPattern = () => ( + + + + + + + +); \ No newline at end of file diff --git a/src/app/icons/index.ts b/src/app/icons/index.ts new file mode 100644 index 0000000..a9faee5 --- /dev/null +++ b/src/app/icons/index.ts @@ -0,0 +1 @@ +export { DotPattern } from './DotPattern'; \ No newline at end of file From 9acbda73829287542ffaccee1f906dedc74c3dd4 Mon Sep 17 00:00:00 2001 From: code-mattclaffey Date: Thu, 4 Sep 2025 21:55:15 +0100 Subject: [PATCH 28/29] fix: pipelines --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9593f00..a7c9ccb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,7 +38,7 @@ jobs: node-version: 20 - name: Install - run: npm ci + run: npm ci --force - name: Security Audit Checks run: npm run audit From 7e83f629cffa36f2ae2cb3e0467edae905f5cf44 Mon Sep 17 00:00:00 2001 From: code-mattclaffey Date: Thu, 4 Sep 2025 22:01:35 +0100 Subject: [PATCH 29/29] fix: styling on landing page --- .storybook/manager.ts | 47 ++++++++++++++--------------- .storybook/styles/docs.styles.css | 49 +++++++++++++++++++++++++++++++ src/app/components/Footer.tsx | 2 +- src/app/components/Header.tsx | 16 +++++++--- 4 files changed, 86 insertions(+), 28 deletions(-) diff --git a/.storybook/manager.ts b/.storybook/manager.ts index f387a78..122026c 100644 --- a/.storybook/manager.ts +++ b/.storybook/manager.ts @@ -9,37 +9,38 @@ const theme = create({ fontCode: 'monospace', brandTitle: 'โš›๏ธ React Design Patterns', - brandUrl: 'https://example.com', - brandTarget: '_self', - colorPrimary: '#ffffff', - colorSecondary: '#A599E9', - - // UI - appBg: '#1E1E3F', - appContentBg: '#2D2B55', + brandUrl: + 'https://github.com/code-mattclaffey/react-design-patterns', + brandTarget: '_blank', + colorPrimary: '#3b82f6', // blue-500 + colorSecondary: '#A599E9', // code-500 from tailwind config + + // UI - matching main page blue-950 theme + appBg: '#172554', // blue-950 + appContentBg: '#1e3a8a', // blue-900 appPreviewBg: '#ffffff', - appBorderColor: '#2D2B55', - appBorderRadius: 4, + appBorderColor: '#1e40af', // blue-800 + appBorderRadius: 8, - // Text colors + // Text colors - matching main page textColor: '#ffffff', - textInverseColor: '#ffffff', + textInverseColor: '#1e293b', // slate-800 - // Toolbar default and active colors - barTextColor: '#ffffff', + // Toolbar colors - matching header + barTextColor: '#cbd5e1', // slate-300 barSelectedColor: '#ffffff', barHoverColor: '#ffffff', - barBg: '#1E1E3F', + barBg: '#172554', // blue-950 // Form colors - inputBg: '#ffffff', - inputBorder: '#35356b', - inputTextColor: '#1E1E3F', - inputBorderRadius: 2, - - // Buttons - buttonBg: '#35356b', - buttonBorder: '#35356b', + inputBg: '#1e40af', // blue-800 + inputBorder: '#3b82f6', // blue-500 + inputTextColor: '#ffffff', + inputBorderRadius: 6, + + // Buttons - matching CTA buttons + buttonBg: '#2563eb', // blue-600 + buttonBorder: '#2563eb', // blue-600 gridCellSize: 24 }); diff --git a/.storybook/styles/docs.styles.css b/.storybook/styles/docs.styles.css index d5b4c92..e843262 100644 --- a/.storybook/styles/docs.styles.css +++ b/.storybook/styles/docs.styles.css @@ -1,8 +1,10 @@ +/* Main content styling to match landing page */ .sbdocs-content p, .sbdocs-content li, .sbdocs-content a { font-size: 1.125rem; line-height: 1.6; + font-weight: 500; } .sbdocs-content h1, @@ -14,20 +16,67 @@ font-weight: 700; font-style: normal; margin-top: 1.5rem !important; + line-height: 1.2; } .sbdocs-content h1 { font-size: 2.5rem; + color: #3b82f6; /* blue-500 accent */ } .sbdocs-content h2 { font-size: 2rem; + color: #2563eb; /* blue-600 */ } .sbdocs-content h3 { font-size: 1.75rem; + color: #1d4ed8; /* blue-700 */ } .sbdocs-content ol { list-style: decimal; } + +/* Code blocks styling */ +.sbdocs-content pre { + background-color: #172554 !important; /* blue-950 */ + border: 1px solid #1e40af; /* blue-800 */ + border-radius: 8px; +} + +/* Links styling to match main page */ +.sbdocs-content a { + color: #3b82f6; /* blue-500 */ + text-decoration: none; + transition: color 0.2s ease; +} + +.sbdocs-content a:hover { + color: #2563eb; /* blue-600 */ + text-decoration: underline; +} + +/* Badge/tag styling */ +.sbdocs-content .docblock-code-toggle { + background-color: #1e40af; /* blue-800 */ + color: white; + border-radius: 6px; +} + +/* Table styling */ +.sbdocs-content table { + border-collapse: collapse; + border-radius: 8px; + overflow: hidden; +} + +.sbdocs-content th { + background-color: #1e40af; /* blue-800 */ + color: white; + font-weight: 600; +} + +.sbdocs-content td { + border-color: #cbd5e1; /* slate-300 */ +} diff --git a/src/app/components/Footer.tsx b/src/app/components/Footer.tsx index 05edf96..6acac96 100644 --- a/src/app/components/Footer.tsx +++ b/src/app/components/Footer.tsx @@ -6,7 +6,7 @@ export const Footer = () => {
    -
    +
    โš›๏ธ
    React Design Patterns diff --git a/src/app/components/Header.tsx b/src/app/components/Header.tsx index 127dc48..4aaf497 100644 --- a/src/app/components/Header.tsx +++ b/src/app/components/Header.tsx @@ -16,7 +16,7 @@ export const Header = () => { -

    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Ꮽ)n|d5o5fd8CUZ*Uv@#<55LN46Z=yd5DKIO42*{oO` zyz`$ms_;FuZr}tijuP*jCW`*mX>HFDTsv@@yNBu%s>M|~Af}`B>G>anc^haOf0;lo z%=FvCOEb;k$_}pLM@uTeywq6i~5!zw%)XB5B}k z|9E<@>l@{$`C+=Fh4ORY`tO)U1|@=0UOv?vDOmU?AauP0LKnhcs09r*pom3LhtuVI z__e*p-)A%#EPRjoRVb$CGeJUpEoJFUW7T(3XG4gIhz6FiM0ghtP$GNZ0Fse->Ojs_ zro+Win^DJ3EPy63qkE2->FJ}g+y-xUavqzL{tc+9GpAkjWQI$#6(!ITAFf6{rbgVi z96C@`%=X<&@E4!nW|dR)Q3W{b)*40ZquC9TLvoPS@*+x;TWOwC1n@xV&I_)ty3GzT zHfS0nhXTJ|B=?6$BJOo!z9=L*A@bsu=#HoqO%QQ3I{9)bM(JnozP@u3nh;6Eh@ELc z{|~hkpM{F1rFP<1h0#mvu6Uh4JFG2J)><&Ui5hB_FivTi}d=cO;ezwhq;Y2Y$7OluL@hs z{+I&;#n^l%*r+p?VCIIlmpwYxO)e`W(7mdEU{n77Bn4K0Nn%*ugCx2$C{76uWneKz zu^F(#sWeZod?g(^Nl2UiDk511o!TKR;ejL|p_@(6dl#g+LyP#}xAAxZ*X)U-V+Caq zmYu~53mYTa7vvGMT528N|C}r#*jabRfGx0woZy$aO7+;(WQQo?@JYnVr`=l_HOzVJ zJGi$1anDp!=*0eM?*y3mJSFwX07(k|N5*Lleb0v!mA&R3w!zC4nYxf1`d1C;60K@e zI#R_L9V%m6h+QRxaH*OSm&N?YR7cz-t=PoEiL8P``ZOX1UZ3BeEzWwL=Scg3-J29o z`5Wl-=mV)%x10hMFpHq2!5ndML6vOgl2J6PFV{X<8RE<~arz(Ws;Zm@KqBH#GuID~ zm*(PDDvu%$RXRBe@)FmO>_W}v%qZjm5f_n9D7-RSEd?4L2c`EqNm%J zpXQRTse)zV5!(>?vF1jt4`8oHW2!HLxRABjUZk~PX^#kWMbtM@2Sflm(0=0t(h#!g z-BM&A*Y6E95x;x!N!u5Q3!^0+Z{{gJf!(OTTM68nc@oQ&kbB%oGLzPV?OAAuYUAGzxbrq=G zr;fE$0#kT8dEj?;pStJew%?x^V9pn)#bXy4W`eYB-}nbs0yBH3=M6+VR4p*y>K zM%32~!w1FW%`_4v$c-I_d~P?Do<7z^Fn^YZqN7qu%^HEwIoF(T75R2HsFSuY=*aKG za-I%X$qJ3~peg3WFPJ1&KN-Z}z^Iwn!D6^HLZC;#T!hqT?|Hf*2h8fB^e%5-3-6_8 zMZAM&w!s(9u$~?&;q0Ii8IK6D=@*;Yi~k;d9GlTOozyy~T5n)Ai^)}3*X1Q;U=vjl z|35W3V!|{GT5=f4VUxc`I{AicI}fWJ%--m~CkJqQdQ(8e>4Q>IMzN>W3MA+KzfV3< z*dYAdIVX{gT|t?0JWzxE13x+XB@OeYFqje>>2dF{blr}BT<*?n#=rc)ojaTf*##Af z7Ho<#r1}J{!xA`pWw+EQ_vC)b?YI1m(!Gwbs~`g4v3c*4w2n!VOZ+7d*&dtB@D7-B z3GEc+OBou!YTG)%^AEvvGhCOa*P%Ds@VxuYib^U4!#X6hKCo(MZPLa-O*c(Wx!Pe0orwhvut&a$%j!##6M?|!wcqlO} zscUy9W)TRmP??`S$G8g6rk(VS9;YV)lhn(vebx}!W1=mP5ltai+OYzK!H?;Q8;P^~ zWKq?BLrL$fDMn&T2l>%ve}!sn%`W9tIT%4yQp*t1hI<&)JUxe5fK~5M*n0 z%E+N1QlLZw-v!_HLSs-aT^^qh8XKnuj78pg9*#95WenlPO8T~hU?n>q8j0=P-Xe{| ziX5)TaiLn7w>IIqNQnOyP=+1D_|9EC7bJll91WpqiP(+9#Uf5r2;W5i0&n>A)6*jv z9r>k<;*&4kgCxycz80@oMhE>A@|X?CgU zGnRq1*G@>sgRJYGqr&;Hd6j%#8r+TQ?aikWHjDi)*ncL~lQy^u=4t>* zxK2f;vY6mMZw~3>egx(!oF(+bP!~E7jSBw_ze#G01&%)h`-a|4D=9-5foZWRCW9Jsv#(6vKa~rA{E2R#}UIEvp zORYX!(v7)tQtk)U5bvKF3!Q#^JmZ$Eri>t>c7FeZ*NF3kneobR8hv#VppCDC zR9RC!OW@Zpel=a>|m$VkJ%Ab#q4g zhzZq1#u#awa0wHM!EUK2V4G%Khml^K1)ggU7%;{mb%0qGOZe`FKKy@P-C9xT;X1U@ z3kFiJpr9zAQT?}!a;*5LNq#_4SX|*cS?4R;pWdv!e@mBHY50n;L<~(Sq`%umwo70} zagr!#aLwucU_6X1BuE$UZEVBIvc_?xE1oZ-bTQ@V@ww3`pv<+R0WUdqjq!P1bpWwn5tve3;ek`v%vNW1@uEA;XACN5>9cmB) z#I--#8;s9>_vxjO4X`D&lcuSD&{^s8%BVpI_Q`n^z-R2<4n99QPeI16E%THrn^^r6 zp@&pCL?m4S)aNq;Vy_!fi-KX8vrIUzoL5<;i7J(gbyD(YxS$8IB9L>nWkPo!iFd%> zb>x8Gausa-)`PopXqwQsYvTb^!M<_Zh?#W79_JUDmZdkH3GUWrV;bY$4 z6A8u*#YeNg;1Pu9o$fE|J-uS%-|;EhdC}Jg@4Mf=k=CNs+VRMLONYvy7B#FCnlkD-|p&F>x3_e8B zz90#)$rNfMRMJ)*(8BSoJFoZAk8wU&Y~YbE8?r$`dN&=^!)rQ*NE1?mY{T#kg@mEo zzWx(QFwtq9iO_IP38CxygpO3=Ta5QB^ZOHKB+^YO*wK3z12dRxP z2#R3&)(E%5PRn6o)RM&{OItYIGXuH^d>Rv67~|FhlRhG@ogz}^Kg&_!mS5Ous&HvC zSo~43xedkJM6rBVN2`l{Fh$BU;?8rhdMp7B9ty#d>=&W%_=jdk;l9C{S%D{`19p!g zRY*C2_2H{O=^+ovTy}JL`GNz=UJ&bu%1pwX1f~;jOyl(-gp+fO5+ZvQi~&|%{sUf71uC4GcnJJo*8i&dVixd< zi$G%T@dOz{S6N(_yu_?%Ak&=uKcQxBYkq$7?vUv;K4WoTk%v^Ckc~4MR;N#0A@|2L(Stys;fa7FGH5DWa*cl(=Hp*J(e3oy9?#Z><0)+q5c;;*DML- zK59_7!57@H7@lzGVqr0gTkt6DUpkZrRaKiRZy+<5ud`c)nkxjaBLhDXVo`++DFqYK zFV(z6w$F{U*&MbwDu2JhX076yDBp4{FA88OB|*q5oJl$2_c$p2Mn?Yc5SNmvP2>LQkE^`+s8TjM&EhG7H%i-QG4YL5eySVNX}Let=yB&e+*x$Xc^~nA;;CYJjR% z+?gvJX~ny4$ELmEQZDFwlsn&yX_6#5`IW795GX_e9rJta-+$fOf^sID&K3MLDk$H+ zAm^)=pQ#?H(e%&P=UnoRO~VAa9(J?**HdY;(ODJNvDSJ5C(B(Vn+S??!{*4L(fJD$ z?}Z)v-9*sksT zOBXPTSHXxP)D3(1Z}Y#RSClSKhq<1)vMIuNST2Up1c!0vAZV1z`L9cjZSbB;52_y? zmEFbGB+YzQsn;iqh2{#{V~FO574+3S1^q@|LC4FNQ5;+cRukuFEIC$W&+*|LbO32*3~u|9e`? zG|o55OJs*w?~a%3#(n_eahpjRD+|FPA3*>`f!;32gM6>1!8#=7t}yh@&g#sOP4$Q{ zQ{`pEl((5KUvo@8>MY3y`Eb&IuG*SJLBKb}jW3E-gQ>!GSP>nua(LScX50)2XG%6@ zqcmm1SaAerC~uefRaYkmdB-U9{vQ96(00W41G!S7PcrZwDqJH3UnSY5XrLD=(e^^B zWOfC-!ap>>{#AG0L|<5wUg-sAq}>$G(^H8nY&(k`R(1K!rZ$ht;n-WBc1e^P{+W9* z44habdIO13HuWCde@9W^vzzAK47)uCr}Tn`vg-TZ&b954=_AI4h(SY5P~|9!S;`z)D>x_&lL3$DKnQSq(U8Yqp zRxOIphJ9p^IlkbD+f65E#@oOgHe=TkS)kQ-)C-%`21Um7E#+>siSVWIAp8D= zf1giLe6^LyJ{e^swjhO_;?E$8MH>O9Cgx?FG{o+JI+Pv<26_GU0R-_NbfOIy5GXxy zCJ&B$7Tm>kp36Y^7O1%G%Y)N)w4FClVWnQ^SpfSc_ zLDp@(e@u^v_J)NwA_fbnW4#37XayBYGywV)|pC2gYPn>7Hgr?~^UAQ`vEl^FYZ4jktpTZ3` zkqKu_u&?d%E_ITt$Al3A71>9CzzZ)0Jn8$c`}$Ka%M~L5I;Q+096(C1xv6h+neX>t z2%Ovov4B`U3dB`M0JP%_+oV$YWyP5C)##$R+S2FrX%GFvyO7I^ObmhqZyFDRzMD}G zwc`0}+VRm>OfA&{Gflz;P}HQeMab!p8Kxw)8S(e+!8tw3<*)I|&&B;Md?Ad-4uDia zc=Joz9K5yKxSYOdnG<|GOZCS)z8-hqHtip*e1&a94jKoL-EynJxkbEqq?i(FqP(XrydMj9nK1T002@K%z7jSzOpWo{92xf5mVTkV9tEbB zxcmKtOtC)3`x?s@K?QKktV#_kAk)%;lPjOY&zIkT@67x&7CJtI8VrDF#-aj*s^IC3 zr{kkBBKbIOea)|#@v|4N#os@@9AUFHI^m=dNsACr0wLUX(m!$ha z!21@JsL(Md%@6?5;Qv0&j_sYs!2hYv;OFY2Dfw>FeZvewxdnuD^n&#tc=cYTy??Iy zJga3be9Ta>7SZ*OA+T&Xr~Q2r|6YXwCN!wDqn+y97^g!BI<#!Ov3>cN41}_sVvtEG zFe`7Jp=|(EW0;w_5%*mBTVz<0M?B{X5Uj1P#1q@T1>I}{lGw^fLpr{5#50B?8TLyT z_O@nU34}xu7e;LW(@hGVSa)hl=9|qN{r)z$E=0R1h`?@o2cZ(hj+WS46pC_UKMkqM z+7b5&vXCUKPm{nnU92A}Acryv;l3e>Op_M3g~))9(jF)pQYMS5 zGX9$eD1a^x!Ol7vYnqROkYz9;qrp`W*R|SQ{eyu9PdPLs#t&o?K`b*6{=YwI8_E_I zega8n2V+(thJ0bPy%@)w!tE%{Y)NQ5%|j%>VP--iB;rP)KUzIG*wQ@7`1s2@v|}rG z)_{tGaTE~(m6`QPYtxO_q7j&HZe0jnGJuFt2xKbm+K>Kwc}ipt(!K!TdF1OI99Vhi zDSy9W03ts#d%NFPw9v7GR2PFpkcTA3mlMIK4KMK@==Tw;?+oP9uyHUfuF809&FK(n zNQjfDsfrjdDFY%9Ged%E0no69qf6e#-!J+DY)K>&;R0A+orwoF{0IS)9Y&}^448;8 zfr-w2IB)(NhF`m}UFyfW-r_9*r^HZb9a9uMv-xDy#~Dx!$D)z&Fs|U`-4}qFl>Ytp zgrLS%67%&l9b!nB-+rse*co?mOGp$O8m5eAQ_(iF{iuK*u7KiP4p~kffuGal!toPc z#FcaIf*jI}TN{MHgfsp+Z)@L;hT^B2DRY{;A--qDg**@AdDN2`8Up- zIdV+;gDL>{iXfz+{oKd3^Rc(RY{Z_yqH=Ivf%dD zuLc*%9j|Ec@K<)9gBD&IDbz%%{<}gB1hm*sX9}`oTlbZjyFhdv-yQ*=)>S-aTZ4=; zi%FookYT}8J?W@`630F)FJ5gZZ;6m8Y62Db@&B$LCj}hE<_X#Gimv9O z(ODOipf(Pr4v&vQiy~!O@2N}}{=amB@#Kysb?Agmm4aAa`fk$rn)(*DVnY}h`r-lw z=5`!A`HRHkkD&?xy18db#3P3wpvs9C0NT3=wltK13**&DVkH7B&)m~tdCoQi!crwbK!?e|AHT#_#&hvbELRD6o32hO6>N{giwY>`p%j7+xwR$b<7TZrVTBNvAS^xjs`;D4me`|jND8f zQ{5-m(gGXx#qSfSjGB%Tj9UyF+##rd!W^Cu;r2jwV&${2Fk_*LLXohseHMQ8>eXWs z1vb|0)9N|6M!60rp`XOAj@YNB2_CP55B5U^-fFZU7L&y@| z*tHz3o}854`~H*v{ae-A=A#en>4ea@3ILt%=-+*Vr#zv=fiR9)#k-9!04TA=UB_0u z1}pb~2%gkrd?XMCL}U2&@}~_sWRdCM_wU!^M~o5(I1_AZZF5oVsgu#{ErZ64F929n z5mO9aYCot1KpDf_()2Z(Dm>)J_5*N zy-@%M+5WrVQ?$Uby*9%Tgm{W_v8?v_n-|qSfAb@2@4LERiEPgsD_aS~lkuGP4^$xV zhrNoObbDctcC?TvK5v+X-E9?PE$?}?)0X4FRXORbv^1>Y%B9aGu_FLv3COSwz3vGSC~?*yULPSS!QS@L@eFIYL5N9ULJ`A) z=^7>)tRUc2yK(Y_7eO`6;I*OP?R_ioyEnd>mfGm!7A5%&oAHM?zKI)N{0Z7o2(HM< z{Xiig2Sd2!xcd!(D;r@0DggM3hjro|mQ7f-?zEI9|2yj^!om9-K1qIS`GHtwqcQCS~u%=`%kt zCefmU=l7k#y!Z=*LLl4XPAKNv(;$I?pwUy397O>F*v&Bz0cKS95K9UaIa-i!?@X&{ z(cvBd{%-&SG1VCu$RnwI9y*%VqzrbGAjI7p1z_>I-cy7AV|tSCCJ6B>T}Y+1FZ|@f z9+>~&$s5F<4`lwsJnfJEK7m$`7gS@3`1QV$X^MhXYfr;iw86*#XXI@do@AO3ki%h| zSo$1pJ@$`SHnj~j1u^rz@12U*_nif9NGc7F*mTgiziqUj8E&St0BRFr1+`8sDWIAzHLz&JWQMF1e{ zLyrN~<0=T$RT@)4;KJs|&}7LT*h#C4TFiLl$m{W)M?MX$fXh7<1$e3bbbRnHKgI(` zE{A5bAeq^e!y4rzV2(%eoy&iXx2}D(JDuPBB+iL@YPHIGVAkydVOLV;3-y>x*|xX0D#?})mMi`IokUqfPm3xHR75D zipULMnF$eKqq7Gl76%iBm;WDE7($cu9j0#vG4a46tor?8DuGXiN8l-?*;-p|6f)de7c}e0vXwp2^1BpL(YO0+Eo@ zE}{t-w0Dps;a^A2!$_!n!6{7TCF%R!Do3!ae1ArO4ATTkG%r4}?s{CaaWhsdisv-q z^0;h%^?cm9_iAXC+%o>3Sg;8NA35i~oZQ!oZsF}kK9p>5bP?WnwM)R)9HUQGHjR43l47DgzgImirX{3uRoOfKyZiaX9S_A-xLUI>W}55FBk({j?DH#iD~%I+TX#XMaT4GkBOZKV|s;$a*M}0S+r7xJ-fGm;Uzt1NnTWc?AF@H&g2`06J(5`=7GJ!d;S} z_1zC9!~C)3x~!7#1+g14GW^pT{WQU#0BH!vN5dzg01VhU65N7&cD-T2DNZGh>6yCM zRPRUuNU z2rG}~Wj<(D6OP7~;NyS$K7POT-Lq1(Qr=HU`1{UF@trL{#%sO{lcgCc^J)gOWmlGY zW{Ojp)sx&SveeE-!evFP%gBR}1C=hqs#X>2ma3?$$Sx|NrL-NFEqVmWYB0uXy_!|V zbA9LF%YXlh?sZWVgN$9(jNfm4C*J?(pW^wUv!U6{*?aP4yA5ap|9%W6NpRt(|Q#F68yHj z>pk=C#NGVfgo!C{H&Nm}-4A~A!yONQQ}^lD^Uwc&2*9C~M*j@n{t1TvI2jHE%Ow#r zEPhCsB;3E}^|N9npAW(?eH6fHHQT{8E#)D=H4T|NEB>VOo1el3jZc87 zTGj?Q=CGO|BSMvB7(adM4KPWn;rCCM8J5tjW0f(o)R_dKxneNmcJvQ(bPY-=z_~^s zhY zBSM|sjk6Z*$;tf{jUk>$|18z^0UYtI9;sH_?8oVi2eGWC z6Ai9mR5*N)xqvD-LNNyh0@Y~uG+}rD3iJl+K_)2~zG-2z4$xpAEGp{*W3!)At_%p2 z$}FP*WseVfqNNbTz}dpAzg!4%oGZX?CG0sSXRMscT*3QS|3BRL+W#Of*$wB<5VDM3 zwFw{l>-X@TH~t0}FWXietUMb2FSMV5-#-6tbOhQU%reM4H{^^=5ZWsG3l9KDbB_4p z6^ILKcDAl;!X2-@8E;$rw4r&~CyXEO#B(hWFT?JUwIJ(My@Vu&o(v-Zq6r37OVb~* zN?k5E4uQJT+yMZx1XPuZw8X%vO$P34wE@%^N(edMCQOmjN;Z69Tdt#}$kJj!PH)6IQc^=9t>D`>m52C!w`sAgixBYyzB1n2r<4W{QA`>l(qSDh?~5rfhbKaAQdq-o5hAxbwijL$euAYDp4Dn7i)cjlaG1h5F;;W~0BN$O+~KkC|7`YT2K0yP z@buxcaM_BN3~jHXvLye_IN3gUK2&Z=-B!lXl_3BSo+fiHlyD`%!7R@x8;_yX8p`Qd z)>qAL-(yVRnsvX$?R!3n-JY}Z9uxafb@GsPiS#6j66%J zad=>n6PeA&Sn75tni)fpT96evO921?hh1PanEqL97Q%+s5tvMm7St5+qznHyV16@I zKrrdHxp=PmmjaL@8G8I{VSsKqX*EpE?Vr~8pOP*>P}LY5+~Z+FgCVie4CoHk;kjcQ z7F6GJ29!-hzO}YyJGds2UVrOheD&=A!RuQ8fl{Z6)0XDZ`0rbRFa7G^;ro#-10lGo)oI zUsBOskbM?Q69JGc0hm2#qM2mezU?Z*+dJ%I6I3@MhL`&<0?BC>{47!c!ZAJ#08oPL z$%6W$+m}qGwhhu9$N=?*>LVVljr8*@k-$zY*^} zg@FK*OlYi`J-^7?mfem;_I7a9z_8^>Q(Bm19E&c&$L{5O#ZrL=NHk%^Jv-ij z_ucX{eD$GkV2^(dG^-hqSV6Rpk-*~0zJlvFRbx=p)P3ZM*6AX>17|oivJ|g&uQ3(? znB?JZ_pZQDv_5TmA2FcYR}ad7lO`NZ|Cz8-lKuD#1sA^xi05^2YEyVUa2VZ#ivCPJd%2f<2k5SWR0GR~Z5TVX7jF$3& ztoze$o%tZ+I_o#bHB-*!!+FRxD;hWA*IMD#i$xwmyo_Z1@Svq!BPxI%_-UchtC4Z91PHPaa99>-9A^(Rs;YhcL)MvGcjWXp zBtG-zdwT7wkW^LordQAx5tyRk^~+z>)t`3D%>7BkbQysOH8%jjzz9dz@e)Cdl3azy zD>O(lVipJPIdtj)0gM4lvM4cHW*x=H*4+T77RbKzV@3@eiw1}-#uxAZ7QS@vSJ5@v zP(0LUQqDCKo;q|kzI5+b@t#|6z+L;^hM=h&!Yt>F@dE0-Z$~OmWKf?fe8mnJ})9bXs9*~;PYqw82|I8+u^Y0rw`Mm#T@jU4oFh= z`ie$aRhJn6ps4AibKK!35KkJkN{%DQ#;5FB%!Ji#h3WY}-e-YH;s0^XioL^Y0fX58 z!%=4u04Nk`|L6B#1>vRiMtyp3e>w?;12#KCab{2)6v+~vK6*9=7ntH*F-;mkRhhur zrE1>t{Q=;v?N{PEk9{67v*RSuTt0;WrV=(Z?$4OyC3^s1=FhCJeGL{w3OlDy?=`k5 zU!{|8!IFV(&MA8ZkXUXa00u^;mIz8!34!FPK!7_~x!}(SyOq$Cg*U9Bq#M_)yAhUn zG`AoC07wj)&5Re_n{f5*KfzD_^^sGWzagFM8(fMX|Ldc;>UY1!e?R_3yy!j)iq)Dd z7s?8|?M#TmtN@Uv1YlTkAfUJm=~rQrafbn10Ga?ifr$s-j-j&=XV#h9$r-w5AOegl z>2D%P22CcIp5k}F`QBJGF}|MEImQeJQpSir`H|ABAv>$Ngm5}n`Yi9LdqV3C1$vd zlFIl$shXx~hAbl{%ed#z8}ZQoOYoM}TX4-;52B{TgM}&$)dHhIK5c$)Dp_8>AMaoN zd)%@ATBsIF&LfIQl91pw+<*8=Jbd6XT)N~1eE94~u%xQzls(xyM;r0fp-p&n{{`p^ zHzWx~<-#}krzp{KfmQ&Br}Ho-D*zbM%@W5`N6x{uzRhSX8_N1N9zUN_spYkSi$Eqm zb3570r2u1z>A(kRhMje0>mi>J0LKlsWi2WWcFD*V!)$VO;uB}wfNuX{JlS~_4h*k{ z#-uUh)8IuYlK-i)4&eOer*LlLGjQ4CSh+}pCC^4@s>qm7q6F=}rGPCN8`1jt2duO^zBZCL2^2Mdo;D&!za-+4sR|i7beIx0neI8=X+B zoi>4`%>q_){x%6kHQ@)3UyH|&T>xR3`{ltjf%6vc%=z!wEHfLd0PwoDNASwv1yCj1 zyi=Mp>TgBEN6x-a_kB1AOcJFHxg%l-t4>`zu+;i0R+=&lvcnljC?Ojmi zWK-(M#IS#GDYhKhfGtNhp+8&?lB6VnIabDR{%vDaupmyl9AkI5!BFyC&1Axz+unq) zUiPP~?=d<2zdzW5j=*BTI&(XTGj#i=PXUN0*fbc1rgsWJ=J_xCS?VM#ZBlWhSJvee zX(?^TM^C>Afp{sl4V;f1Lz~baS_FWPg*dRRI)%nc1KZ>vR@d#qS@q9jarq%IMkseB z+n#Sa4?(GcvHc@wKuFn}16sgaU=~mHxO)|K+dU%-0Hn{Ytr*?F&FmPqi77l z@Weh#XjbeUUI(%m5dN=f2*Fwe018evZ`Ro5|NUD6`)Njof2#KpVLKnS!fbbtk}`J63-nu6VJ78fHzS-#ps{c z0F*Yn(qH*BbP{RKkCAA-Atkd;?lTe|K5!8}e%`$(w*_WDM`8@2Nx8kR4PBVIXOD4o zjZPl{piGkjz?rH~w_$*wxrxH7EF}n`p}k*%-s9Y5@#E6gf8f&Ae;}A};n2uhboiH{ zFVKv^a5EHPN^XzDCV-{vgxXC2F_hXoXfEkQqpJ%mD)yqev;%yc65d!X&|IU<3ECHl z@s$0veeeS4u?3jWu(4$p5ddi-)&{~&7+>Z=%`pJLISStR2Y^xk)THhG!>1#GEXX@) zY6Q8<6dZW$)_>nKm(N%P0Md?R(yZJ9fSuD=QD< zb<1AE1xt6JwE&IFX$>t+#=gO&c%^F%UhOyy2Rut4m>DFo1!MG2Sd%0Qk^nF^4a?DR zq~7q9mpHTIo*kFt-_N^e*54iWOb*nLZMiml>SW51nQ%Ocsg#YjNY=DqC^dete6JTI-p1)lL0Mbb3mg{_r-tetk z0rt6Rjs6S_|KoqEY8+PP_Jp;DDnrT&F^0iN4W2rD7T&P(#RbvtR!jajou>|;jqm^K zQ;14VkW@IzA`^r)4M+0!du2`mkAG$bsNCwo6-(~JgU3Dqqc|<*ysuS(cddOKYwC^~ z2PjNaGBT2Io``nL>Hq=3Vn7N&G|p#G90-iEkM^YTK)`{E@s?H3EGQtLrB*{Y3IxL0 zfdCdMire>o8d2Gm{}Q7NAd>_#N!TA;g9ERw!f#&sAZl%HoYT4!7cAY0(;E&KT#8vl zvEtBB8}<(_#w(qxv3p<@;;<$6ZZ3hW3otin{F*A%5`fceM}wgTFv(ad9g-#7yXy_O zdgH^enPZuMkAM;1#5uCZy#XPubXFR%fP`LuZCWrUT?K%XAV2~NvMU8hY;FMn04R46 z)+|LZv2{KOrXr>L76SPv!cvNXltDNJo8|Rv*ESD3zO(XwuYpt;esXGGyZri z&e8%kgP|sjCnF+aP=y4Oq~cT+Rb>c7CeG=&X2G7}^?(ulSDJuVv)@((018-SZu+Gr z)XyaK?b&(zQ@%umUU#6Pvbq+UL5WCYl5yAeD;Ep^kWUfBaP!vp;I?gVgJv-s>Y*^? zPMW~->aM)?N2xRO!|39c$FXZ@6S|_y3=Qp^qltie+aRvJ;BG^2JDok}kC*H7`VwF_ z#f;cNW>CV&kjYH4j5wSN4hUcjSl%MAubrj!uuLbbt9IctXZ{$s?D`VCYSrA4YjOtS zNiblL8gc)T20U=^jbNmq!8w36bw{zb;TTS9IEJ>$UYMo)5Rmy}PV{)|abU0w`}!B- zfO|3e0u2ycN&+SlHzXf(Ud=f{SX^0nQ5xyyzoMDpiByB^1{;YC2xz5vaL=W9--bss z{+c&1wH(^^p$kDKKI=8p?X8;z0GK{?%4G>6O4%Cm1b@si=VR`$5!NnN(cUM+6DSPe zO_m6)^#WC8V$Rz_0Q{5k$WE+mC*Bxzr{eL|Su(xJx_@^-X z$4ep+<_^j04^mYr89<6doZ-O8Qta$oiS>;Ki-VS-gyMEw_vmM_wf9U2iwR(b7tQXF zBWNsj=k52*CW6aB2!vuwcoF+{2p%E^F{PV+mdDI z=5$FgKpqR9hY7uED|)&X;j#8h!32RrL%G$12A3PnWkYDG7(%^k1TI?$E=w3LYZ#@r z;Ixqg(S#MjgdM?{1EIJBp|}mhp>p(iYtZYf!GOO8{lQv9HG6Vz;T$w}9cQbaS4O)g zz!VL_s=vgR%M{8u^f$i%Kggg$Qhv0GSJP36Q|@>dw69Vbzpt2!u21lQQcl{_V8i;^v)S zfsicoj}L$VrmFbH<$u76nvR^@KkWe%sh>VXU! z;;+ZHg@!_f2O>^up%S>F z!m17+&g=9qCL**|7G?#2SQ;7tDc|n;|YbqADY;fZT%NQ z%fcH=jG=q9A?v0)EkPibU+5^1z5B` zO+#35{-qF}m7^%rO4pM=d>X*cld20%)MUoLM0E0+@}ZNT2}gZqI}b;VVnipI`wV4y#eRHX)gQnW1@lWY zPnKc%)VX(Mq&OOE|HB=r&pHBt5uw^Knn#o*hj*7{#LZ6JbMOkh=g&XK&!74T+`-Cu zxxYB9gk{YtvdxIG$q`(==GS<~k~?6IMZq*};+axl7lEB1+9O$lFv}2T6NEK+X;w3Y zEqQ6s|mtlN`A*AC;12@p7i#!d7j5lNyEQH8-llICpwQDHiYAWQDKJvgVLt} zj6~~l%5OokgulM_W>5w-HHWnjDKUGYhe0nQY#0ny z71T7~9B8N#IAev1MRi)Tyl{?0=0IJgz!}R`v^5lFP5IHl#KU0U@EPa{Ez-3fm@07j zs;#q|D`~*F!BFFrwlDzdDGUIN22zCod)6XG2E;@VP+|@bJ^#1&?&lU%mJt#7*}66K>6Rlh+7K zs=DXo0B@|BUGCh5`WNx8m460TRAVrJJQy!5q2j|E@4<)8dMIaoscGpuV<1$Y{q9RZ zmD6X?M4RDlmM3FUk`OmJaL7(TnAo2sI3%+lzx*q zQ0Ynnt!J)M&|0s-Y${C0LoCh^iA|hXTe`2BJ=esf^}o9M2$s%5H`FvBmL@G@DBL_{ zZ6;FyB%G=;nkM{T7!v-k0a{^%{}%y(48>7lxf8%ZHs1c!RRaj4eviwnB@AQP5VDMa z9K5Jt^1=#9Ug}(jYi|1yUK-qx46V&a*=w`@vNWwnPYOuoImf`!LdZ1VT%aCE6X1 zxFMjkpn@y`I9&w*cc>xf5go!L;f`0|Jnd)v5r%kTD*2z#VUYP_E(EgFxvK0#y4E#G z3`d6-75vgDu?w`-YdB-MiuKD?EN;|LS1I6fh-9=OON(#7W+7C#2+g$`R<~)`uu{RY zmSkDK0{7uybP_4zVb2=02lSC9b4|mWPkUyTYm=vS%nXK_Qbr=G;?#@)P(>=zMichB z&q$(AdJS$>Af(v-QYg<`w7Q~ zsBp{;$s6auqIwO7yJt5qPp^FyE{hMh?)ndegbQE=hA$GqiV(hd@o({#_1km)bLsG( z_R-}arUOkDgw~4wymT33n4Zlrmp}lLK){3hFPV270i|Ogz@cs#B1>XH!)9O=~&K(||xOT#c66_q>0O;UiBAz4yOjSO1CIhv~ zKBNoqCdP4O9smI2#cC#4%~Of96^%{UCnbq6-?zHqanU9M43DoN%mBnUV@he^U ze?qgs9j;ADs+v$uI2Fa~=PB6#*CHb9`gJKHl$gTq$8P=j&O&+KVgx{jBB}H{5Je{% z{+W6E)4ex1;;rDSs^bKh=ou7}gnM>e4&@YGyyIne-wIswmmlHIJ#T?#F(pST51we2 zP+_Ki#55n64zC9zLY3V&XZ=^C&-v@1FNAR_C{|l?jb|hoZCZxm6)g=H`V3Ff6 zzW;`A=f(I3rH=sU4K4-g&;yttEUwBU5!f7(OwH$rfKn%6b*rYEk@~diZTRXr-@@6| z&q0bQ`oe~bV$hOEhWa~IAJkwcy8Dk|c*G4cGC0Nf2LRdPl*s^t#$_qg3IJ)MouXAx z$JZFk>0Tk@_E+DM@;y~d^~i1;JP#_@**3<|J(@ZVU?u~$+CBuCR9(M)$lbaC7878S z2^RCoYrY%*!3dwQCSL2m09~OrU8yik6}V!>^Kh7>vjYIq5C9H^8ldsi9et{D0C)l^ z!v8%RjVaHo34m#L7Si(;0e}qU=yN|BCPsHmo7+m_J>^Mag3vPJ&8hqy7Z=);TlGl=2QX&B^dePyF!(rBRg+0nlgUc42)4ptN)L@op zZ)`ZtL40ieFY(Uhe}N5wWJBt;_z3Z@umZICsTBUiwbaSlY0=qrMd& zuA>GpG%PkK40z@$08|jvj`I}=aM}eG00O?Wyh9dQOAv)9x+LTK&VldEl}ea znYPBd(ZUL7FPV=(fW=H$vqXi8$VIJy08Gqe&)s)wV zy%Sa4F8^Zmg_`xZjq$VwxST=cPg6;UzK9-)apC_@b-o!stz6e! zV;bSSmK|7>Me5&VBEsV`6lqGErm1MBUaFZlI5qtLkY^1d!eM|8MJSOY?icU+z>XpS zFi)lX>7VQa(GzJ6{|t@(@t;j3LMY)LpJ8F0oZ+aq6|Z)mcFK%PM6u$>|M~>JaQ}Z{ zKxxQpo-8ARRgJ(G8-P1}GUS~>2iYJ2E^DwL`qpVDv^8dLWH?M=ymRI4`0oq9jLTc^ zgN+5jGyz5e$;#gbUL?STfSDpVxBh8-{hV*%-K+kL#)<$|F3P?x zIC=(zjst}WfmOBb`J3Z(;y;B81WYCiJnKQ!I7bVvt&9P!4H`=3e4sKYfc4tS;tNJ52;4WZu?M>HB>1To<=A{0oO>z_)j zc83DabRSMspd|p)?R5a)j?^RHbnOXl!QDIFh|gYl=a@k@alcJyR%{KO2l3!m2?)D9R`F3Aj`9rGi>i&g_vT2)f6wT-#Hc`fWgt^yb>f~^MQ{;k!-qe zy0+#J&S*X`>#s(pnG$`07LdeJo>Qk<89+@-#r`j2_uv_Z002zW;Ey@~QfSXz1OR3! zUC;dFpRE^v{Q!VfdJX^M??)p(Q#}tx!c=ZIB3zC*$1A;S(LU0gU$U?~i=r{K_epTj z(Y%+*VwwgA`Ox9>Llx>cM=6aNPAqkPRfa~FC@>AawnBgabPh%8p=h*F$eoONL#hMHRylu_XxO&q=1qlRH zx(JS?DvoqZQ#_N|NQpUsH!r&j7ccq;p6k8>Tl+6XL^wdQl-Ka<`6`SEPzAUqV3Rx; z^@ZUN2O)$qb)F?+pkM$bGLN{ZsIZ4)jFstbXQ5UANTh#nFw~IJ$jJ3xaR2Vt9~Z~w&hLVNBa0FcdS68j|>{Vpq`KgDMV1dqo9Yk4VPHU@dq zBopr1ekK0v@|zdrAoxNI$ND5F3PY*Yi&{rN`n5&KhwF06x;V=bJWgCXWoIXXuBomf4X()Bho#h&hO?~a~=xRhspWb&69m6-gDaOQY}y!|;bfP}DRoGZGM?V8&km@nW&srI-O;@Pe%XkOssGCR_+A z&a9o=dL&E^+_(EOT)FIFOr}`cHgM6*41Kdw2!I}M-4p;I(}2Vk75m^BuE~0hk|g0D z`!6a20COxt0R1D$F&qdr;h!CEgD}e{PtYu!R7@jmY}ktp&HJ+h0MbO~42GHj#!@@Z z=+vtMkc$0(|M13S?w(#9OB3LX?kdFRE&>2qh^i$1BAHD;U_e#IhW`wU-yayYmsQj& zF*r>|D1#((Ja+gzTzmfA1*tS-y~z^LJt$$&GjU-qEjxe#cT0}F4Fn$7AkiR>1tMcZ zOlkB*Nr&RY3YNsN!bMoqrs7Digjn48HkOzJC@}}J{(iZOu(&~kNuE37Ur|{~Qmg|b zXM%Jno{R{qYmdSrC-U|z3^XMgqX+~TWH&INrB*{liNLWwY1&Zg*@#(+;k>4&aBkz% zI5N5#uMS>}-R?7xfF;R~!cWnt03njfn3gp9y@3e);Sf|cKK=Pj;-8v#TkmNY3{;`cHBwa2=SbW>p3J!yGVVU`38<1)xA&N-*?a$i z^x)h;xM|w8shmOpfF=NcaBBGfF87%RvHuf7!v5z%eDG{nR1FbpJfQeeg3cfEeW^m{16u=|KS#rOw+r4b+e6Ty^-BO32Vk3T#% zrqfh{VA+o&V+1N-gUewftXra@wn7w4o6IJ{vKAF9TGTl;{sADE!bzCSJ3U7i0U*-| z>l+W{SCK}+@&INI1elUQzge!o*@iqxCJPeB-GF+YMG)F5`wOz4)0F^phZ_tbKRgMgjE!jq7YOjgE3oC* z`4jH(+TewNBxQV`OfF(Tm$xCs`U7kkqyem|-UV62b+tiA9FOe3sHmLJl@X}{eeUF3 zf9S{u&=p#&kL(vo&+#QoUdCzlN3vB}(}K$GaMQGF^Hc%=yg@eQC_Fm48WGKDhyvx1 zLL<-r_20G?0e}T0s!G2GkeF!rPci~$NfhWF@s!CTVVG4#CXV}dzaBBgGOzoZC>dI{ zuR~5-ZfI%wL2xZMOxR2naA;n*g9GqJqv;ztZoD}Y$QJq~Vu-8ef*upjfwl$>tJ_pK ztTdM4_`3xX`end7r_PduAhYAu!4nIuO7)&|Irw z-BJ~0IY?cx$`PE^_$;nH^T+t_^S*+2FZnapy0*g{4}&Mv+=8chJSdo^fhQEmu^61n zAhbXiMu(1|r*A(x`UepTd!cFSOm`Xv@Ttz~_O+c)|Ku?ST9s{+OcqBn6HKq6S)dgF z1Oag+T?K%8z=&r*Vbs&C*ZecxSDxV0(MZCp{pT6O>OINQ@A1~91@~qWixk6}s=Z)3 z^6E&EaPO`+AUe-=z4IIj7gi*1UMIW73#AWQvkA*o)>=Fzv$wx-pvSCPcQ;!YuS#U zD_R03;r6OpbEOfJkHFwlOYzSG7vSydo}9OReV|i9AUu74*i9icmv*8fz8prn(Rf0I zm5rdo8-yS+?f8xFc{(2Yl}%F^;ETED#n`wUN$=&bkE3sxrv+$?7PCyKtrVy&C&+SP zc`^gxseSOuz{R@DQ!R<^3}1sS>q zjY|s{FJ(3l&ToDK=Qlrrgl5Gd&l(&aU5gIi3iOBTlV}8IAkGZkOAVj5B$6f61VIuS ztdbYONDSU^9En5-G<$<8N{oKRDDlJ2Ex+)M+vitj3xZkfb`6~CpZo<10L0=f?H=Ne z*3T^oB?AT{_1Np)fOR#mVgK-Y_>~I4Xl$J`^mtR30(962@XmaT)zzP-($;gy(;L}Ka84vzj% zBVa0~0B9QEn-u=9k%T>NV*mgqG=vf*e=gMLF9HC%4rO)&Ag-Rh@{*n}pm)$yS=CSv zl8iZ}NS1Kd_ABt#wNFhcQ!{@gGbLhi*#UG8EY+1~J$^{!cmj$xgpN>TqK=c&@Sn>0 zz$nQBn2khL3!;hzw)s%nhXK{)0@Y;#qalXD5spBF=Y&1pX&0!g6sUAjLE0v&21F+1 z-Sr1s(BWUIEANzP0v9aV2ASu#-a9+NbCG4+g#e2MG_%P9g;l;79&uNj8!0i?UTOKRxukWz;3?jAZsx=VjTnQ$#V#Z_R^s@!m7n|>n3Zqqx68{Dk zmRrN*=w3y3A3sVOj8+Ca+bC4E;BT&rNR}Q+rx_o%OGE1;7{+Ih7`h3naqFFRRjGV&4li-?^%?( zUkb?N{fi0BmM42q)mtaOhVl3!{bWJPKVQf2H>%faaV&2%W4GVH*@*LY$dZ?&%0 zXx|O7zGq^$by7Vrs-{<)t0BAOe}(W;)9V8!UjT#fK7K2lZ6!X<0Y$0IkSy7NF_(>8 zo#E$b>I-U3iC2mED&K??s{7dEwy1Q4vvs&0+4A~zBTI)$ocPfg9^OjIA~xMrZ}xEL81Q{3sfIB_p7Crm%sOX-lT#|wB2#MqG%U8TJf~%@ueda#b$9EqI^>^d6bWWFVOdY(fSz*8OZ0pk*ytN+C-)uzgYu_DM6u& zy&q%v{R&vnDcVTIf?k%lmADt$5B9%lJv9OIxV=ol%Uh>iQh-luT%e>YQO5!mqlBJn z8$&UJzN|@1-duGz#VJx20~u~@&bB0p;SMHh|rX~BAHJg#O&Z_uq_*V8UbB^@i$syH{7g7oAvmwcQ}kUH2g`^WtzCYvQ*cT@@L`nYh z*7L4wazh24#m~HL5W)FE`t`-K-(lB_&&0@7{dUr^`xtu1 zUo7y%3-^iZSdp?d@t&c?q|@7O?=1b|Eat2IMOoD1kmHsDGYUp8;|I*#$y>HlN_$VJ zg+06RcRqLxTGrYn+N5O+n0u!{sL;V8ryb46bMgmP#1AZxMZDz{BlF%e)h+3`9&(cR zl^8cYogKpPgHj2bE(#jIYuo^4o0;8x@NnVd`E*jNW$z37uwQRr}fU-g=yS*Csyl%8K)lWx#-*`{w*2b}@5{N4b-ez~ChvW8c$G{9b~D^j``O-`*?PCM<+oyB1=zk|=Q) zvSY1h(qk|O(raFZqQ%A9esCD#-yfs$ckS>B^W*LO!@-iB|(h~ouBg+;uR};IueEmjH zVA;9mGHSDxYfq)KBXQpXM)1?>^(h92r{+lIPlPc9MtL}!5GAbs8;Yp zdx!;~^+lb2s*r3QJMGGX(RxiUH%xAEq~eRwDAZp=lSIhl5=Nyl_r4oBLcEER1Ap2` z-zq6!BtI9tKgg;*Kb*Q9B6|YbO1GSIR>3*K<#(?Rem_OnVDNOiFBY;Ejnq?Jgf9M~vZ6lKxj5I4~u#${xYbc0xGs*hURG`Bd$c*fJ z-Wy6MHOY$)DeBY=w(ZHI&2{$x%@I>iyi}f`fjHUc&|IN9!Ibxv2SRpY{g&6ITU*F4 zn+YO?d>e5o+nv6r?9dS?yRN>OQP<3MYFj>b#`+a*PIO zd=ANd6bncf^TR8OefLXvl#h|&yO>gAc{Bd&+Mx6oinKp!f-JLtD?dJaF_S}1Ia&&a z&6p6P=!=a=b{pthQLUC$gMf}KxUjyGqlEqUuIO;8Q2cv?65cW;5oA<06dL~b(^Hj*Wmf@ z?Hi7g3NIp&(=Sv-ooC-}(E*$I(odPK{Yhk7L5n^qDWE;RuXu<^a>_?mJa|cxB7=>n z-vHSUWz%9T_S5lG#ZfC)N1U>V40JWV%|7o{_bCf+Q=&GfRl(F<7d)ML6RwuOB=mfT zl)}No2t;~1M>btY!cPW*)I792rmsHYLLY@4Frvx6$l_U~R@BdS!A>5d;K%OJo0FD{ z%l9V>b4%cM62tk_vD~lmP4B3>q|4{69g6YH?!IYIe2eTUE~)k*P_GFZhDE&Ms{SRb zzeFG=NJE(a0o((xvb-Vz;y3V~c3OQ*uOGN(|v3Gy{bgnD8`Vh386i(mR_L0cq%TJO1A9=}u z2j6#qAB81P17y5dS;Cw9{9dLwlV$12_f`r6k6qyJzzJ<=h0fB`YXC9LP)?cf`U$S! z)vEMlbDoscMxmZBJcwZ$45Tuzbl(Yt+|;3Od{o#qyf&SHyw0216@K@9){u)^CMmQ; z@G|b6pRVRPoi}-nj-9YyjSih4Ng|$HpIDwJ+yiqS3BCN#0M4;IRd8Rl9$t&V@ZJ}B z#ayygBRsiVw;ISn_!mIF=0|DTH$mN$kUAzCN+|ikM2dJ$=t@c@vRK^y1B3a4j;-IYx@Z$XIy## zG%iz}?ajnfb4~;Zs4FYIs6$;{Iz07U7)^mg*7;T#DcLYlq-sv@?Tl3mWUUh6^lSCH!zip!Lq{ zrdn^IP`Nq!?)qh{;M=S1W)fZ!e8+dC4g!xC5!%+yr-7;-u}Mo`$4zk-Uf!C?GYZcT z0xbPc_FHdCP$xnEWUJE=CF^)9;mD-|wwtpj1^`~{YP{B-R@E&Hb6w_fMrHK#y1vG&tq?q zQG8S*o6tmcoaG=RWcDDC&h2#c&l0J5bk1L1u*oHLX%bR5xhiXS@z5%IlZN*)ZgG)c ztV4pp@$7G>UD0~V@0l5e_F4);FJc@5n1C|aXvO~wH{j<+Ou%yf`|}b_{F2WI1cE5G zDhvJzN>KxWsh;Z4jT0maC|Kc4i+xr6;hW8BFoe5=uY~WZg8#|=b`*OA6g*IxwreY= zOfQj@WqJ|U_|CYVE*W#pA64KvL;XGmH~4u*0gMSlOCN#^k~Fn7lOoG#%4jK$*-(mn zk51XTx@dx^J~kiqYyu){xIGmOCR$q_4w$i^t~v8z9j$Iu`yQu*rWD9(?Vz^eF3)y(V6nQ@Iw4hT?l|ftXW1W_TK-1 z|1Yb0cy~u>YXxDxEKVzv_e$sXn}F-T;S-j|p4k9GUxV-C3>er!2WH@qVP^hhCsz5X zj9xM=ioLc<1ZbqyN?4Vu;ZNNVa@L@`tqsc%Gj&Gq8M30B%%5>sTvj__*H7s0I@?p7%?R(H0JDF zQ(`inE80gE^u^aW2d%MzhjgEOOoLpk`@mxH$Hnucc#6Y=!<2q|)?cQw=fm|!3#KO;$#pfMIfm6N% zyDKdvNK$?%h}ZnvZv{mY;X(K(Shk&}h!F%_=^|)dj6^B8j?K@D(^)zQ@fI4V6>uGs z$sXImgWFPjQVLp9!t9^)^?!f>?Ygd=YOUHA;b)^-EJLN-OGQa ztD0{WE|ZY^^0=?0Lr!Zby`eF0dG2LW*%Wm=nN@$;Esl{oK@bE8f4!S-p#PUFa-vWe z)UD@r6mo$7%KUIht+v8NknGR+kFz4U4s>WweyCL1F6WDEKU7$}zpG?takV>S?T>pk zV~e!0X%nr!gLDf?La$qEIeBgM_8?mzk|8H>S#mNUj-0pD^0XwWx@^~`xWlLEq6n++ z1LQMxtgkkHXPI4;9%(;GYZk1Tyc}{qg@D4RoTcY|ze9if4xZG=3=#X*X;ADwri(%K zpcny@MIVNds3o%glEkgZyl)O7%R$(~H|VgX9H5TvQ%n!3=^s?IEu>G7sv67pRVDXi zKZ$2hy`OZee`WJ2sIsoaVedmDVXbM(yK{N&G&8CaVFE~7WEUIo8U)iuu zI{QWv9#Kxd-NdTeH(w}t{@-19VwDxUo0ub0LF1+wemJN*Y%@y7_H|Q_03k=Y?)~w* zryTx;SY4n8)+oO1Ag*YZ%Y=BG`hNDLbt(`*wDVV=8q*`UbgkY$48F$a_6!6>*?!GF zG9Y-;98wBbL!J=I7(!Q%@b2Y4gY79f0ndL?kqdml!>$qu;1&Za$x(6%_Nu(j9C5-m z`0lm4SmB$}<=oi?^u{e^;mneo&0AX>d1%Bj8DcP~QCwu+Mqu!WOBnWmCMKBU%DRm7~@1heFiM8i-8GF(Tv z+`a$?YR>s(T1z5!ZBMzonB#FA3F~9n8HK#u6R3FRuy=W0I!k6J^vAX8I>ne-o~#_* z{(m2@saj(~s6F3`P}n9Wf=fQ}fiGY!JEH_OHyBL>+wt1el$#`@rBi9G96RAikVz{klDXpAl{=vR9W@@ z`&kz~-F=!Obp)#p1@}xaB+_>8>>2CMrhT>MbFIMi|47d$R76%h)At&n1BYY)04R3# z*Bi9*00KIp8-|Kc-`-2Jg8}i>Uwh$4sMg_mR+sksC)%RHp=D7V`fsq~^qGksTelMf z=ROsHn6Jk1`fPX5(;b1J{y-3$O^m9DwO8(wLVq~N2vPjt<$DGds_Ww~RFR2Twk0ZJ zV|-hC1n{=I9@dS8F4f*ctV)~xE7l)>VR*;OV6w)YF^af@dZ8_fj5 z$ucr~(C6>*SSG}t@KvG7kZ0T0;SV5Wp4DGXyYoedR)3t^9sjSTU{s-uC4#l8(Y>O& zp|e%dM=VOStDQ>|E6i-8Rb-F_Bq>WP4{{S>A>dTxgw@WZg53b1X6isd%OIwe z;+&I+RhC?_5L^l~!ZPJ8zwj`-uF^gh)qY_thKGy3qV>sfDUfgeuFdH{Y6I;Go9G^jRrV$viK@55u%*dy<(ZtwkTDYNv}%%Yt-;4{EmAEY*sKSNb5m zF4cWp5xXG}%taOqls#5o1h0><$YaptvA}nKe8GA``W)h!S2^yIePlG6wrqATm)Zgx zF*+qmXeb(FqvB`eJe^Jg&P$Ng@znC|b5JDv&X!HF);4#H9y*Kk~6wjEW`(_f>!2hJGu3 z$v6SKP)n0C&i?uQS#f=*vxpQJ+0du%W2w4UIbINcN)5BJ@AS;FQC%~UbOjjBp$S1Q{sk zC9UnDzH5lF6QCqw)sFt-i|(Oe}nB*L{%yLR8r?lQi}4VAp4GwJJ^Cb?O(2uD0!_XVd- zX<)l)#u16!-)4QMYj{MfvG68}{sb-=3?XTlUmCgkEzAQG%d2;jx$+f}@08bES7 z?&)JkCG%G!T9F~Zzi7+!aSy!Q5`VY;|JOSRJ73U-PJj4Ul&@Z7vAI}HIWK~G$YRS3 z1Gdg2IfRFS7qKST>SK5SS{9*R9!`tR#NwIE<79v&m=gOyq*5jQEa%0VskyN?TW3F1 zj-T@J%k>3DqF28pdeT2Y-nT)$eBFcZDXx6;kH^|qK2jI6&P*)HC?Id5T^Nk*P?-9_ z$^S;_2MPL{=-kq0RLVVls9+zm7l9c^Vu&o0de(1#Xnl#xAO75^55PwX;vKk35bPYe zHO!2?Y&mFu?ul3sFq>V$=M;Z>BP|g)Lj`*kU3CGk;77a(*JZY_bvW zQR!=tz(O!3lRlr6+))kQRqNA@U9%)|TfZasLCuM5^IS%<(I`Dhs$xG}9BtPfS(}Bm zemcr4dc`Ov@s7?7MdfS5@cB=@HwvMZ)-T^_t&ix=(JS4v>Z4$7q?2tmTYXn8&8;G1Ms+|C_lpz#k%6!v$y6So}xhIROZeXmn|uXAYJ-2MIhPcmbJTKfeX zX&*POVO5M<(1)(+N_5lDN_rb+QAZs1LhX^^LM_SXLiK;}kv{{#wy2C{?*fwWx{ zanA3k#2`6H017gG9VZ%DZsXc;UcT1S*;d*6)?-D0_O22G_-U*`a*qc@A>*s$7DY9Z z%%vc)8^5 z`Mn{qqQ?A;t6&<&fvVwF1`|5WN%8Z|{Hv5)!)(RWgPp`kM{nl2-n&4mkMg^czoyo{ zDJ#g1?H3jsXfT4sCr!DR{Zn;A(g?ibYdVe({?XmN$Jw*;8VXaj! zYRn+1%U(!VIMri_J2gO-b^=yHi!bKL0|9E1I2j@iWS{UMy+cX_xOXMhp0$LlTx9YM zye(}15J%N-J6|=HZ_z!v&YjY;tGU4dv{co;G&p6~Q-k(45)g69 zLM^xW+ppjM6ddV#rcm1xe_$>Hc}t8vK71Aq7zY@npt2~_cCxPpecOAaB@AV)*US6; z=fK|9;+$7M}=&qK~pci`ndHS7ksf_ts{(+AI zd?Gju+=Dt5w1SEWecz-o^j+;V>SjNd^sgrc`-o*p>4u3<8%Fu+xEo@V z?Nyo@%*c#-y)sG+ygRbX$6S9{nY>16tK5m?eL38Ij|&b>vi0UU5Lv{Wj@Oo+Iq){A zuW+(<^<`ri8-s8Cov)36!Kg;#5$IQ6@Q_eD;ty+=d^PXN1_vD*F|2CsX}sq9UFNmi zn%<$q9YyXym?Ua)G3gkL6tTV-jWp}qwo71>)?TY72c9@~JASj`^x1T}7M!7U?{r!V zQ?7+pO|3x)YJT=efc6O`g{-SJu25xBcuXxg)D0g{Ywg7_kKjj+>o#)9Gd5Pr!3QP@ zF(!XmYBdCtTh2uUpoS8y_V(Dy`?Pr^9uG{x0O=?n7wUu5}c z%|Qe5Ua2v6E4(OW^9f1Wb^rYKFQwtmde1qEbp1>|y(}F<#bdx=GHRMHYbj|&p5@!Q z)xyC(m8*iY;`*2U1wGbYspl6hy!DGIg^z<77#7VEBtk5`R?YO!@NNAXJgp@+UiTQ* zfLKEJIYz0%LvG&%^w-o^HQv3(<{E}3G4(cuQw0y_o)=*&Vw1heCRu4AVl%rz=mG0FD^23cEGV=^nT8t)BRAt~Z{ImsFt05YJXC0*O)}IQyrL zjva9G7?fo**w@a2)o>ohog{|1=Do>{FOEqK?Cihf$IEFd5Wa@}4)%;ZS4tmG)ifGm z;WWqd{y5-fWMKDcz#tA)(y=juAJOtNpW6qH00gfm4<(J}$P|1C#;1j9sn*zUADG~_ z{qc$!_EnY4>E?9Lah1?wKj0()+WTx16n?a6#ZE1hjTiZa;GVN<`4@Y55V0_rDimCb zb4}ewb$t>lm2Vyu@bEjQ(R2D$U9|+->w=NVO!`>H{?}(}*Ny@IIddaLF9aAnUJz#I z{XBIJg8shcQ#?aFV&yw_1!*;DBPRIuiOQJ?h?8_AV8u(zf$GP$zEwMFaK{V%SFF@i|l(|nRcqY>316uOCLC&j3V9m zf&ELVom=MyKzP@Q+V77s@5Iq#lVDv^dJKqD8Brpp$A_H3r@1h($R)dpcbPP7(;56v3bT1JsSp{(*J0W$p|0AKNYAv@i^*#i$V3r2SAAL{_F4JJ?x7xwwB&;v+{5SUZY)q! zyB9g^lpI=w;4tylUkLLeZT|TvzoA2CJ$&Z4$p4XrT#+7lOEp-UC@qxLuIqMP_WhYc zJ2yrSx7g0Bv}jypk64n6648bd`bcqk=QK@Q7s)YB7t4eg9A+@bN=-x%>g=3NESzBZ_h>t856m`P|L zD%8@bx%_(-8v-J@ecJIVaix#8o~8A(V~<8&fATP!`PYrN3bXYd#p9&daY~kHgzNzjibRK1q0!UKREIJ{wf~ zjRWY|WrQW$;9%UyEeBe(7ux(4h~J^XP-#&f*p|KZuy5?MV%L4^R2!ERu8kF z1`DUn655LdDqpejL;^f@jTSRV#*j-a|NJG@gr`!pvBs!YJ1w=+>sksdaeG{IKEq=F z9zokDYp!erUFhUjgqwxDk;O}c26`wucysB<`~5-}PTW$8+4Vg-Ln+hK9n=~zpmpg0 z(V{y0&=WDeW9?`&`pd4E{}Q2902|%U+-BAMy*_#e8h#LoN#++|(UDeW2QY0prdK`5 zaYStuuM2bcvH%Rkf{ZWjv7fsT6Gg5gr2FuTt@O2S6BbOGm8qY%(h#Gv7AtjV#OE46 zQ%z3T#hr;~bM0k=vbpf&Yw3wL3fOHJnW3;OEK+Yk+17&t)l|8e-liRV8N7xmK%SCTX}yLzA2SXtlw_kd zHZIMdBN?{|((}qvV(Qqo0xCY&=S($4TT1kg?~w_X6MhH1Mv#haN(hZBnU|Q zZQKWf0h_B8b9mrm%DV$LCNDx4KPsIISs%RF%|l1n&9D2!`BSQttv}vtf0iife8h&9t)PGeRTyYQ zs1J^7u5f7dj)nFu6P?IL9B>HWgAeGhQm$9Lr2ai*$S|`=NpEmwT8<`DGFF3dNoUZ> z_e}v&Xc;SjQy}mT22o64ct$gLc9wc#Z0~-sSr+%M^JMu}=M*mqj|%4U@ns9k=RO{s z*m@^pO)v^aE=N!F2-%G_Q*rfq-CK&YV0gtw9YU!R^YSWK&xi+CNtJ~Wpg`gh)oBph zR_U0Wq>d(nga*rTXG?F^hYzwEBG`XKexNf^(Q~|>SaN^Q2wV=25$d&kb#R+`z>41eG?A@=831eN_>!re9kBK#dZxBm# zlFD2E00U>)v#rFEz2*r~Jxk+3{?3jP<6N;2aU8J~P|GMNmGyy6_wNFv8Y4Ci?%4{Y z+l&Nsu5{2m-7WribCuN%;!OwJF-Jvkgq%b)Y~(hOr0xQgwg}#JLO}4xufKcHNm=Sn z>&aK^C>z+W_TigxHtAiE4H-VA`0Q{xI{5P+gjGUjv02%%P8JH)AEbXxb70T`A}gJH z9RHFLUqV+i{!xPlzatJ{2-2`Q{NnK6N8F?#pQ-q`vxQn?$&82^V?ni`k6XkZl>`|1 z(74J0!q-|_tf};GkCAO&rnqJrv_o;Ot)EK&*vM*BD3VCQ#yT7Yp@mE@ z+q00#oq(JDjuH=N%W@8G_(Ycz9wxxpgj5Ujn_pMLFCoQpfOvV35|(jMiGtB;yFlbE=hB;Eu14#hlI&M2C6H=09w@kMLUzw9K(#KUpL?= z;5|B2=EM#erL%!P%7jYvTjD!$a1xZ&kc$d^sNSa>zLzl(5@z9l$O3weuaFePK}S;K zH4U*9Mtf8+pHf@n4nG&W3_{{M94mBFX`jnOC)06WZ_peHDCtk$^?K+Vz2~zGQwAUm zIEe!1?RQb!WLR@3cAsb0Zk>k z@LSjmMQ33;O2Klb|Jh5Z_y92Kg>KS|x}nAd8xFkr$)TcD+IlFuI3iwRiVO`6{6qVC0&b>XzgYmy_AU?qOimh1AfC<3BF2w zx@oGH@zw@|rSaif@%+$)#*lJBOo))ELGsYY9~8PCgdjz_9yTNfSvv}NMctt_5{p|T zgu6^N0d0(pHpXonOq!;E8snoWeCrPBI3}q4TLz(BXqapCk03e`qaEdMF5uan2tz2pY%6LNBE5yD@4{AfmlO+JOyU7Y2=^BavFBwwAMV^acb^l*pWNp7TV zM@7WPWSSvp&|wqJm@#CF)*pVq7~2PEP1V9@(Wmu0^eaB=8yki&iO#9sJ^-twlo$R=R`d7|WkoI&4teoE61 z6@nKj_>5`~M;?l}&Z}tcHkHi3R|vZ?msr6mPGtax8nBX$3Iushd`q?G^iGZdR+pjx zffr{(gWsm0+!NALDm%fOux(9~fy7I^KRePUOY$ zflf5g4P1?P3mviF1j_GeXE7oM?(v_{6PXa+44oZe3+Pi)tS$=qHdI=CkE{aYgwqMm zzsqS4Pcu{m2{N~!|2(V6Q|-7Iy03yoNt(I@UOo5CtrP%jNwi8uxv7}; z>6Fr>+pcupsoh`qTjYw)QT+~*Cg|@E<{g>~zs2ZcVQChYNy z%P(iOk5@TvL8TXil;KOaTW=QpIs>*TpIdxs(o#`FB|VlK^5Bkth(^Q>1JQlTUhnIJ zV0UB#!5yuSzXgVF{JqXE9-`#orWDWoG!a{!s;5svCrl-M49rlpMY{bz3vz<-s-1Pw zjh{8Ys!Zl-9lqh*pO65m) z>4X{u20+G_Ie=a^Wh*^}0VJ>hWi#kOJON@Hz+}*%I>{Yi(4fTui3Bc=XoYe`F^hH>f&Ve@& zN(gg&3_2^XQd9V~fA~Q(PwD$^EQh~&Z7pempu9>#5&%xLiSXvJmPDCMdC6*X9;=;Q z)9hP4uw-TR5iNQSm2#XguPj`vj|*RVea1VR8nQ-w%G4*RD-w0$LHRBsP`R((D&0Hx>O z)y6%_pVjb9+WF^t)J!bY=+UfySN&oiP{yC;&jc4mj@5=N|F1p>0dM~)_osdl0@sPL zWtm&p`|Y9yf~!A6^hvc(HJ(JjmOKM{ukD9+08Tt0UsjD^yV2w%cYaxhSaC747yY|p>RAHKxixh6z!d66u-b+{ZB`Qop(_HQ2I{N{cnYw2TSz5+G_ z33=S7`;hXB+oJ}3XaX~E#{)!t6wd!s!DiXsSzBva75>SNC^XbucVtA$Y`w5IHPv-d zk_TyqadXlvdUKsDyIuKrhitbdlZLmqdmp`7@a+oN4wkU^;;CuNU`JH*F`BTGL}8t2 z$~DP>GM=GY>b$*Z-8u)IU_J9|)0gcXN_T0udlc(x!syez(jv6Dl~iHd-@EwS^Abvn zZxfP>bZ{`+`1o))+7`K@|5F#re17?9l8M>lyfl16X7Y|=t#ixBAP=qQnU?|p(wK!4 zTIO0SZ-{@ymgoHg>LSQps+*)>DE3OO0m^HEDO#8L!9RQ!s(^me)2XMsK~U6PR>)!0rURMuD=p*WKP6c`P zFB4NwfNnbgs4*7+myi$H#hx<}1fii%8}CK-xeS$_L6yVHK}AdVuI4T0ppc&hcXZw- znWpx2}zh9!l?KC2Nu1R$_nFudCe>>wNjwbzTjPSlW%{zVHl|=wQFi`7Yx}q z&W1kwC4NSzkmuc7N5}$EA!-zqk$i=}(qGmXK$1IT`^)udQ@t;6^dC822GDS-m zi`9i7n2s!u;!%d!4HjNS$ai3918}Cq)>VDV%cfKmAu&)t8+m+v_Ulgysf&fF22zW+!+BHNgkSvqXI!{B$xz)j%C}AL**QRId&(DQcqB)#gjM&uH8T5Z{ z@h~yYe~LWH+BROVqii-P$$Sb9Ak(~h&ej--DaY^~pI@8Zn{MvWtd zW&l?4cK$f;>>-Z&Bbd30LU5xH{4cAYbH_87Hg$NbPM`ur14Ix`d^(D&zHMA9HfXdk zuxPC>>SV`%1P&QDkC>$zy(@D9JNmAbsOZm64#ZEW18||#vAb|1KZSVx-7LfeRWSU* zmG&hc%||4vC8s&Cg+=RWv4MPX(%$t_`{I?w3-`d)-_J107Au*PyIs4Xr;-nt9xPa` z66)_D*KBV$QyICq*W*qKv2oeaa|!HtzCJ_F+ekb2htKTE(etVucf#vN1FW8-!LmKz zKBynsx5LkS1)n0qmoGgYaV8|kEd4p%g8y&_{M{EPNH3?oMW(nNM#JVo8+?2>3_OeK z*f!=#Y>6*>1dRGzsR(+y@QST?=x{U>##W`?aW_j=i7YH!a(cODYzVLpy@tVbaWZ@) zQR;I-nluy!p$W4T40UJ@{C2zKbilR8*1A*p!4PAMqw?N`ZMd`4twPhiQGADuf3cYcJr3Um&x@6z=`!j&t;0O3 zQ`JQyR!^0T;p02e%(j*H|oEDt9>-{=Hodq8UR@-6D%@CLRYNs&TawGo)J5$3g(JnWu>yCX5sr` zN^+Ebc?aia29_FceV!!2fz6)!<+T{}EA!ZtqG0^BpdZgvu<5wr%BLJFkB4SwdJ93R zJ60zE;`d%FCIz%sg^k?j5~=$kS4%K=GRib^I07M@6SBLo*>>;AcU6>Y zp3beEhf_n5ArbW1%qCF9)v4-azC8cgo3~;lu-l#lSP!#ULm+k^CVHa4Q-$|e-VmVm z%}dtugB9K-rYg?7%6g_DBNSsyI0?#M)PZon?)zoerH3uw$VJlbVT@~v2d*_;(P!;W z1X#GT7`}c%UC%_ziRsAXuN@e{{H*5lI^TvSxQLAYt0ueh^uhI5od&YzOZcfPfKMme zS=sSD5K!a>UmRokO=d}&v7;CbLMOu<-yEe*HbR5y;tHyYN$6rio&YiU5$ZidohB3( znd{W=@e!KZf^vo_bjkok5+>i2pN0Fk(MUz?o}(_bJbO$0I=*Y(9w{g3Ch1G!uR>i+ zp3cocA@r2);h(QNB>t*#k}gnM|4e2N^LOoTZUh0!cJpJ+i6}#azdU2}y*O#lb-)O5 zGvq77{&P@pQ`J%u8K*Fd5Bo$wveV8@x4rcLO07uJUGN{hR#rKnTymcnyHMKe*{@|^ z@~ie7)(h#0TYYmf3>Lze;E$x|3RHe^(f|UuVPFn)30I<6U}wB*y=Q@JB;8Xnj30d1?>kl%SrcuTQ z({b6&vL6j_iYva*zUDxi(m51K%6R`72~Aerr%K&8x9_H@rG8L-h(B6Abzn$MWk;w9 z?asX?`lMRJ493%E8zM|ZkHi$ANNR_k^F?PjraXNv2X^^-i6_2Pq)(KiY*a)t0#rD{ zaxXB3C4T}cClBj3@r-XUL2ua~dl=1l44V+;$j3Fp!=amCKqhX~((icy$ci4{Tfm)7 z!B9&cVg9Yr-UIatmuE2lb6Jdd9&=>B9L8{E_=Z<4`dll0+ka^LO6}tEe80?l^L$$9 zPFhSTAGf7hRb;g-NGaZC%WZz6%HFj^FqKHGL_gi`k?LHp5Uh1PdYb$G5cDu*K<#)H zPj-dJ)xV8w$*s{Rl~uf%B6#$>C>h|-EH8u6QdJZHuo$7^a~2Y7oHdnA0+2xEI?)i> zM2C|gvao)9?f#2y#&@2834;|X>TiGPc;BIzj;5$@{W6DTP6*o#88at_IUyZX{*u!D z#c%XoU*n}*F5k>41ndfm&IPF}0R(A}8KUjdK#>tzQ3p;(oDu6XL;5QtBBAd&c$T_@ zfazGqLTS@e&Js$?qf~~5@qArrS7M1Xf{`rm;YVF7J_Qk|1j;mr(n1_DZj1QJGy(zT z38ia6-eZ9^H0XzfdsQtVZ-kh8OEER^6fqN`u{7Vv8(PuL)B$zuf4a$fsnC879KWsc zti@bfeyYml1osHt%)_cwUkp$%iku?5IV7(NY5wBebpw)3>8K=+7D)WH!gK$%AMN{> zDN+hO%Yp*;f&f}P>xReSHR)+8@)Ci{qxdmy?%#bG=@N*yuR=2kmZkpC}h`IoGDPH%6DQCup6iHEGGCLatI#~|hMSta8Zx?Fso1BVYGBz@MEeI< zRY`EpV5*W=^7|CmK3FjYaw$XP@vvST3^~h&bv~1bviLlpnqEP4fPBp5$6$X_mrA6_ z5&!+Ly5zII_zr4A;n&{y;3Vp3CVnP(fA^2cVIt*8#RRIMc2^0{&2(3>mIIj4s|1&d zb&IH%$W_Z~3#MPb2R;WUKoNojd$-qD+Aw>F%lMmQOWtT57PbJWIvd>P+vodlWSHQ@6T>{h!_Y&$mQne>4e=;cT`gYibF0ZT`{t-4AYUzutQ&> zkN?MwHLgdWi9a4jBsFKhSj}B<=GuGKP~hg18ED>}Fu29f(X`7vuq!q_wkVU_0O@9v zhkqmvmf}oj$5epR@j;P@@3Qs~g4bhr85=#uPQ5?%d`K#y6ZJ?Q3FOxsqhV+Y5e1G_ zzHQT#&eIY#9>x;~3$4RFknU~}q+{uj6r{UR>F%YwmF`Bm8{YNz`M%fn{C{@O%$YOio|*eTI8HV%k=&Wz zAh(O50bZd7rSY{5~iUN z+#jn4$OhL(I{b1J4&LU?)H0Y3`N#iGC$pMCgXmuQ>L`nzjMyL;)>Tm1;Lj9F`&}OI zp|NNIi!PaChk(8sxr%S7|tScKGkf^e7>3BvbRGP{Dnx1EH{Wp+Kh zg&=>@20hGnEjBq*ee70CUWy#O89gzIL67PmbczfyWNSOW=$L*y>;_-k1JN$_n&R8E$d@TBO3e6jLO~F zy5@9}ln;xj-t9&35oRaTQ{UsOK`^r93!NQH`-)eSuNBLkX zf0HYfo$&fh&`jqKbx;uJt1GF-*t}*pPW8bg_w6ix_(m|C6H6sg|6>`n{WW|AIN(hf zAqE@YUwvAq`5-6lF2*{9&Q!<qwY11 zZQr+!el4#cf40^TX^sefpGo7}{Po&}z&vEk)N0vPV&oO4Qf2WFQ%0=_14E^$iJRG& zrw$U|XF(W?3i4y{ZbSeb{__5PDO_$Kx>-x#9niKg+rFph!8?+=?JfK zj)xzlWs;_w+tm(uq8(1?e@z~Sj$AipT$_jmql*Jtgu`tXseX_LG1hn#FSQJb#(drBL5Yw)xc!O*m!Mw?LytA7muFz9P=2Xw60{o8xM}{^iA01_MC$+=fUUA@Vc1HB z(_4DL!Z4DJ3~@?S0@cdq@$R9rz?nS_RfqJ`{%+XYhaJ+y423$o`$Z?3XIcpFze!KV zupe-&zj37pr?_Y(rBTGC=g!u$-k)_w$KsiZB?%%t=BA#-6yiTj&dwsz-pg=|;Zzy^ zzUWe+v~Ia&&So(2PyJq%cI`4aYx+Sf6fq{J-H#K`VfOo~c{nxK4)qL`F{42cwL6Ae z^2u$gC6p4j#}8_`KXLS zd8vPcDfbR^h6+VLXf8VSm{nLSLG9iI{{YD_4Jl5Ybt#S4Wj#>F!Z$9(>01tEr;_I8 zR{mxDV$l&heiXoa97S&1ftVwv6Z>pMPE>{zU5Fb89DYQvqk}JTM$elP41cpo^=^U) zhiX~?75&>FDlaFqI|mU+f==jNGzonwoq&dojXdoja8>q_%weFA2j>3r2uC4301 z8ip7+8oC$uBZYD&w?CWaakUM_O!c;78PC^)`u{Yh;1cW?9X_Sa`ffEuopyLdJu{I? zy2B`1hdv<>nneZBf!t3T;Z8&@vO~XD!ekc*4(F3J$e`RJnj|0T!BO4jR~-mYTJrJ7 z$NHJC?e|{T`U#mIRhYyQ!VxA{y@{fuzvAq&na(L;s0%6+rEMmLrj}r%El9}&(>zX5 zIlesnK{t$dhwlp7dUtSnzh7rFW=MIhq1AhBvQ^9A@ zD4HOz$s_{+Vmo<2xwHq~@8q#cs(JeR-*B;AeQjR2@2)PZ*D$wm;UcR}AyrN6!XN8x z2sU!$aLxOcQrDbs!|B->G9HyV>iYNslRg7ux&|lZ(Kc>@LDb?Ld2VQ&hyo%2HoQ{` zq#J#14D7AMO=oc6_uQ~D{}@{IZgg#a*1k5mGG*wUoE2T}k^w&eHPZ5b7VCbD0NxG4 zwAUuF`zgSkq--ynlFN=TU?_KFGEM z{C$!apeVHYY{%UDjJ?DvK^P%Ghuz%ippe{XJr;8Ki?PxBT}^v%1;( z-T^yVP$CwK!iSIY4`>};Wmg&FqA*dAy(TQ2fas&y`NQ6{Z%`%~UqQF>3HbtO&I4B> z|F5SiWD%qQWr!g0B1ofCyXh;dvc3MibH7@R z>)gUT$};??k^vkJaj^7s0jyR)@GH=Pm?=O4mO{|zI{<`wj{hseJO*H?QahU#SL zlnno?CZ=2-KWth}r(vyj5{X)G`L-;257{QYg)~YOioA|#sW;S)0|276&p;OOwE>JM zH18YC=Eli58$VA6!7~^V>cC~{ zA6b=rg-@c7W5LJw$}hqM6z}R~oAeMAoD)ADZoDNHb-6vQyU(fmTVVZ!D08PF(9Fm8Z<GEnAm#Xo2?h>hl+vkWR5fBJ!_A=snYu5ImIpG1cX;6C zw7SXcV-cE5tjn%-^xPly7%0{FpA?`zc+p1d#^}9W$fc7!rd&xk{&3)w0r(sgnTAa( z>e9>}WqUd=8f>FG0H+LOcST`3JVD%N1B3`OiD^>V6P@Wqz3WxPq0= zB)X|0v$3yEgz^+1D_)@z4rt&#|E9!H^c`A0Rst+1%Dk+=j;)6`Xr7)hDs#BR=j8@6 z8-s`SehCJtq3F2dF$%ffJcT&3iLMp10_#99_?#I=4hjk)wgl{5xj9|U`U*cXX z9Z4izT1oaoiC;rOXxMG zc&_#enkudzLHZ9K(U}aB!GD6md&+Cop~`ST)$d^wem$)3=o|;()VcU>$R@A*iV7s6 zWPeU<+fI)MNkgH0J&U91of1j*ad z_qu+*JKLESZ6$>YZmLUxRQ$2#4?YFGHoX?!AWL^|#W8F-ObR^c{>VW6R`AJ1K~F+= zABPOJe7fxN6(sI&NzkJ0Ieca+9>zQAsso6I+ZMRnr;N z6(Y6rii|{G@YCcsfF`AopG4vQ!zK}gPsd^I0#6WSt;GMY%Q5LZK=k1`7A%`zF2;6Q zs9k`~Zq=LWz~wX9_+Ur>5uLV)U+<5Wi7`N(7vh z{|1AfMkOC;FV$qk(&Ulyg{<&Rf%`cxFOH05|A*UUBqMwfhY}v`RR7KQH>z-s(AAcH zcdMJ>!ga_)@^XvLir3x*s{{>+f-knIjs7yzTUH_G$Ll7Jn&wE{T*icgo8DD2gi}HQ z1!#~D*2rXkx^G)%S22Brr(zRS$@)5P*ctvq92ZgqlFVR?)l8fxt)VY3A0%&ug#PNV zs~V{p4Ws*l6+uTXTpbLq$>(mwt3Wq^(6;~SORPi8!E*wO|2XwGtbKLVPIuq$!tyBg z;gwJIzowwE9uoF83}{@HLRO?lB{S-1V-O29%k9JQFlzaW9$l*WHGA0-7IM&i5O<<> z;K{qPiKPiSk`-i{n_n0&iLzpax7>HJ@0{>5`>$#KilEknzYc(?X@H^XAVN>LfEAHR zsTPAT2)^(c;wO9aUxpiElW9!s?8-*>DlHPkAraZL?{f39_{$%(IkBi{sn!b9DgOGD z+H4YGSNe`YfT`O7r|};#v_f{Aj>h+SKSIrxsXQJYZ3-(0_lbdx4vHmVam{bBYrvhk8J{nM)=K#{88f3uXd$Y%E#Kjt;Fr#0w>AHCx>%a#ZtKU zQD@Y$3v(R|-=BpXPAzi|d-13_am{5|1}mZHOmHcTaHBQwaHrUiFx|1ck#yp-6QI^O zh5-A<%S-b?_M!Awo`b)+xxI_DOJa_eXeFcPw=qqBZhr4!1buCrdK}ryH#lWTdnQ!G zABySo!V%WVh=aL3(VYe?gv`Ejf6{iq5X^ilA2Lj#aWsP)I_DvKs|M440Wy-l>ZUiA zYVQF?WacbI9Pr3FQT&W(b|a_)2+#zYM;J>{XqAGN;KN7k*7ft%Y}EHPB`NrgDXEPp z;v)mzHVSyv3n$z|2TDL|6|7zMv^DUcKVQ3k-_^Rwj-J6K4CWaNpYIv@L0Q+*XAS1XXg?h? zO#i0jg9hpsgs_;la4B!MF&0_2|-%B&VSwW z!&efg`bC?Y_Z^-k2iu1BG_sBer#dU79QDb_xmA<#?SJGjPn<&A2C%&54eE5K> zE~xPH0VpZu{hpZIMU@pa5h%T30#PV7B!p8C4z@CySXlg{?d!A}5SjmAP>)IGi%F}$ zh(jj0#n&t7^*&{h*4FupLM^makI!q03F6e%%*aqSRU$ur!-}ab645-qt>N*p1P7S( zh&+Aq5QyIXu7C}-Be3i!7O}oFR^b-9_+W3eEepNeo;)o& z#1t%{7Y>pEb{Tw@a3(xwyZzoiK#zuo;C3`E36d55FhBx;AH8r<&JblU|AKSk&WQpQ zdwjAwr?JSFvoDJQ)k6 zMeT)__jG4l3v&ymFgq5xH%@+=_wV4Fxk|N5Ou;*=xYD6Up1`w5xDc=XgcK8(tcR=? zX7tr>M7#%gqI|lMmTDv)q+wYiQ`?IasRRe4MjSh1XD|QZZ3mz8n^F|=sb_xZCq{At zF@201Eo2PJ$)Ta~K0;)b*G(%L&Ej#aclRGZU+NJ1R%9(d&B8F4zVoa%JNzX+>&A23 zbIc{{Vrg~Pz-{FW4^sSQX7$p|YQZ1-B9vQXQ;ijT=~nmYMs~5Wvc$}PvNdFzi8~Q1 zv*2%d94_|6fOs$COS|DbD%dV#BWzomD1e!~yS2O?w*nJ`O$FP)zpHaU+xaZp&kDc#IT>;+~@|M4JN1SCdA``}Ca3J43;mlw)CzMI4SajA?9D z9GN1pC*LQm)68tEwY=Px6t7G#8H&SRAMMHnAC_{vs3dgaqiAtjiVGLMw+hnZl7b1t zN-t_&$qMmzc&z`t*kxr=N$Kng&K<;58G?y~}TBD87Hh8?hs^ zc$<~IC-pqxCF9VF4uB<5~xtc^}VRjfwEgwc>x@5SlX+CINCMax0M-ZsOa8^*cuBMFPR;iLm_mqTJTmOzm7cs z3xZe>WEH^gOclUZ%(gHV+SAi^A>yBZZ_K`liV*WE<##lU578$u#l}YvYFqqB)ZPXS zz*pL7y=UiKr|db+mZe_%mcA+-RRQlYJc3SMv|_cG)@~)Vi_hX?{@nHLynvV`uqXnP zYUFqD48PAQr!%vi4b{%DN_xd-RCC<{dT;%Cvy;H*rvI<+{pv|;_0TdeA;CVznSZk9_=`0tr$JNvKk1m9Rv|%XeA0^~b*l1aIYlM37geFzC+#FJ=bG+2 zF#tB%BHG4dp5UQ~Un+NKnXFU7opO`}z~5Pw?C&%@g^@kqOR|b(bk>Oe!cE~$-<>V@ z)NY^tjv){BkpQtp`%a#cr}u%3BR569ployMtuY-x$^^IS@x9;4qAAKs_T#kKVSR5O zuFPbmrW6Xfyxa{1jS4m^x#StmANrl2n6Q2GODi`zkW3`{eK!SDe}7EtDUUND6-ZsO zZ2bAmk?dRHVBzCctvO$+<-erdyQ!TFGe?@mXKH34WGep98AFq<2ADBG)JFYBt_s4c}z{{MFr@`G%+(v|YR_3D5gk&F3uOAS zz;}FJytX8@IZ1DPKyltJ)#tI(G*7(sr-us&jk1Y#;J;B+>KPDoZWngSVjHavJoaE; z+}ENpFZ41EwXiU~vv{lV!)_SKXnoDHHGDl=R}#92C<0dxcr1>ffQ|^SuIgyNb*$BW z`xbJMVBsO^3bVz&gejRt-?ZvA9iT68C3~-kjDTuEzr)2{H6?vA*m2NI>R?!~NEs#l zHB$Jr8SV~+`J>s-P7b&Ud6jl+Tma}k&)Bug*X7Bvq&Z<>B+s;V{Lz`fUoS}JrrSJR zVX4t$Ip;7t@sJsc0`H|0M_+kW@CYxqcfv zRc};#f42Dv->$$by}CSb+retg1C-*lxeim_R55t|y0pg+MiYrDD}S$Q zN5wB!L3{++!1d}+$Cc<=5AXb$Po442j2}R0@Oi)3e{`e)jxm*~`z5L$!+wuhOV;L= zGL9^~?{n;BOFml$y`M5t%IW-$=E+?qe14(J?`#TF8u8dHP7K}sLR?Xod0)pO}crMt^m+s7!Q;Mjmxoo28M~)$teg`x_C7{Yz@YDtZ zTyU9$uk02fnNbAh-zM``HoFNP*ebR$w;GV7a-Rd?vUOb$b_+*TK~nj-mF>82op_hZ zG^81P@mCY6T52;Las9-1qa-&)K{KM$!=!-Sp_QYg+ylq@JD1MwYYF!UY*eplPh*jT zv)e)GiFB^+_NLdETRNDK_@$Q{N<*a3w|D*F#1f8w2Q81Yg^pph>=7=(o8@elij`Ij z=x&DHjfbvFiLifZ^}3rZXac#z>&kwvb{&QY%xCPiw8lxgzs%#0Zp+loO$v<1Uj-B{ z9cvl9AOhn`RyY^uSbhfDE}IEM+s}gVFMx%w;@5D_=DXO%)7=lx(0oc^nNWXbE#J0M ztNr?jriWOoL09z5m)EaAuSl=A^wB)E1Vl&_pu5xD)*Tcle4}=P3$~@Mg5+G>5qC-P zQ$%EN*lCjQPs}ZRYOG_4o(0&!hwV?Ez_{kGhu_=Mm%U8*aI(J2L=o*{yhsl*w26j~ zHuHH?U5)XYY?2U;?}saGf556jxAT4*C-2#ssST@&NeVs%en61KfPvm!)8+*#Cl`E; z6r9{_KRfb;mt)VZm_ws98~BJ^9%zs5?sBF5UdK7wK5rD@jF1j_=6B*qB}XtOtv1-5 z`YAqt)1OJf1-P8K$gAB`4Ic6)g(VZKjm&Vx-v+qCJ(rIeuf!C6|kGoqRKw4JvpUe)E7Cy+!)0gbt4CU<6n!+Ffw z;>E=WAk>Z!f4$H9V!}O9FCeG+J=qr)&PN7kWw0yfg}VmhU0YIeeK9$&BT8nsze8RG zpkyAaFD)2<>!d{^T46!tAoAVl`@73)QVXtl6m40XfK(+|5(9s#a)SFh^yL>moZHhL z0x-*J;D2Ebxqnr(L<^C{foq;2J3Nr2(yJ}b^Om0}4 zCiHJjgnT#p0hf;${2llK@+cmDmyNOx9eGV-L60MU1s9}&ntQ-DeDsy<29b8TSH@ub zDikfjt)%CZ!%bWm)G0QAQyJ~oo@6l?z#i&z3Xr9T3RQz_5g^PA`TSfzahr(Zl%cDn zA{t~9`V@N(l&uEY+Sq5WecQ1YPw%)sP4pK`+TkIwYzNBmSw0F^BT}b|)^%*ig2VEe z*QD{0VzWl?y>~}b5C5H%k2*IZa~}=b?r;D~r|RXTzP`tA_~v8Z!M_8tkJ}MXw4Zo* zK60-(8enzr8k?aDeg#y~;Dqw;Y{3o>zAq zsect=)-ytXu0)IQsU8VFS2TqCVGEu3Io9<0&s*%!$;OLoLXcjAM>hjs;QlbM5?#u) z?L0&K@oaA)=*tIiSFiV*H$#z|vIUaIaU2OeC(ARTAX1M3?2^)^$z}61BO(wMGCaI7 z`1|f>!@2tH>I9L(R5KOpmr4$}<)g26aCc?Ts;@t&?_C82(lK$X_ei@ce4!%I22Q=X zeu9J#2Uw(Z?if$3%)!576rpc6afT%X+Zl z&!jyusDK510N{RI>|vb6oq(0g+768K=|%BnHwZB_v$FRLTt@lXQ>&2C1rXF)5(-Ts z?2U)AYduXq1U?+ECc18rr2Tt*m3R2r_fF%_ zD%Md+MIVYkMh()#DHia$zteQ8F~3``D&XQy_2^oA_MkU0O8_k@O3f#cia)Pe_F3Ws zqf}%TJKf+T4r`m6Dp+NT1@~Bh9{jjHmJH}M6zuf9*P3ld?c?SC!dB|xdepg4g2QBR zebpKmvY}72g_j?OWA%p$Js1ali%=_w38P~OWDFgV{`N#3Z|0Ar9E^j#U*Avp_w*Jm zFGL2!Om>QIfuk4qwk;rG1rE$Xy{25@a;RU{n6cWJvH5FuYF@FxEeN=eEF|dij3x>&e8t^^L592$>`JM(QsxQ4mh+{hh2`f-s0| zZaNekzSMmPTY5>IOdfhT)5};EjTD>lY0k$vI%SHLD@f(G7kl|e7P@YG- zn?Fso02p7;drZ`rgc|;1<%jals?PM ztH0!V-2BU)8pd%49M40}O?qWkAdB1SjtdU`8@?${3Mk^!wkPsPiek34ep_GOE4xv! z(83Q7P^AYl&j_(#A%L2P(UO^g2FGs_bpVvn!PH2oNl8TN-SeS(&mbxqZBho)WIPT{$67j61ea*2a!=rvJzoLHi8r}(uSWFsxlkg2bP%7clQjBeej`3N#$+Uya$gJmiTaF z^Ah>R;fLdti=MO0pOJ!Hf>?i;WBmpFhQIw2(eX(`|0JU2JUQqH*AwA~6I(D&hB-xL znWCTIOni3Cmz)NSe&}U%5!!~kkO!hUVAOQDpx(okQhA**^kxYDuE~!3t6lT#_e0Vh z0YH3OMV;kT7Zy{5**EHWM7HF$<8yL_%M-RK7|NScY<84-j@xmnU);+=OnlE9(ut`Z z&#_`fssw}xEm>eM3Z~Isr4E)@HU~J>^{uGsm8Gpg-9)54Oq4p3^JFi08y&I@=_-EDv{=c7%8A#OXJIvCT>8NZKsDA#I}=+qGnP| zKlG)vpP1?~70=J!_L`QpVr5n^!L;DH^M#Pv`=yKt2*{llc~2v5QkU(Wk2*iHU@fcx zugFT}K;@K4^EqDr-JkA_+5w{llktK%jeE*P+8 zoYJ_vZ&SA)M>@S|hU$&xP^c1|3O5<8Y)RQL8--ds1CP7Dq5Y)a^fuWMXM$$kk{U{U zSw6@(lQxxA$uk-=e<1`J)nANo6KJlU!o&IZ4_a(4A02-?zlh=qq_A{@qNq@=Kva5n zjExIy`&wApUEM&?6NOwB4|Ju{ZA@2Ct{A!Y=RZi4Z~S(ODI=AqO*SV|jYyA|IS~VR1 zeCuNMcx*5LgW=@Z$JC>tpLmUafvq)+%0b2m7Mes7SScOc_}{&h;|5gxrEBW8sdW0H zw!tj@P)jDVU*CAi(J=ed;POb$49tlg$LPHk=$l+ac)Q^s#;;zLPA|QTQen!nyGjF2 z-CP5fPk!ZN60p2tCw;d|zdod*tFwoD1l3o+tf;}G$?`YRt@DUBDw}ZYP3G;$cdRjw zY#B+^GJVU{-#VYvVSEQ%a_0a?fZ}O7=LoBlApiA98!-@z;pen+pFXTK?1vWL42J4( z%&aAT{;XED80!GJ39LjKY2gXkjrp^ZKJBn4y$7z`ii!a6`Eojn6L72EgA9(LD%WsU zfNDR{(%`Im(0S;AjzP2eR`0ZO>Pv8Gq33L@0eoet$D2}fKpPe*wJQHf6- z^*<5cwvao&#NKZBdmJ98bHlK)nwWn{rKUmModLG!(Vo=o3K|r79E#uUFTr zF}c_*Nn4i@G1e_!o!2{JLm_?v!rOFp@8Rn?q(yZi2czP-Gm$klSC?W63NN6QHaCxQNu2>2^ra>elHAPm#~>#!aD)*Be+_yeJ` zbmNv z2fRm7J;EA|L}4{fjkB#@zOeD*o!cki(>C&kI6kn-LP|m(U}!Ocpy;|ydc6HQxq@K) z*Yw~39MsDaP82VnLA&w`+c-1K`#uw0+k>Ah_=$d6FCwb{po@S`RpJV*0de(@9%VI##)_jz)5Z01M9t(G7k43&+kiV_mv4%n|&77gtZzO^?05v+S z*Y?6<{fBf2Mo7qCiClZb>p|S~2d=yc6hNr_y8;#Ra%=8z_(?SKvym^hNfKF22hUZzuGnjHI;JkiD%o4J60PN1#4iOZ3a56I7Z5+z(!QX0}xBfTHY~} z(&TRJpOuH9rWJQyUcTlQo;G2*o&XUb5UAi!|I%R9t2@>lTFlJ?!=sk&+Z|1OWsagQ zJ-yYXHmwgXJ8BIA49OmQwh#bB)Yi5R-^b(h6_Mm!@EMHXTs6}1PdFJqFk*))db~I6 zWZh0X{}yFW0e>U4`{3*vpMBiQe;c9SBA=E0hm`M&LKu$=MzfM5$f+Z0ZYeY+ohiXb z{lKm^(Y_&}s~dH}IQs6Y5C2JNV~Pb?e0ofX1n{YD-RV2otDhA4!`Tr?rI^7Pf`g6% zPsAFvCWGi->iMFwqevIK+|rQ7AG8aF@Ed*ao?@2JE9KM(R2_u>PM-08WuOG16Ks4$ z6pO@3pMeaq;72i1u>;?{9DgzTO!JsMnK{&ATRx6L17yJ0&aB)NNB$kHI2A`B-X-vm zOTXCFWCOc~k;%O4R&h7v-h>mM7SRr-M2Q&U>trQ9ChUR63NjP5syipl`|a>%q#(?oEvPX38O zH(k>7Cj|Tu82h!5EbYHEpPLQX3c+iBxT?wc5k2%MRdS$z5RNOpaJg%Eo{N-x;<48q zX5NDuNQE3o70%C*VLmLn$t#)q1Q>Fr7Dx(KeZ)nVFdET&1&yzHsc@ku(FRQlWIg#0 z^u@FB^Ock>J3#g@ix-tHzwC*!?(1bL(g^$4B1Vf8ax()_yjbGvAjo&ASxgT|!>|=T z?BBvZ<>L0|2W?2%AgRZ7@D)7Gn%K*&^W!voN3h}H6Em3#p7L;AIstXO;PTUIEc~Pm ze=e|>X52NCo%n$s4k{3`pGOIDnkfq*Zp&0c-`4x=A`={D=(!tp{e8Ck)KjjbC~59h zEdzl-@@yuLG+foq;Jg1VEUV`g6dlmJ+ao4zv(cg{&9UBHKBLiiG_T~by1d%WKyfd4 z?0MJ5|GNoqL`_Y)yyYd@8q+49u+yXgH#0Jv;p+1JqF7uK4m~xni%0H#vj3xG<&g^B zf7h%J1_@2MaehJ z-k@ZFmg$~Akb(c(GUP5&PWn|ax5r=QIB^J~)~Si6Ew5X>`ewbDW7oGpg4WW{>}(ZF zU;j?c=1$Hj=P|queDI%@sZ6vy_Z%P&2fy<7x-L&o;Hs4d_$`e$bi)TffS_DcPU%OS z(>09aOze;qSBhtky^nfr=S@r}3DR#N9XuMFA)&Alix3qsC%o1vNY1R+WmEu=JPbM8xeHJ%`I6H-6TM~g#81{d?xg#A zL%S#7a_j5HHRKm(r9-A5|SjsQd$J>SIqV5A(6)O)@3d12u$DJ zYUH!ur3^OcO)o-5hu^HPakAo!>ENJEn7C*5BbT%cq`V(}Pi;lc&{L3=#HJLok*T1;yNRj+y$Gz0d7s&5SXW|Y0%lj^s&eXWF%-AI$vnzgXXr3cA;{YrKL2jKYC@20_O>-J9Uo8s3wo|jI4XJ&;;kT1Aq~IHqY@_XVw_T++r1|*3SFNAj?R^ws)NArel_fAzlp5K58d1^x92D5bO<}T^Q>}!yw#Dy~nWHH{{@P zR`lF?X2J$cWIeY3#;y=9xxdgm%fI?x;O4LfL9{Vg_^sXaP9s!7q!hp1$`FGXjsWP$ zzy)14{tawue+O?gtiDb|-En0M8kT9tM__-2$EIhYeJaT9$J3P%#?~F3>?rr4(s6IR ze(J+wJNm!3rx-;up2)tBQS;E15@-F?CC9Wm@JvOk@$b3-O8`LEmK0D_i@5gN9T)&@ z2NLZKhSm>-zXBN@E77D~i~UN>hl5nx7kH=Fls{hRDSLwEhJxe_e1$|?kDXh8Hq_cveK56=rbR6901 zQ!eBFwriYa7Ul7DLo-+dDb_Cxuf;K_ zjvp#Be)B*zPxnJ(6g|vGICSxhr*DW|esXLy0R!pX!^e~-$mL1}% zi|GFARTov+-2WLM3BQPQ>RTFAj`%lXm#jtI$lz)%EBsm3CzaOXx|9;kXQy24=8}>y zAGCAsq;a%7?S!}D4E2e8wDYj+02_dsH#yX}9&nGXxXa49)G$b1e)4ReyNF%&RnwFGh77<#@{x`m ze!)^GMtJ{CvzDhxH_gdB^CFavhb8Vi;sPMkPN=}0ug>+7&P2qXka3t1UeztMLZp1) zS$(+uLC5?z%76ETsiAikBm6rN$O2X#hLPp4XRIZ8hSHTybj8gW5+^m$k|{E0rYzU_lBayPZ~RT=He}0HG5JI4 zDg5SJHau59UesKV^HYVd-EEbYYvO+w#F^yVZ@<=w5ofXYhV;&9dAU)_@;R$=^3Ra< zD8$ZpS*@6un6CRqx;%|AevR6#0GV@KDLqdwcBYEWM#mr8|Ar=a`a~qy+pu8`{apE# zvb6Av=RQ>e-Qx?bl$*QhdlyZr19#DYMb{mq|(!%fv*}(@3t_(P49FdQ{{(gu3e1>$i@4&#iD=5e20!hMS zVBd|9y@O*cfNp)B>j$Epcj#)SVu`DvuJ^&&hU>4Nas5ld2Kpy(Q^TGT9xu$lny($`_x{-?B9gC($< z5P#;+^3UJp2>Ad;_iF71zNipFJ8)dLG9Y|<=KbnSy|tz0XQ8Pv{U5>%@vL^9fOg!S zg2Zdw|5hx@nEIiF&(`Hl?8*xH8MnExcrA|{ z%kMQktqd422EX~Vd@5t{oHTuu)$un^OO`oeD{XRo1i5+B*LQg`>v*i5i#y>|v9fM9 z{ET_(Ph0fEvDh10`Tvf0iUWauW$C-w{pCm}5zq;+Hfo=ru!cc!InJy-K{3MoMn0|6 zhbHV5GMDP!iT8C?i(~Ug*-&Jlix>fdh0oEF4gzP|%@V%pdmKt5F30EsaDkohmX?Ft zvv!a1vwFHuZwpl_I|bqF8Ro?3zWw zm{DD=1u`ZUMr9IDGoSDbiaXf-<^7oY=vjzChlU#H+>%44Fv}6s3X-n32SWZYlsOR$v9%ft?E7G2A{~XH z5-x<8|GX-_hT}KBJV-P5o?M<>nA%pjc4JYk=Z)X71K4prgy#Pe{sYF6asIdNh)#Kj zK}E@hqq_7g?kOKYu-!_uc5WsWf`*mE`UaCuWBpa3>yw>{xL?jmQ@_8@bozEEOhzn9 z1`LZE{GbyzH+5^R`W@76qmKSbH)10aj{9YunM7=zubd>tNPiS8K0n@7XsVCYu$Fl=8GE)Ce+8k3%KptC z7rBi`+QR&|tT~NV+nxQd)5Pf`0(PA=47|*fFI{L5Vk$)8a@VBjtj(j*%-&Lu-(p(* zj;#em9y+FWD%$t7lTwQtT&VwBRe%9;&xe&;KTKQIATEc=u(7h`=;9FxN6oFE#PUXy#BY%9=x41~z6Enwmn~sx z>Q=B{Y-cp^|7N?&Fh76Q+8V(di=MoLwyh5Rh%CCu`}5Q>YmwEjib?GZYs81QTs68J?-djNnf%+s^uasr_vxT{z?S1d z#dFbrFaB)1B*pk+PR?~{odbVP@v6^Zuvk%g{MN0$YT>Qaq>4(%L%$Dd%=J1P|MJiJwd?PF-&L{)*q=Q-B|QHg+k<4kZ6^3po4Ne0Yii{l zkg`vpX!_+-+;Mh-hvU_Mz1sNqyWEs}BEWflwo|Lj>|I;7`(FC+Ug-fxgZJ^DS9zHI zGh2?|S@Q4uiG9EOPs<8I6Zy^Kle+UF7U_Jw&a+GIfZLp%dlZa6NT>bs%6_|dy~p`Z z1_d8w;5yB9sTYzb8~i)I`}O>!2bsMa?@r$OZ~MM~2WQ-2h17A57Udc{eV_-XM0`rvc++C6`_A>1s?We{5*Jz%9A>y{(`RA2I z-`}lW_GJsSnZwZpOsG6TQN_s)=hJ30h|E3uLB90*d~kvkw7(W|R8+M7fyBvOcgo z`$0bQ`u;wL+XugNFS}L!YRPw4IurnoG^PnY1@=H#jV?PK*2!hn ze%*I{>2|o%BI)TRjRkHIfB6^GPH5C?;1iy~`KQz@_V#T4+-%^oTA0EawjJyZ23kNv z>zhSo8NR4TXT5(nQ4Sh^ERHkg{;6*){-&|^zl}C { + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 1.c: Setup a useState state colocation variable called selectedPokemonTypes, setSelectedPokemonTypes which will have a default of [] + + // โœ๐Ÿป This is already done for you. Feel free to have a look how it works in shared/hooks/usePokedex + const { data, isLoading } = usePokedex({ + path: 'types', + queryParams: 'pageSize=8' + }); + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 1.g: now we want to validate whether the selectedPokemonTypes have 4 items in the array before we call onPokemonTypesUpdate(selectedPokemonTypes). + + // Once completed, head over to Screen.tsx as the Form component will be complaining about a missing prop. + }; + + const onPokemonTypeSelection = (type: string) => { + // ๐Ÿ’ฃ We can get rid of this line once we start using the type param. + console.log(type); + // ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป 1.e: we need to check IF the selectedPokemonTypes already has the selectedType + // because we need to toggle it on and off. If it is selected, we just setSelectedPokemonTypes with the filtered out type + // if it's not in there then we set the type [...selectedPokemonTypes, type]; + }; + + return ( +