From 90ddbc6fc24f48419cbe81ab0b3b0b6a4e74066c Mon Sep 17 00:00:00 2001 From: HM Nayem Date: Sun, 16 Feb 2025 00:01:30 +0600 Subject: [PATCH 1/5] lecture 1 completed --- index.html | 6 +- package.json | 4 + pnpm-lock.yaml | 411 +++++++++++++++++++++++--- src/App.css | 42 --- src/App.tsx | 110 ++++--- src/App_lecture1.tsx | 70 +++++ src/assets/react.svg | 1 - src/demo/hoc/example.tsx | 67 +++++ src/demo/hoc/withDataFetch.tsx | 48 +++ src/examples/EX1_SRP.tsx | 152 ++++++++++ src/examples/EX2_SOC.tsx | 56 ++++ src/examples/EX3_ProjectDashboard.tsx | 271 +++++++++++++++++ src/examples/EX4_HOC.tsx | 45 +++ src/examples/EX5_RP1.tsx | 21 ++ src/examples/EX6_RP2.tsx | 34 +++ src/examples/EX7_RP3.tsx | 32 ++ src/examples/EX8.RP4.tsx | 51 ++++ src/index.css | 78 +---- src/main.tsx | 2 +- vite.config.ts | 2 + 20 files changed, 1317 insertions(+), 186 deletions(-) delete mode 100644 src/App.css create mode 100644 src/App_lecture1.tsx delete mode 100644 src/assets/react.svg create mode 100644 src/demo/hoc/example.tsx create mode 100644 src/demo/hoc/withDataFetch.tsx create mode 100644 src/examples/EX1_SRP.tsx create mode 100644 src/examples/EX2_SOC.tsx create mode 100644 src/examples/EX3_ProjectDashboard.tsx create mode 100644 src/examples/EX4_HOC.tsx create mode 100644 src/examples/EX5_RP1.tsx create mode 100644 src/examples/EX6_RP2.tsx create mode 100644 src/examples/EX7_RP3.tsx create mode 100644 src/examples/EX8.RP4.tsx diff --git a/index.html b/index.html index e4b78ea..6693102 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,11 @@ - Vite + React + TS + + React Design Patterns + + +
diff --git a/package.json b/package.json index 83ccf11..e344965 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@tailwindcss/vite": "^4.0.6", "react": "^19.0.0", "react-dom": "^19.0.0" }, @@ -19,12 +20,15 @@ "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", "babel-plugin-react-compiler": "19.0.0-beta-30d8a17-20250209", "eslint": "^9.19.0", "eslint-plugin-react-compiler": "19.0.0-beta-30d8a17-20250209", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.18", "globals": "^15.14.0", + "postcss": "^8.5.2", + "tailwindcss": "^4.0.6", "typescript": "~5.7.2", "typescript-eslint": "^8.22.0", "vite": "^6.1.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24289dc..ced0499 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@tailwindcss/vite': + specifier: ^4.0.6 + version: 4.0.6(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)) react: specifier: ^19.0.0 version: 19.0.0 @@ -29,34 +32,43 @@ importers: version: 19.0.3(@types/react@19.0.8) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.3.4(vite@6.1.0(@types/node@22.13.4)) + version: 4.3.4(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)) + autoprefixer: + specifier: ^10.4.20 + version: 10.4.20(postcss@8.5.2) babel-plugin-react-compiler: specifier: 19.0.0-beta-30d8a17-20250209 version: 19.0.0-beta-30d8a17-20250209 eslint: specifier: ^9.19.0 - version: 9.20.1 + version: 9.20.1(jiti@2.4.2) eslint-plugin-react-compiler: specifier: 19.0.0-beta-30d8a17-20250209 - version: 19.0.0-beta-30d8a17-20250209(eslint@9.20.1) + version: 19.0.0-beta-30d8a17-20250209(eslint@9.20.1(jiti@2.4.2)) eslint-plugin-react-hooks: specifier: ^5.0.0 - version: 5.1.0(eslint@9.20.1) + version: 5.1.0(eslint@9.20.1(jiti@2.4.2)) eslint-plugin-react-refresh: specifier: ^0.4.18 - version: 0.4.19(eslint@9.20.1) + version: 0.4.19(eslint@9.20.1(jiti@2.4.2)) globals: specifier: ^15.14.0 version: 15.15.0 + postcss: + specifier: ^8.5.2 + version: 8.5.2 + tailwindcss: + specifier: ^4.0.6 + version: 4.0.6 typescript: specifier: ~5.7.2 version: 5.7.3 typescript-eslint: specifier: ^8.22.0 - version: 8.24.0(eslint@9.20.1)(typescript@5.7.3) + version: 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) vite: specifier: ^6.1.0 - version: 6.1.0(@types/node@22.13.4) + version: 6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1) packages: @@ -511,6 +523,84 @@ packages: cpu: [x64] os: [win32] + '@tailwindcss/node@4.0.6': + resolution: {integrity: sha512-jb6E0WeSq7OQbVYcIJ6LxnZTeC4HjMvbzFBMCrQff4R50HBlo/obmYNk6V2GCUXDeqiXtvtrQgcIbT+/boB03Q==} + + '@tailwindcss/oxide-android-arm64@4.0.6': + resolution: {integrity: sha512-xDbym6bDPW3D2XqQqX3PjqW3CKGe1KXH7Fdkc60sX5ZLVUbzPkFeunQaoP+BuYlLc2cC1FoClrIRYnRzof9Sow==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.0.6': + resolution: {integrity: sha512-1f71/ju/tvyGl5c2bDkchZHy8p8EK/tDHCxlpYJ1hGNvsYihZNurxVpZ0DefpN7cNc9RTT8DjrRoV8xXZKKRjg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.0.6': + resolution: {integrity: sha512-s/hg/ZPgxFIrGMb0kqyeaqZt505P891buUkSezmrDY6lxv2ixIELAlOcUVTkVh245SeaeEiUVUPiUN37cwoL2g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.0.6': + resolution: {integrity: sha512-Z3Wo8FWZnmio8+xlcbb7JUo/hqRMSmhQw8IGIRoRJ7GmLR0C+25Wq+bEX/135xe/yEle2lFkhu9JBHd4wZYiig==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.6': + resolution: {integrity: sha512-SNSwkkim1myAgmnbHs4EjXsPL7rQbVGtjcok5EaIzkHkCAVK9QBQsWeP2Jm2/JJhq4wdx8tZB9Y7psMzHYWCkA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.0.6': + resolution: {integrity: sha512-tJ+mevtSDMQhKlwCCuhsFEFg058kBiSy4TkoeBG921EfrHKmexOaCyFKYhVXy4JtkaeeOcjJnCLasEeqml4i+Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.0.6': + resolution: {integrity: sha512-IoArz1vfuTR4rALXMUXI/GWWfx2EaO4gFNtBNkDNOYhlTD4NVEwE45nbBoojYiTulajI4c2XH8UmVEVJTOJKxA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.0.6': + resolution: {integrity: sha512-QtsUfLkEAeWAC3Owx9Kg+7JdzE+k9drPhwTAXbXugYB9RZUnEWWx5x3q/au6TvUYcL+n0RBqDEO2gucZRvRFgQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.0.6': + resolution: {integrity: sha512-QthvJqIji2KlGNwLcK/PPYo7w1Wsi/8NK0wAtRGbv4eOPdZHkQ9KUk+oCoP20oPO7i2a6X1aBAFQEL7i08nNMA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-win32-arm64-msvc@4.0.6': + resolution: {integrity: sha512-+oka+dYX8jy9iP00DJ9Y100XsqvbqR5s0yfMZJuPR1H/lDVtDfsZiSix1UFBQ3X1HWxoEEl6iXNJHWd56TocVw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.0.6': + resolution: {integrity: sha512-+o+juAkik4p8Ue/0LiflQXPmVatl6Av3LEZXpBTfg4qkMIbZdhCGWFzHdt2NjoMiLOJCFDddoV6GYaimvK1Olw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.0.6': + resolution: {integrity: sha512-lVyKV2y58UE9CeKVcYykULe9QaE1dtKdxDEdrTPIdbzRgBk6bdxHNAoDqvcqXbIGXubn3VOl1O/CFF77v/EqSA==} + engines: {node: '>= 10'} + + '@tailwindcss/vite@4.0.6': + resolution: {integrity: sha512-O25vZ/URWbZ2JHdk2o8wH7jOKqEGCsYmX3GwGmYS5DjE4X3mpf93a72Rn7VRnefldNauBzr5z2hfZptmBNtTUQ==} + peerDependencies: + vite: ^5.2.0 || ^6 + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -613,6 +703,13 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + autoprefixer@10.4.20: + resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + babel-plugin-react-compiler@19.0.0-beta-30d8a17-20250209: resolution: {integrity: sha512-0pQHlz5nmBiEQ8ZWWVLeaBzz/FkToAdXEXBBnd21uSrDtIzhSe+s3VMvqMsv6vYHNTr+0KmsvVfEqXQp0W0kzg==} @@ -677,9 +774,18 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + electron-to-chromium@1.5.101: resolution: {integrity: sha512-L0ISiQrP/56Acgu4/i/kfPwWSgrzYZUnQrC0+QPFuhqlLP1Ir7qzPPDVS9BcKIyWTRU8+o6CC8dKw38tSWhYIA==} + enhanced-resolve@5.18.1: + resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + engines: {node: '>=10.13.0'} + esbuild@0.24.2: resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} engines: {node: '>=18'} @@ -787,6 +893,9 @@ packages: flatted@3.3.2: resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -816,6 +925,9 @@ packages: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} @@ -856,6 +968,10 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -889,6 +1005,70 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lightningcss-darwin-arm64@1.29.1: + resolution: {integrity: sha512-HtR5XJ5A0lvCqYAoSv2QdZZyoHNttBpa5EP9aNuzBQeKGfbyH5+UipLWvVzpP4Uml5ej4BYs5I9Lco9u1fECqw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.29.1: + resolution: {integrity: sha512-k33G9IzKUpHy/J/3+9MCO4e+PzaFblsgBjSGlpAaFikeBFm8B/CkO3cKU9oI4g+fjS2KlkLM/Bza9K/aw8wsNA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.29.1: + resolution: {integrity: sha512-0SUW22fv/8kln2LnIdOCmSuXnxgxVC276W5KLTwoehiO0hxkacBxjHOL5EtHD8BAXg2BvuhsJPmVMasvby3LiQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.29.1: + resolution: {integrity: sha512-sD32pFvlR0kDlqsOZmYqH/68SqUMPNj+0pucGxToXZi4XZgZmqeX/NkxNKCPsswAXU3UeYgDSpGhu05eAufjDg==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.29.1: + resolution: {integrity: sha512-0+vClRIZ6mmJl/dxGuRsE197o1HDEeeRk6nzycSy2GofC2JsY4ifCRnvUWf/CUBQmlrvMzt6SMQNMSEu22csWQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.29.1: + resolution: {integrity: sha512-UKMFrG4rL/uHNgelBsDwJcBqVpzNJbzsKkbI3Ja5fg00sgQnHw/VrzUTEc4jhZ+AN2BvQYz/tkHu4vt1kLuJyw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.29.1: + resolution: {integrity: sha512-u1S+xdODy/eEtjADqirA774y3jLcm8RPtYztwReEXoZKdzgsHYPl0s5V52Tst+GKzqjebkULT86XMSxejzfISw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.29.1: + resolution: {integrity: sha512-L0Tx0DtaNUTzXv0lbGCLB/c/qEADanHbu4QdcNOXLIe1i8i22rZRpbT3gpWYsCh9aSL9zFujY/WmEXIatWvXbw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.29.1: + resolution: {integrity: sha512-QoOVnkIEFfbW4xPi+dpdft/zAKmgLgsRHfJalEPYuJDOWf7cLQzYg0DEh8/sn737FaeMJxHZRc1oBreiwZCjog==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.29.1: + resolution: {integrity: sha512-NygcbThNBe4JElP+olyTI/doBNGJvLs3bFCRPdvuCcxZCcCZ71B858IHpdm7L1btZex0FvCmM17FK98Y9MRy1Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.29.1: + resolution: {integrity: sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q==} + engines: {node: '>= 12.0.0'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -928,6 +1108,10 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -959,6 +1143,9 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + postcss@8.5.2: resolution: {integrity: sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==} engines: {node: ^10 || ^12 || >=14} @@ -1035,6 +1222,13 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + tailwindcss@4.0.6: + resolution: {integrity: sha512-mysewHYJKaXgNOW6pp5xon/emCsfAMnO8WMaGKZZ35fomnR/T5gYnRg2/yRTTrtXiEl1tiVkeRt0eMO6HxEZqw==} + + tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -1382,9 +1576,9 @@ snapshots: '@esbuild/win32-x64@0.24.2': optional: true - '@eslint-community/eslint-utils@4.4.1(eslint@9.20.1)': + '@eslint-community/eslint-utils@4.4.1(eslint@9.20.1(jiti@2.4.2))': dependencies: - eslint: 9.20.1 + eslint: 9.20.1(jiti@2.4.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -1527,6 +1721,67 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.34.7': optional: true + '@tailwindcss/node@4.0.6': + dependencies: + enhanced-resolve: 5.18.1 + jiti: 2.4.2 + tailwindcss: 4.0.6 + + '@tailwindcss/oxide-android-arm64@4.0.6': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.0.6': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.0.6': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.0.6': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.6': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.0.6': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.0.6': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.0.6': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.0.6': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.0.6': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.0.6': + optional: true + + '@tailwindcss/oxide@4.0.6': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.0.6 + '@tailwindcss/oxide-darwin-arm64': 4.0.6 + '@tailwindcss/oxide-darwin-x64': 4.0.6 + '@tailwindcss/oxide-freebsd-x64': 4.0.6 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.0.6 + '@tailwindcss/oxide-linux-arm64-gnu': 4.0.6 + '@tailwindcss/oxide-linux-arm64-musl': 4.0.6 + '@tailwindcss/oxide-linux-x64-gnu': 4.0.6 + '@tailwindcss/oxide-linux-x64-musl': 4.0.6 + '@tailwindcss/oxide-win32-arm64-msvc': 4.0.6 + '@tailwindcss/oxide-win32-x64-msvc': 4.0.6 + + '@tailwindcss/vite@4.0.6(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1))': + dependencies: + '@tailwindcss/node': 4.0.6 + '@tailwindcss/oxide': 4.0.6 + lightningcss: 1.29.1 + tailwindcss: 4.0.6 + vite: 6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1) + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.26.9 @@ -1564,15 +1819,15 @@ snapshots: dependencies: csstype: 3.1.3 - '@typescript-eslint/eslint-plugin@8.24.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1)(typescript@5.7.3))(eslint@9.20.1)(typescript@5.7.3)': + '@typescript-eslint/eslint-plugin@8.24.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.24.0(eslint@9.20.1)(typescript@5.7.3) + '@typescript-eslint/parser': 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) '@typescript-eslint/scope-manager': 8.24.0 - '@typescript-eslint/type-utils': 8.24.0(eslint@9.20.1)(typescript@5.7.3) - '@typescript-eslint/utils': 8.24.0(eslint@9.20.1)(typescript@5.7.3) + '@typescript-eslint/type-utils': 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/utils': 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) '@typescript-eslint/visitor-keys': 8.24.0 - eslint: 9.20.1 + eslint: 9.20.1(jiti@2.4.2) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -1581,14 +1836,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.24.0(eslint@9.20.1)(typescript@5.7.3)': + '@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)': dependencies: '@typescript-eslint/scope-manager': 8.24.0 '@typescript-eslint/types': 8.24.0 '@typescript-eslint/typescript-estree': 8.24.0(typescript@5.7.3) '@typescript-eslint/visitor-keys': 8.24.0 debug: 4.4.0 - eslint: 9.20.1 + eslint: 9.20.1(jiti@2.4.2) typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -1598,12 +1853,12 @@ snapshots: '@typescript-eslint/types': 8.24.0 '@typescript-eslint/visitor-keys': 8.24.0 - '@typescript-eslint/type-utils@8.24.0(eslint@9.20.1)(typescript@5.7.3)': + '@typescript-eslint/type-utils@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)': dependencies: '@typescript-eslint/typescript-estree': 8.24.0(typescript@5.7.3) - '@typescript-eslint/utils': 8.24.0(eslint@9.20.1)(typescript@5.7.3) + '@typescript-eslint/utils': 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) debug: 4.4.0 - eslint: 9.20.1 + eslint: 9.20.1(jiti@2.4.2) ts-api-utils: 2.0.1(typescript@5.7.3) typescript: 5.7.3 transitivePeerDependencies: @@ -1625,13 +1880,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.24.0(eslint@9.20.1)(typescript@5.7.3)': + '@typescript-eslint/utils@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1(jiti@2.4.2)) '@typescript-eslint/scope-manager': 8.24.0 '@typescript-eslint/types': 8.24.0 '@typescript-eslint/typescript-estree': 8.24.0(typescript@5.7.3) - eslint: 9.20.1 + eslint: 9.20.1(jiti@2.4.2) typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -1641,14 +1896,14 @@ snapshots: '@typescript-eslint/types': 8.24.0 eslint-visitor-keys: 4.2.0 - '@vitejs/plugin-react@4.3.4(vite@6.1.0(@types/node@22.13.4))': + '@vitejs/plugin-react@4.3.4(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1))': dependencies: '@babel/core': 7.26.9 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.9) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.9) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 6.1.0(@types/node@22.13.4) + vite: 6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1) transitivePeerDependencies: - supports-color @@ -1671,6 +1926,16 @@ snapshots: argparse@2.0.1: {} + autoprefixer@10.4.20(postcss@8.5.2): + dependencies: + browserslist: 4.24.4 + caniuse-lite: 1.0.30001699 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.2 + postcss-value-parser: 4.2.0 + babel-plugin-react-compiler@19.0.0-beta-30d8a17-20250209: dependencies: '@babel/types': 7.26.9 @@ -1730,8 +1995,15 @@ snapshots: deep-is@0.1.4: {} + detect-libc@1.0.3: {} + electron-to-chromium@1.5.101: {} + enhanced-resolve@5.18.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + esbuild@0.24.2: optionalDependencies: '@esbuild/aix-ppc64': 0.24.2 @@ -1764,25 +2036,25 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-plugin-react-compiler@19.0.0-beta-30d8a17-20250209(eslint@9.20.1): + eslint-plugin-react-compiler@19.0.0-beta-30d8a17-20250209(eslint@9.20.1(jiti@2.4.2)): dependencies: '@babel/core': 7.26.9 '@babel/parser': 7.26.9 '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.26.9) - eslint: 9.20.1 + eslint: 9.20.1(jiti@2.4.2) hermes-parser: 0.25.1 zod: 3.24.2 zod-validation-error: 3.4.0(zod@3.24.2) transitivePeerDependencies: - supports-color - eslint-plugin-react-hooks@5.1.0(eslint@9.20.1): + eslint-plugin-react-hooks@5.1.0(eslint@9.20.1(jiti@2.4.2)): dependencies: - eslint: 9.20.1 + eslint: 9.20.1(jiti@2.4.2) - eslint-plugin-react-refresh@0.4.19(eslint@9.20.1): + eslint-plugin-react-refresh@0.4.19(eslint@9.20.1(jiti@2.4.2)): dependencies: - eslint: 9.20.1 + eslint: 9.20.1(jiti@2.4.2) eslint-scope@8.2.0: dependencies: @@ -1793,9 +2065,9 @@ snapshots: eslint-visitor-keys@4.2.0: {} - eslint@9.20.1: + eslint@9.20.1(jiti@2.4.2): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1(jiti@2.4.2)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.19.2 '@eslint/core': 0.11.0 @@ -1829,6 +2101,8 @@ snapshots: minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.4 + optionalDependencies: + jiti: 2.4.2 transitivePeerDependencies: - supports-color @@ -1888,6 +2162,8 @@ snapshots: flatted@3.3.2: {} + fraction.js@4.3.7: {} + fsevents@2.3.3: optional: true @@ -1907,6 +2183,8 @@ snapshots: globals@15.15.0: {} + graceful-fs@4.2.11: {} + graphemer@1.4.0: {} has-flag@4.0.0: {} @@ -1936,6 +2214,8 @@ snapshots: isexe@2.0.0: {} + jiti@2.4.2: {} + js-tokens@4.0.0: {} js-yaml@4.1.0: @@ -1961,6 +2241,51 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lightningcss-darwin-arm64@1.29.1: + optional: true + + lightningcss-darwin-x64@1.29.1: + optional: true + + lightningcss-freebsd-x64@1.29.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.29.1: + optional: true + + lightningcss-linux-arm64-gnu@1.29.1: + optional: true + + lightningcss-linux-arm64-musl@1.29.1: + optional: true + + lightningcss-linux-x64-gnu@1.29.1: + optional: true + + lightningcss-linux-x64-musl@1.29.1: + optional: true + + lightningcss-win32-arm64-msvc@1.29.1: + optional: true + + lightningcss-win32-x64-msvc@1.29.1: + optional: true + + lightningcss@1.29.1: + dependencies: + detect-libc: 1.0.3 + optionalDependencies: + lightningcss-darwin-arm64: 1.29.1 + lightningcss-darwin-x64: 1.29.1 + lightningcss-freebsd-x64: 1.29.1 + lightningcss-linux-arm-gnueabihf: 1.29.1 + lightningcss-linux-arm64-gnu: 1.29.1 + lightningcss-linux-arm64-musl: 1.29.1 + lightningcss-linux-x64-gnu: 1.29.1 + lightningcss-linux-x64-musl: 1.29.1 + lightningcss-win32-arm64-msvc: 1.29.1 + lightningcss-win32-x64-msvc: 1.29.1 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -1994,6 +2319,8 @@ snapshots: node-releases@2.0.19: {} + normalize-range@0.1.2: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -2023,6 +2350,8 @@ snapshots: picomatch@2.3.1: {} + postcss-value-parser@4.2.0: {} + postcss@8.5.2: dependencies: nanoid: 3.3.8 @@ -2097,6 +2426,10 @@ snapshots: dependencies: has-flag: 4.0.0 + tailwindcss@4.0.6: {} + + tapable@2.2.1: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -2109,12 +2442,12 @@ snapshots: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.24.0(eslint@9.20.1)(typescript@5.7.3): + typescript-eslint@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.24.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1)(typescript@5.7.3))(eslint@9.20.1)(typescript@5.7.3) - '@typescript-eslint/parser': 8.24.0(eslint@9.20.1)(typescript@5.7.3) - '@typescript-eslint/utils': 8.24.0(eslint@9.20.1)(typescript@5.7.3) - eslint: 9.20.1 + '@typescript-eslint/eslint-plugin': 8.24.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/parser': 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/utils': 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) + eslint: 9.20.1(jiti@2.4.2) typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -2133,7 +2466,7 @@ snapshots: dependencies: punycode: 2.3.1 - vite@6.1.0(@types/node@22.13.4): + vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1): dependencies: esbuild: 0.24.2 postcss: 8.5.2 @@ -2141,6 +2474,8 @@ snapshots: optionalDependencies: '@types/node': 22.13.4 fsevents: 2.3.3 + jiti: 2.4.2 + lightningcss: 1.29.1 which@2.0.2: dependencies: diff --git a/src/App.css b/src/App.css deleted file mode 100644 index b9d355d..0000000 --- a/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/src/App.tsx b/src/App.tsx index 3d7ded3..d1955bb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,35 +1,75 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' - -function App() { - const [count, setCount] = useState(0) - - return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- - ) -} - -export default App +import DragAndDrop from './examples/EX8.RP4'; +import { useState } from 'react'; + +export default function App() { + const items = ['Item 1', 'Item 2', 'Item 3', 'Item 4']; + const [draggedItem, setDraggedItem] = useState(null); + + const dropZoneConfig = { + highlightOnHover: true, + maxItems: 3, // Allow up to 3 items to be dropped + }; + + return ( +
+ ( +
+
+

Draggable Items

+ {items.map((item) => ( +
{ + onDragStart(e, item); + setDraggedItem(item); + }} + style={{ + padding: '10px', + marginBottom: '10px', + backgroundColor: '#f9f9f9', + border: '1px solid #ddd', + cursor: 'move', + }} + > + {item} +
+ ))} +
+ +
{ + onDrop(e); + setDraggedItem(null); + }} + onDragOver={(e) => e.preventDefault()} + style={{ + width: '45%', + padding: '20px', + border: `2px solid ${dropZoneActive ? '#4CAF50' : '#ccc'}`, + backgroundColor: dropZoneActive ? '#eaf8e3' : '#f9f9f9', + transition: + 'background-color 0.3s ease, border-color 0.3s ease', + }} + > +

Drop Zone

+

{dropZoneActive ? 'Release to drop' : 'Drag items here'}

+
+ {dropZoneActive &&

Item being dragged: {draggedItem}

} +
+
+
+ )} + /> +
+ ); +} diff --git a/src/App_lecture1.tsx b/src/App_lecture1.tsx new file mode 100644 index 0000000..c59ef0a --- /dev/null +++ b/src/App_lecture1.tsx @@ -0,0 +1,70 @@ +import { MouseTracker } from './examples/EX5_RP1'; +import { DisableContextMenu } from './examples/EX6_RP2'; +import { ListRenderer } from './examples/EX7_RP3'; + +export default function App() { + return ( +
+
+ ( +
+

Mouse Tracker

+

X: {x}

+

Y: {y}

+
+ )} + /> + + ( +
+
+

X: {x}

+
+
+

Y: {y}

+
+
+ )} + /> +
+ +
+
+ ( +
+

Disable Context Menu

+

Right click on the div below to see the difference

+
+ )} + /> +
+ +
+
+
+ +
+
+ ( +
+ {item} +
+ )} + /> +
+
+
+ ); +} diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/demo/hoc/example.tsx b/src/demo/hoc/example.tsx new file mode 100644 index 0000000..5a2e021 --- /dev/null +++ b/src/demo/hoc/example.tsx @@ -0,0 +1,67 @@ +import { withDataFetch } from './withDataFetch'; + +type BaseResponse = { + data: unknown; + loading: boolean; + error: string | null; +}; + +type User = { + id: number; + name: string; + username: string; + email: string; +}; + +export const Users = ({ data, loading, error }: BaseResponse) => { + const users = data as User[]; + + return ( +
+ {loading &&

Loading...

} + {error &&

Error: {error}

} +

Users

+ {users && ( +
    + {users.slice(0, 5).map((user) => ( +
  • {user.name}
  • + ))} +
+ )} +
+ ); +}; + +export const UsersWithData = withDataFetch( + Users, + 'https://jsonplaceholder.typicode.com/users' +); + +type Post = { + id: number; + title: string; +}; + +const Posts = ({ data, loading, error }: BaseResponse) => { + const posts = data as Post[]; + + return ( +
+ {loading &&

Loading...

} + {error &&

Error: {error}

} +

Posts

+ {posts && ( +
    + {posts.slice(0, 5).map((post) => ( +
  • {post.title}
  • + ))} +
+ )} +
+ ); +}; + +export const PostsWithData = withDataFetch( + Posts, + 'https://jsonplaceholder.typicode.com/posts' +); diff --git a/src/demo/hoc/withDataFetch.tsx b/src/demo/hoc/withDataFetch.tsx new file mode 100644 index 0000000..7b546b6 --- /dev/null +++ b/src/demo/hoc/withDataFetch.tsx @@ -0,0 +1,48 @@ +import { ComponentType, useEffect, useState } from 'react'; + +export const withDataFetch = (Component: ComponentType, url: string) => { + return (props: any) => { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const controller = new AbortController(); + setLoading(true); + + fetchData(url, controller.signal) + .then(({ data, error }) => { + if (error) { + setError(error); + return; + } + setData(data); + setError(null); + }) + .finally(() => setLoading(false)); + + return () => controller.abort(); + }, [url]); + + return ; + }; +}; + +const fetchData = async (url: string, signal: AbortSignal) => { + try { + const response = await fetch(url, { signal }); + + if (!response.ok) { + throw new Error('Failed to fetch data'); + } + + const result = await response.json(); + return { data: result, error: null }; + } catch (error) { + if (error instanceof Error) { + return { data: null, error: error.message }; + } else { + return { data: null, error: 'An unknown error occurred' }; + } + } +}; diff --git a/src/examples/EX1_SRP.tsx b/src/examples/EX1_SRP.tsx new file mode 100644 index 0000000..6002d51 --- /dev/null +++ b/src/examples/EX1_SRP.tsx @@ -0,0 +1,152 @@ +import { memo, useEffect, useMemo, useState } from 'react'; + +/** + * Bad Example: + * - The Dashboard component is responsible for displaying the user profile, notifications, and tasks. + * - This violates the Single Responsibility Principle (SRP) because the component is responsible for more than one thing. + */ + +export const DashboardBad = () => { + const [user, _setUser] = useState({ + name: 'John Doe', + email: 'john@example.com', + }); + const [notifications, _setNotifications] = useState([ + 'New message', + 'Server update', + ]); + const [tasks, _setTasks] = useState(['Finish report', 'Update project']); + + return ( +
+

Dashboard

+ + {/* User Profile */} +
+

User Profile

+

Name: {user.name}

+

Email: {user.email}

+
+ + {/* Notifications */} +
+

Notifications

+
    + {notifications.map((n, i) => ( +
  • {n}
  • + ))} +
+
+ + {/* Tasks */} +
+

Tasks

+
    + {tasks.map((t, i) => ( +
  • {t}
  • + ))} +
+
+
+ ); +}; + +/** + * Good Example + */ + +export const DashboardGood = () => { + const [notifications, _setNotifications] = useState([ + 'New message', + 'Server update', + ]); + const [tasks, _setTasks] = useState(['Finish report', 'Update project']); + + return ( +
+

Dashboard

+ {/* Memoize */} + {/* Memoize */} + {/* Memoize */} +
+ ); +}; + +type User = { + name: string; + email: string; +}; + +const UserProfile = () => { + const [user, _setUser] = useState({ + name: 'John Doe', + email: 'john@example.com', + }); + + useEffect(() => { + // DO API CALL + // Do Additional Logic + }, []); + + const computedUser = useMemo(() => { + // Do Additional Logic + return user; + }, [user]); + + const updateUser = (user: User) => { + // API CALL + // Do Additional Logic + _setUser(user); + }; + + return ; +}; + +const UserProfileContent = ({ + user, + onUpdate, +}: { + user: User; + onUpdate: (user: User) => void; +}) => { + return ( +
+

User Profile

+

Name: {user.name}

+

Email: {user.email}

+ +
+ ); +}; + +const Notifications = ({ notifications }: { notifications: string[] }) => { + return ( +
+

Notifications

+
    + {notifications.map((n, i) => ( +
  • {n}
  • + ))} +
+
+ ); +}; + +const Tasks = ({ tasks }: { tasks: string[] }) => { + return ( +
+

Tasks

+
    + {tasks.map((t, i) => ( +
  • {t}
  • + ))} +
+
+ ); +}; diff --git a/src/examples/EX2_SOC.tsx b/src/examples/EX2_SOC.tsx new file mode 100644 index 0000000..210d755 --- /dev/null +++ b/src/examples/EX2_SOC.tsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from 'react'; + +export const CheckoutBad = () => { + const [_user, setUser] = useState(null); + const [cart, setCart] = useState([]); + const [_promo, setPromo] = useState(null); + const [loading, setLoading] = useState(false); + + useEffect(() => { + fetchUser(); + fetchCart(); + }, []); + + const fetchUser = async () => { + const response = await fetch('/api/user'); + const data = await response.json(); + setUser(data); + }; + + const fetchCart = async () => { + const response = await fetch('/api/cart'); + const data = await response.json(); + setCart(data); + }; + + const applyPromoCode = async (code: string) => { + const response = await fetch(`/api/promo?code=${code}`); + const data = await response.json(); + setPromo(data); + }; + + const handlePayment = async () => { + setLoading(true); + applyPromoCode('code'); + const response = await fetch('/api/pay', { + method: 'POST', + body: JSON.stringify({ cart }), + }); + const data = await response.json(); + if (data.success) { + alert('Payment Successful!'); + } else { + alert('Payment Failed!'); + } + setLoading(false); + }; + + return ( +
+

Checkout

+ +
+ ); +}; diff --git a/src/examples/EX3_ProjectDashboard.tsx b/src/examples/EX3_ProjectDashboard.tsx new file mode 100644 index 0000000..dc3f0ed --- /dev/null +++ b/src/examples/EX3_ProjectDashboard.tsx @@ -0,0 +1,271 @@ +import { useState, useEffect } from 'react'; + +// 🚨 This component does too many things! +const ProjectDashboard = ({ projectId }: { projectId: string }) => { + const [project, setProject] = useState<{ + name: string; + description: string; + deadline: string; + team: { id: number; name: string; avatar: string }[]; + comments: { id: number; text: string }[]; + } | null>(null); + + const [team, setTeam] = useState< + { id: number; name: string; avatar: string }[] + >([]); + + const [comments, setComments] = useState<{ id: number; text: string }[]>([]); + const [newComment, setNewComment] = useState(''); + const [status, setStatus] = useState('In Progress'); + + // Fetch Project Details + useEffect(() => { + fetch(`/api/projects/${projectId}`) + .then((res) => res.json()) + .then((data) => { + setProject(data); + setTeam(data.team); + }) + .catch(() => console.log('Error loading project')); + }, [projectId]); + + // Fetch Comments + useEffect(() => { + fetch(`/api/projects/${projectId}/comments`) + .then((res) => res.json()) + .then((data) => setComments(data)) + .catch(() => console.log('Error loading comments')); + }, [projectId]); + + // Update Status + const updateStatus = (newStatus: string) => { + setStatus(newStatus); + console.log(`Project status updated to: ${newStatus}`); + }; + + // Handle Adding New Comment + const addComment = () => { + if (newComment.trim() === '') return; + setComments([...comments, { id: Date.now(), text: newComment }]); + setNewComment(''); + }; + + return ( +
+ {/* Project Info */} + {project ? ( +
+

{project.name}

+

{project.description}

+

Deadline: {new Date(project.deadline).toDateString()}

+

Status: {status}

+ +
+ ) : ( +

Loading...

+ )} + + {/* Team Members */} +

Team Members

+
    + {team.map((member) => ( +
  • + {member.name}{' '} + {member.name} +
  • + ))} +
+ + {/* Comments Section */} +

Comments

+
    + {comments.map((comment) => ( +
  • {comment.text}
  • + ))} +
+ setNewComment(e.target.value)} + /> + +
+ ); +}; + +export default ProjectDashboard; + +/** +❌ Violates Separation of Concerns (SoC) β†’ Handles data fetching, UI rendering, business logic, and state management all in one place. +❌ Breaks Single Responsibility Principle (SRP) β†’ Should a single component really be responsible for project details, team members, status updates, and comments? +❌ Tightly Coupled & Hard to Maintain β†’ If you want to change how the comments work, you'll have to touch unrelated parts of the code. +❌ Difficult to Test & Reuse β†’ You can't reuse the team member list or comments elsewhere without copying logic. +*/ + +/** + * βœ… Good Example + */ + +type Team = { + id: number; + name: string; + avatar: string; +}; + +type Comment = { + id: number; + text: string; +}; + +type Project = { + name: string; + description: string; + deadline: string; + team: Team[]; + comments: Comment[]; +}; + +export const ProjectDashboardGood = ({ projectId }: { projectId: string }) => { + // This is a container component + // So that, we can have states and logic for this component + + const [project, setProject] = useState(null); + const [team, setTeam] = useState([]); + const [comments, setComments] = useState([]); + const [status, setStatus] = useState('In Progress'); + + // Fetch Project Details + useEffect(() => { + fetchProject(projectId).then((project: Project) => { + setProject(project); + setTeam(project.team); + }); + }, [projectId]); + + // Fetch Comments + useEffect(() => { + fetchComments(projectId).then((comments: Comment[]) => { + setComments(comments); + }); + }, [projectId]); + + // Update Status + const updateStatus = (newStatus: string) => { + setStatus(newStatus); + mutateStatus(projectId, newStatus); + }; + + return ( +
+ {project ? ( + <> + + + + + + ) : ( +

Loading...

+ )} +
+ ); +}; + +const ProjectInfo = ({ + project, + status, +}: { + project: Project; + status: string; +}) => ( +
+

{project.name}

+

{project.description}

+

Deadline: {new Date(project.deadline).toDateString()}

+

Status: {status}

+
+); + +const StatusUpdater = ({ + onUpdate, +}: { + status: string; + onUpdate: (newStatus: string) => void; +}) => ( +
+ +
+); + +const TeamList = ({ team }: { team: Team[] }) => ( +
+

Team Members

+
    + {team.map((member) => ( +
  • + {member.name} {member.name} +
  • + ))} +
+
+); + +const CommentSection = ({ + comments, + setComments, +}: { + comments: Comment[]; + setComments: (comments: Comment[]) => void; +}) => { + const [newComment, setNewComment] = useState(''); + + const addComment = () => { + if (newComment.trim() === '') return; + setComments([...comments, { id: Date.now(), text: newComment }]); + setNewComment(''); + }; + + return ( +
+

Comments

+
    + {/** New Component */} + {comments.map((comment) => ( +
  • {comment.text}
  • + ))} +
+ + {/* New Component */} + setNewComment(e.target.value)} + /> + +
+ ); +}; + +/** + * Utility functions + */ + +const fetchProject = async (projectId: string) => { + const res = await fetch(`/api/projects/${projectId}`); + return res.json(); +}; + +const fetchComments = async (projectId: string) => { + const res = await fetch(`/api/projects/${projectId}/comments`); + return res.json(); +}; + +const mutateStatus = async (projectId: string, newStatus: string) => { + await fetch(`/api/projects/${projectId}/status`, { + method: 'PUT', + body: JSON.stringify({ status: newStatus }), + }); +}; diff --git a/src/examples/EX4_HOC.tsx b/src/examples/EX4_HOC.tsx new file mode 100644 index 0000000..ccae9b6 --- /dev/null +++ b/src/examples/EX4_HOC.tsx @@ -0,0 +1,45 @@ +import { ComponentType, useEffect, useState } from 'react'; + +export const withDataFetch = (Component: ComponentType, url: string) => { + return (props: any) => { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const controller = new AbortController(); + const signal = controller.signal; + + const fetchData = async () => { + try { + const response = await fetch(url, { signal }); + if (!response.ok) { + throw new Error('Failed to fetch data'); + } + const result = await response.json(); + setData(result); + } catch (err) { + if (err instanceof Error && err.name === 'AbortError') { + return; + } + setError( + err instanceof Error ? err.message : 'An unknown error occurred' + ); + } finally { + setLoading(false); + } + }; + + fetchData(); + + return () => { + controller.abort(); + }; + }, [url]); + + if (loading) return

Loading...

; + if (error) return

Error: {error}

; + + return ; + }; +}; diff --git a/src/examples/EX5_RP1.tsx b/src/examples/EX5_RP1.tsx new file mode 100644 index 0000000..9c3c0a6 --- /dev/null +++ b/src/examples/EX5_RP1.tsx @@ -0,0 +1,21 @@ +import { useEffect, useState } from 'react'; + +type Props = { + render: (x: number, y: number) => React.ReactNode; +}; + +export const MouseTracker = ({ render }: Props) => { + const [position, setPosition] = useState({ x: 0, y: 0 }); + + useEffect(() => { + const handleMouseMove = (event: MouseEvent) => { + setPosition({ x: event.clientX, y: event.clientY }); + }; + + window.addEventListener('mousemove', handleMouseMove); + + return () => window.removeEventListener('mousemove', handleMouseMove); + }, []); + + return render(position.x, position.y); +}; diff --git a/src/examples/EX6_RP2.tsx b/src/examples/EX6_RP2.tsx new file mode 100644 index 0000000..27b818d --- /dev/null +++ b/src/examples/EX6_RP2.tsx @@ -0,0 +1,34 @@ +import { JSX, useEffect, useRef } from 'react'; + +export const DisableContextMenu = ({ + render, + className = '', +}: { + render: () => JSX.Element; + className?: string; +}) => { + const ref = useRef(null); + + useEffect(() => { + const handleContextMenu = (event: MouseEvent) => { + event.preventDefault(); // Disable right-click context menu + }; + + // Only attach event listener to the referenced div element + const element = ref.current; + if (element) { + element.addEventListener('contextmenu', handleContextMenu); + + // Cleanup the event listener on unmount + return () => { + element.removeEventListener('contextmenu', handleContextMenu); + }; + } + }, []); + + return ( +
+ {render()} +
+ ); +}; diff --git a/src/examples/EX7_RP3.tsx b/src/examples/EX7_RP3.tsx new file mode 100644 index 0000000..13f45be --- /dev/null +++ b/src/examples/EX7_RP3.tsx @@ -0,0 +1,32 @@ +import { JSX } from 'react'; + +type Props = { + items: string[]; + render?: (item: string, index: number, total: number) => JSX.Element; +}; + +export const ListRenderer = ({ items, render }: Props) => { + if (!render && items.length === 0) { + return

No items to display

; + } + + if (!render) { + return ( +
    + {items.map((item) => ( +
  • + {item} +
  • + ))} +
+ ); + } + + return ( +
    + {items.map((item, index) => ( +
  • {render(item, index, items.length)}
  • + ))} +
+ ); +}; diff --git a/src/examples/EX8.RP4.tsx b/src/examples/EX8.RP4.tsx new file mode 100644 index 0000000..37d11a0 --- /dev/null +++ b/src/examples/EX8.RP4.tsx @@ -0,0 +1,51 @@ +import { JSX, useState } from 'react'; + +type DragAndDropProps = { + items: string[]; + dropZoneConfig: { + highlightOnHover?: boolean; + maxItems?: number; // Limit the number of items that can be dropped + }; + render: (params: { + items: string[]; + dropZoneActive: boolean; + onDragStart: (e: React.DragEvent, item: string) => void; + onDrop: (e: React.DragEvent) => void; + }) => JSX.Element; +}; + +const DragAndDrop: React.FC = ({ + items, + dropZoneConfig, + render, +}) => { + const [draggedItem, setDraggedItem] = useState(null); + const [droppedItems, setDroppedItems] = useState([]); + + const handleDragStart = (_e: React.DragEvent, item: string) => { + setDraggedItem(item); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + if ( + draggedItem && + !droppedItems.includes(draggedItem) && + droppedItems.length < dropZoneConfig.maxItems! + ) { + setDroppedItems((prev) => [...prev, draggedItem]); + setDraggedItem(null); + } + }; + + const dropZoneActive = draggedItem !== null; + + return render({ + items, + dropZoneActive, + onDragStart: handleDragStart, + onDrop: handleDrop, + }); +}; + +export default DragAndDrop; diff --git a/src/index.css b/src/index.css index 6119ad9..14325be 100644 --- a/src/index.css +++ b/src/index.css @@ -1,68 +1,10 @@ -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} +@import 'tailwindcss'; + +@theme { + --default-font-family: 'Poppins', sans-serif; + --default-font-size: 16px; + --default-font-weight: 400; + --default-line-height: 1.5; + --default-letter-spacing: 0.01em; + --default-text-color: #333; +} \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index bef5202..bb7941a 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,7 +1,7 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' -import App from './App.tsx' +import App from './App.tsx'; createRoot(document.getElementById('root')!).render( diff --git a/vite.config.ts b/vite.config.ts index 01b2051..3a6a3ea 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,7 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import path from 'path'; +import tailwindcss from '@tailwindcss/vite'; // https://vite.dev/config/ export default defineConfig({ @@ -10,6 +11,7 @@ export default defineConfig({ plugins: ['babel-plugin-react-compiler'], }, }), + tailwindcss(), ], resolve: { alias: { From 058525515be4694702556a82f76d11ea920c831a Mon Sep 17 00:00:00 2001 From: HM Nayem Date: Sun, 16 Feb 2025 03:05:41 +0600 Subject: [PATCH 2/5] chore: moved examples to the lecture 1 directory --- src/App.tsx | 2 +- src/App_lecture1.tsx | 6 +++--- src/examples/{ => lecture_1}/EX1_SRP.tsx | 0 src/examples/{ => lecture_1}/EX2_SOC.tsx | 0 src/examples/{ => lecture_1}/EX3_ProjectDashboard.tsx | 0 src/examples/{ => lecture_1}/EX4_HOC.tsx | 0 src/examples/{ => lecture_1}/EX5_RP1.tsx | 0 src/examples/{ => lecture_1}/EX6_RP2.tsx | 0 src/examples/{ => lecture_1}/EX7_RP3.tsx | 0 src/examples/{EX8.RP4.tsx => lecture_1/EX8_RP4.tsx} | 0 10 files changed, 4 insertions(+), 4 deletions(-) rename src/examples/{ => lecture_1}/EX1_SRP.tsx (100%) rename src/examples/{ => lecture_1}/EX2_SOC.tsx (100%) rename src/examples/{ => lecture_1}/EX3_ProjectDashboard.tsx (100%) rename src/examples/{ => lecture_1}/EX4_HOC.tsx (100%) rename src/examples/{ => lecture_1}/EX5_RP1.tsx (100%) rename src/examples/{ => lecture_1}/EX6_RP2.tsx (100%) rename src/examples/{ => lecture_1}/EX7_RP3.tsx (100%) rename src/examples/{EX8.RP4.tsx => lecture_1/EX8_RP4.tsx} (100%) diff --git a/src/App.tsx b/src/App.tsx index d1955bb..a73c951 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import DragAndDrop from './examples/EX8.RP4'; +import DragAndDrop from './examples/lecture_1/EX8_RP4'; import { useState } from 'react'; export default function App() { diff --git a/src/App_lecture1.tsx b/src/App_lecture1.tsx index c59ef0a..e04cc32 100644 --- a/src/App_lecture1.tsx +++ b/src/App_lecture1.tsx @@ -1,6 +1,6 @@ -import { MouseTracker } from './examples/EX5_RP1'; -import { DisableContextMenu } from './examples/EX6_RP2'; -import { ListRenderer } from './examples/EX7_RP3'; +import { MouseTracker } from './examples/lecture_1/EX5_RP1'; +import { DisableContextMenu } from './examples/lecture_1/EX6_RP2'; +import { ListRenderer } from './examples/lecture_1/EX7_RP3'; export default function App() { return ( diff --git a/src/examples/EX1_SRP.tsx b/src/examples/lecture_1/EX1_SRP.tsx similarity index 100% rename from src/examples/EX1_SRP.tsx rename to src/examples/lecture_1/EX1_SRP.tsx diff --git a/src/examples/EX2_SOC.tsx b/src/examples/lecture_1/EX2_SOC.tsx similarity index 100% rename from src/examples/EX2_SOC.tsx rename to src/examples/lecture_1/EX2_SOC.tsx diff --git a/src/examples/EX3_ProjectDashboard.tsx b/src/examples/lecture_1/EX3_ProjectDashboard.tsx similarity index 100% rename from src/examples/EX3_ProjectDashboard.tsx rename to src/examples/lecture_1/EX3_ProjectDashboard.tsx diff --git a/src/examples/EX4_HOC.tsx b/src/examples/lecture_1/EX4_HOC.tsx similarity index 100% rename from src/examples/EX4_HOC.tsx rename to src/examples/lecture_1/EX4_HOC.tsx diff --git a/src/examples/EX5_RP1.tsx b/src/examples/lecture_1/EX5_RP1.tsx similarity index 100% rename from src/examples/EX5_RP1.tsx rename to src/examples/lecture_1/EX5_RP1.tsx diff --git a/src/examples/EX6_RP2.tsx b/src/examples/lecture_1/EX6_RP2.tsx similarity index 100% rename from src/examples/EX6_RP2.tsx rename to src/examples/lecture_1/EX6_RP2.tsx diff --git a/src/examples/EX7_RP3.tsx b/src/examples/lecture_1/EX7_RP3.tsx similarity index 100% rename from src/examples/EX7_RP3.tsx rename to src/examples/lecture_1/EX7_RP3.tsx diff --git a/src/examples/EX8.RP4.tsx b/src/examples/lecture_1/EX8_RP4.tsx similarity index 100% rename from src/examples/EX8.RP4.tsx rename to src/examples/lecture_1/EX8_RP4.tsx From 80571b7347a83658c8b550949135465dbfa51dc1 Mon Sep 17 00:00:00 2001 From: HM Nayem Date: Sun, 16 Feb 2025 05:35:27 +0600 Subject: [PATCH 3/5] added lecture 2 examples --- src/App.tsx | 110 ++++++--------- src/App_lecture1.2.tsx | 75 ++++++++++ src/examples/lecture_2/EX1_MouseTracker.tsx | 22 +++ src/examples/lecture_2/EX2_ABTest.tsx | 15 ++ src/examples/lecture_2/EX3_StepWizard.tsx | 35 +++++ src/examples/lecture_2/EX4_Polling.tsx | 141 +++++++++++++++++++ src/examples/lecture_2/EX5_ThemeProvider.tsx | 51 +++++++ src/examples/lecture_2/EX6_Accordian.tsx | 77 ++++++++++ src/examples/lecture_2/EX7_Tabs.tsx | 134 ++++++++++++++++++ src/examples/lecture_2/EX8_FeatureFlag.tsx | 58 ++++++++ 10 files changed, 651 insertions(+), 67 deletions(-) create mode 100644 src/App_lecture1.2.tsx create mode 100644 src/examples/lecture_2/EX1_MouseTracker.tsx create mode 100644 src/examples/lecture_2/EX2_ABTest.tsx create mode 100644 src/examples/lecture_2/EX3_StepWizard.tsx create mode 100644 src/examples/lecture_2/EX4_Polling.tsx create mode 100644 src/examples/lecture_2/EX5_ThemeProvider.tsx create mode 100644 src/examples/lecture_2/EX6_Accordian.tsx create mode 100644 src/examples/lecture_2/EX7_Tabs.tsx create mode 100644 src/examples/lecture_2/EX8_FeatureFlag.tsx diff --git a/src/App.tsx b/src/App.tsx index a73c951..4c4a43d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,75 +1,51 @@ -import DragAndDrop from './examples/lecture_1/EX8_RP4'; -import { useState } from 'react'; +import FeatureFlag from './examples/lecture_2/EX8_FeatureFlag'; export default function App() { - const items = ['Item 1', 'Item 2', 'Item 3', 'Item 4']; - const [draggedItem, setDraggedItem] = useState(null); - - const dropZoneConfig = { - highlightOnHover: true, - maxItems: 3, // Allow up to 3 items to be dropped + const featureFlags = { + newDashboard: true, + betaFeatures: true, + adminPanel: true, }; return ( -
- ( -
-
-

Draggable Items

- {items.map((item) => ( -
{ - onDragStart(e, item); - setDraggedItem(item); - }} - style={{ - padding: '10px', - marginBottom: '10px', - backgroundColor: '#f9f9f9', - border: '1px solid #ddd', - cursor: 'move', - }} - > - {item} -
- ))} -
- -
{ - onDrop(e); - setDraggedItem(null); - }} - onDragOver={(e) => e.preventDefault()} - style={{ - width: '45%', - padding: '20px', - border: `2px solid ${dropZoneActive ? '#4CAF50' : '#ccc'}`, - backgroundColor: dropZoneActive ? '#eaf8e3' : '#f9f9f9', - transition: - 'background-color 0.3s ease, border-color 0.3s ease', - }} - > -

Drop Zone

-

{dropZoneActive ? 'Release to drop' : 'Drag items here'}

-
- {dropZoneActive &&

Item being dragged: {draggedItem}

} -
-
-
- )} - /> +
+
+ +
+ + {(enabled) => (enabled ? : )} + + + {(enabled) => + enabled ? :

Beta feature is disabled

+ } +
+ + {(enabled) => (enabled ? :

Don't have access

)} +
+
+
); } + +const NewDashboard = () => ( +
+ πŸš€ New Dashboard Enabled! +
+); +const OldDashboard = () => ( +
+ πŸ”™ Old Dashboard +
+); +const BetaFeature = () => ( +
+ πŸ›  Beta Feature Activated! +
+); +const AdminPanel = () => ( +
+ πŸ”‘ Admin Panel Access +
+); diff --git a/src/App_lecture1.2.tsx b/src/App_lecture1.2.tsx new file mode 100644 index 0000000..a73c951 --- /dev/null +++ b/src/App_lecture1.2.tsx @@ -0,0 +1,75 @@ +import DragAndDrop from './examples/lecture_1/EX8_RP4'; +import { useState } from 'react'; + +export default function App() { + const items = ['Item 1', 'Item 2', 'Item 3', 'Item 4']; + const [draggedItem, setDraggedItem] = useState(null); + + const dropZoneConfig = { + highlightOnHover: true, + maxItems: 3, // Allow up to 3 items to be dropped + }; + + return ( +
+ ( +
+
+

Draggable Items

+ {items.map((item) => ( +
{ + onDragStart(e, item); + setDraggedItem(item); + }} + style={{ + padding: '10px', + marginBottom: '10px', + backgroundColor: '#f9f9f9', + border: '1px solid #ddd', + cursor: 'move', + }} + > + {item} +
+ ))} +
+ +
{ + onDrop(e); + setDraggedItem(null); + }} + onDragOver={(e) => e.preventDefault()} + style={{ + width: '45%', + padding: '20px', + border: `2px solid ${dropZoneActive ? '#4CAF50' : '#ccc'}`, + backgroundColor: dropZoneActive ? '#eaf8e3' : '#f9f9f9', + transition: + 'background-color 0.3s ease, border-color 0.3s ease', + }} + > +

Drop Zone

+

{dropZoneActive ? 'Release to drop' : 'Drag items here'}

+
+ {dropZoneActive &&

Item being dragged: {draggedItem}

} +
+
+
+ )} + /> +
+ ); +} diff --git a/src/examples/lecture_2/EX1_MouseTracker.tsx b/src/examples/lecture_2/EX1_MouseTracker.tsx new file mode 100644 index 0000000..32c87cd --- /dev/null +++ b/src/examples/lecture_2/EX1_MouseTracker.tsx @@ -0,0 +1,22 @@ +import { JSX, useState } from 'react'; + +type MouseTrackerProps = { + className?: string; + children: (x: number, y: number) => JSX.Element; +}; + +export const MouseTracker = ({ + children, + className = 'w-full h-full', +}: MouseTrackerProps) => { + const [position, setPosition] = useState({ x: 0, y: 0 }); + + return ( +
setPosition({ x: e.clientX, y: e.clientY })} + > + {children(position.x, position.y)} +
+ ); +}; diff --git a/src/examples/lecture_2/EX2_ABTest.tsx b/src/examples/lecture_2/EX2_ABTest.tsx new file mode 100644 index 0000000..e763b54 --- /dev/null +++ b/src/examples/lecture_2/EX2_ABTest.tsx @@ -0,0 +1,15 @@ +import { FC, ReactNode } from 'react'; + +type ABTestProps = { + experiments: { [key: string]: number }; + children: (variant: string) => ReactNode; +}; + +export const ABTest: FC = ({ experiments, children }) => { + const variants = Object.keys(experiments); + + const random = Math.floor(Math.random() * variants.length); + const assignedVariant = variants[random]; + + return children(assignedVariant); +}; diff --git a/src/examples/lecture_2/EX3_StepWizard.tsx b/src/examples/lecture_2/EX3_StepWizard.tsx new file mode 100644 index 0000000..2f477ca --- /dev/null +++ b/src/examples/lecture_2/EX3_StepWizard.tsx @@ -0,0 +1,35 @@ +import { ReactNode, useState } from 'react'; + +type StepRendererProps = { + currentStep: number; + totalSteps: number; + goToNext: () => void; + goToPrev: () => void; + goToStep: (step: number) => void; +}; + +type StepWizardProps = { + steps: number; + children: (props: StepRendererProps) => ReactNode; +}; + +export const StepWizard = ({ steps, children }: StepWizardProps) => { + const [currentStep, setCurrentStep] = useState(1); + + const goToNext = () => + setCurrentStep((prev) => (prev < steps ? prev + 1 : prev)); + const goToPrev = () => setCurrentStep((prev) => (prev > 1 ? prev - 1 : prev)); + const goToStep = (step: number) => { + if (step >= 1 && step <= steps) { + setCurrentStep(step); + } + }; + + return children({ + currentStep, + totalSteps: steps, + goToNext, + goToPrev, + goToStep, + }); +}; diff --git a/src/examples/lecture_2/EX4_Polling.tsx b/src/examples/lecture_2/EX4_Polling.tsx new file mode 100644 index 0000000..d21cbca --- /dev/null +++ b/src/examples/lecture_2/EX4_Polling.tsx @@ -0,0 +1,141 @@ +import { ReactNode, useEffect, useState, useCallback } from 'react'; + +type PollingState = { + data: T | null; + error: Error | null; + lastFetched: Date | null; +}; + +type PollingProps = { + interval: number; + initialData?: T | null; + fetcher: () => Promise; + children: (props: { + data: T | null; + isError: boolean; + error: Error | null; + lastFetched: Date | null; + refresh: () => Promise; + pause: () => void; + resume: () => void; + }) => ReactNode; +}; + +export const Polling = ({ + interval, + fetcher, + children, + initialData = null, +}: PollingProps) => { + const [state, setState] = useState>({ + data: initialData, + error: null, + lastFetched: null, + }); + const [isPaused, setIsPaused] = useState(false); + + const fetchData = useCallback(async () => { + try { + const data = await fetcher(); + setState({ + data, + error: null, + lastFetched: new Date(), + }); + } catch (error) { + setState((prev) => ({ + ...prev, + error: error as Error, + })); + } + }, [fetcher]); + + useEffect(() => { + if (isPaused) return; + + fetchData(); + const intervalId = setInterval(() => { + fetchData(); + }, interval); + + return () => clearInterval(intervalId); + }, [interval, fetchData, isPaused]); + + const refresh = useCallback(async () => { + await fetchData(); + }, [fetchData]); + + const pause = useCallback(() => { + setIsPaused(true); + }, []); + + const resume = useCallback(() => { + setIsPaused(false); + }, []); + + return children({ + ...state, + isError: state.error !== null, + refresh, + pause, + resume, + }); +}; + +const PollingExample = () => { + const mockFetcher = async () => { + // Fetch a random post from JSONPlaceholder + const response = await fetch( + 'https://jsonplaceholder.typicode.com/posts/' + + Math.floor(Math.random() * 100 + 1) + ); + const data = await response.json(); + return data.title; + }; + + return ( +
+

Polling Example

+ + + {({ data, lastFetched, isError, error, refresh, pause, resume }) => ( +
+
+

Post Title: {data}

+

+ Last Updated: {lastFetched?.toLocaleTimeString()} +

+
+ + {isError && ( +
Error: {error?.message}
+ )} + +
+ + + +
+
+ )} +
+
+ ); +}; + +export default PollingExample; diff --git a/src/examples/lecture_2/EX5_ThemeProvider.tsx b/src/examples/lecture_2/EX5_ThemeProvider.tsx new file mode 100644 index 0000000..4be2e07 --- /dev/null +++ b/src/examples/lecture_2/EX5_ThemeProvider.tsx @@ -0,0 +1,51 @@ +import { createContext, PropsWithChildren, useContext, useState } from 'react'; + +type Theme = 'light' | 'dark'; + +type ThemeContext = { + theme: Theme; + toggleTheme: () => void; +}; + +const ThemeContext = createContext(null); + +export const ThemeProvider = ({ children }: PropsWithChildren) => { + const [theme, setTheme] = useState('light'); + + const toggleTheme = () => { + setTheme((prev) => (prev === 'light' ? 'dark' : 'light')); + }; + + return {children}; +}; + +export const ThemeSwitcher = () => { + const context = useContext(ThemeContext); + + if (!context) { + throw new Error('ThemeSwitcher must be used within a ThemeProvider'); + } + + return ( + + ); +}; + +export const useTheme = () => { + const context = useContext(ThemeContext); + + if (!context) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + + return context; +}; diff --git a/src/examples/lecture_2/EX6_Accordian.tsx b/src/examples/lecture_2/EX6_Accordian.tsx new file mode 100644 index 0000000..a7ce3b6 --- /dev/null +++ b/src/examples/lecture_2/EX6_Accordian.tsx @@ -0,0 +1,77 @@ +import { createContext, PropsWithChildren, useContext, useState } from 'react'; + +type AccordionContext = { + isOpen: boolean; + toggle: () => void; +}; + +const AccordionContext = createContext(null); + +export const Accordion = ({ children }: PropsWithChildren) => { + const [isOpen, setIsOpen] = useState(false); + const toggle = () => setIsOpen((prev) => !prev); + + return ( + +
{children}
+
+ ); +}; + +const useAccordion = () => { + const context = useContext(AccordionContext); + if (!context) { + throw new Error('useAccordion must be used within an Accordion'); + } + return context; +}; + +const AccordionHeader = ({ + children, + className = '', +}: PropsWithChildren & { className?: string }) => { + const { isOpen, toggle } = useAccordion(); + return ( + + ); +}; + +const AccordionContent = ({ + children, + className = '', +}: PropsWithChildren & { className?: string }) => { + const { isOpen } = useAccordion(); + return isOpen ? ( +
+ {children} +
+ ) : null; +}; + +Accordion.Header = AccordionHeader; +Accordion.Content = AccordionContent; diff --git a/src/examples/lecture_2/EX7_Tabs.tsx b/src/examples/lecture_2/EX7_Tabs.tsx new file mode 100644 index 0000000..399ef76 --- /dev/null +++ b/src/examples/lecture_2/EX7_Tabs.tsx @@ -0,0 +1,134 @@ +import { + createContext, + PropsWithChildren, + ReactNode, + useContext, + useEffect, + useState, +} from 'react'; + +type TabsContextProps = { + activeTab: string; + setActiveTab: (tab: string) => void; + registerTab: (tab: string) => void; + isTabRegistered: (tab: string) => boolean; +}; + +const TabsContext = createContext(null); + +type TabsProps = { + defaultTab?: string; + children: ReactNode; +}; + +const Tabs = ({ defaultTab, children }: TabsProps) => { + const [activeTab, setActiveTab] = useState(defaultTab || ''); + const [registeredTabs, setRegisteredTabs] = useState([]); + + const registerTab = (tab: string) => { + setRegisteredTabs((prev) => { + if (prev.includes(tab)) return prev; + return [...prev, tab]; + }); + }; + + useEffect(() => { + if (defaultTab && registeredTabs.includes(defaultTab)) { + setActiveTab(defaultTab); + } else if (registeredTabs.length > 0 && !activeTab) { + setActiveTab(registeredTabs[0]); + } + }, [defaultTab, registeredTabs, activeTab]); + + const isTabRegistered = (tab: string) => registeredTabs.includes(tab); + + return ( + +
{children}
+
+ ); +}; + +const useTabs = () => { + const context = useContext(TabsContext); + if (!context) { + throw new Error('useTabs must be used within a Tabs component'); + } + return context; +}; + +const TabList = ({ + children, + className = '', +}: PropsWithChildren<{ className?: string }>) => { + return ( +
+ {children} +
+ ); +}; + +type TabProps = { + id: string; + disabled?: boolean; + children: ReactNode; + className?: string; +}; + +const Tab = ({ id, disabled = false, children, className = '' }: TabProps) => { + const { activeTab, setActiveTab, registerTab, isTabRegistered } = useTabs(); + + useEffect(() => { + registerTab(id); + }, [id, registerTab]); + + if (!isTabRegistered(id)) return null; + + return ( + + ); +}; + +type TabPanelProps = { + id: string; + lazyLoad?: boolean; + children: ReactNode; + className?: string; +}; + +const TabPane = ({ + id, + lazyLoad = false, + children, + className = '', +}: TabPanelProps) => { + const { activeTab, isTabRegistered } = useTabs(); + + if (!isTabRegistered(id)) return null; + if (lazyLoad && activeTab !== id) return null; + + return activeTab === id ? ( +
{children}
+ ) : null; +}; + +Tabs.TabList = TabList; +Tabs.Tab = Tab; +Tabs.TabPane = TabPane; + +export default Tabs; diff --git a/src/examples/lecture_2/EX8_FeatureFlag.tsx b/src/examples/lecture_2/EX8_FeatureFlag.tsx new file mode 100644 index 0000000..269e999 --- /dev/null +++ b/src/examples/lecture_2/EX8_FeatureFlag.tsx @@ -0,0 +1,58 @@ +import { createContext, ReactNode, useContext } from 'react'; + +type FeatureFlagContextProps = { + isFeatureEnabled: (flag: string) => boolean; + userRole: string; +}; + +const FeatureFlagContext = createContext(null); + +type FeatureFlagProviderProps = { + flags: Record; + userRole: string; + children: ReactNode; +}; + +const FeatureFlagProvider = ({ + flags, + userRole, + children, +}: FeatureFlagProviderProps) => { + const isFeatureEnabled = (flag: string) => !!flags[flag]; + + return ( + + {children} + + ); +}; + +const useFeatureFlag = () => { + const context = useContext(FeatureFlagContext); + + if (!context) { + throw new Error('useFeatureFlag must be used within a FeatureFlagProvider'); + } + + return context; +}; + +type FeatureFlagProps = { + name: string; + requiredRole?: string; + children: (enabled: boolean) => ReactNode; +}; + +const FeatureFlag = ({ name, requiredRole, children }: FeatureFlagProps) => { + const { isFeatureEnabled, userRole } = useFeatureFlag(); + + const enabled = + isFeatureEnabled(name) && (!requiredRole || userRole === requiredRole); + + return children(enabled); +}; + +FeatureFlag.Provider = FeatureFlagProvider; +FeatureFlag.Flag = FeatureFlag; + +export default FeatureFlag; From 7ac76e8dddbcdfedd32088829ac8675360905ccc Mon Sep 17 00:00:00 2001 From: HM Nayem Date: Sun, 16 Feb 2025 23:48:51 +0600 Subject: [PATCH 4/5] feat: lecture 2 completed --- src/App.tsx | 32 +++++--- src/App_lecture2.1.tsx | 28 +++++++ src/App_lecture2.2.tsx | 45 +++++++++++ src/App_lecture2.3.tsx | 67 ++++++++++++++++ src/demo/ccp/Accordion.tsx | 81 +++++++++++++++++++ src/demo/ccp/FeatureFlag.tsx | 55 +++++++++++++ src/demo/facp/ABTesting.tsx | 15 ++++ src/demo/facp/MouseTracker.tsx | 22 +++++ src/examples/lecture_2/EX3_StepWizard.tsx | 98 +++++++++++++++++++++++ src/examples/lecture_2/EX4_Polling.tsx | 6 +- src/examples/lecture_2/EX7_Tabs.tsx | 4 +- 11 files changed, 438 insertions(+), 15 deletions(-) create mode 100644 src/App_lecture2.1.tsx create mode 100644 src/App_lecture2.2.tsx create mode 100644 src/App_lecture2.3.tsx create mode 100644 src/demo/ccp/Accordion.tsx create mode 100644 src/demo/ccp/FeatureFlag.tsx create mode 100644 src/demo/facp/ABTesting.tsx create mode 100644 src/demo/facp/MouseTracker.tsx diff --git a/src/App.tsx b/src/App.tsx index 4c4a43d..fd8847d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,27 +1,26 @@ -import FeatureFlag from './examples/lecture_2/EX8_FeatureFlag'; +import { FeatureFlag } from './demo/ccp/FeatureFlag'; export default function App() { - const featureFlags = { - newDashboard: true, - betaFeatures: true, + const flags = { + newDashboard: false, + betaFeature: true, adminPanel: true, }; return (
-
- +
- + {(enabled) => (enabled ? : )} - + {(enabled) => enabled ? :

Beta feature is disabled

}
- - {(enabled) => (enabled ? :

Don't have access

)} + + {(enabled) => (enabled ? : null)}
@@ -49,3 +48,16 @@ const AdminPanel = () => ( πŸ”‘ Admin Panel Access
); + +/** + * The **Compound Component Pattern** in React allows multiple related components to work together as a single unit. Instead of passing multiple props to control child components, compound components communicate implicitly through **context or React’s `children` prop**. + +This pattern is useful when you want to design **flexible, reusable UI components** that allow users to compose them in different ways. + +**When to Use Compound Components? (Use Cases)** + +- **Building UI libraries** – Tabs, Accordions, Dropdowns, Modals, etc. +- **Designing flexible, reusable components** – Form controls, Wizards. +- **When multiple components share a common state** – Controlled components. +- **Improving code readability & maintainability** – Reducing prop drilling. + */ diff --git a/src/App_lecture2.1.tsx b/src/App_lecture2.1.tsx new file mode 100644 index 0000000..ce03d5c --- /dev/null +++ b/src/App_lecture2.1.tsx @@ -0,0 +1,28 @@ +import { ABTesting } from './demo/facp/ABTesting'; + +export default function App() { + return ( +
+ + {(variant) => { + switch (variant) { + case 'red': + return ( +
Red
+ ); + case 'blue': + return ( +
Blue
+ ); + case 'green': + return ( +
Green
+ ); + default: + return null; + } + }} +
+
+ ); +} diff --git a/src/App_lecture2.2.tsx b/src/App_lecture2.2.tsx new file mode 100644 index 0000000..b30da8d --- /dev/null +++ b/src/App_lecture2.2.tsx @@ -0,0 +1,45 @@ +// import { ABTest } from './examples/lecture_2/EX2_ABTest'; +// import { +// PhotoPreviewer, +// StepWizard, +// Timeline, +// } from './examples/lecture_2/EX3_StepWizard'; +// import PollingExample from './examples/lecture_2/EX4_Polling'; +import { ThemeProvider } from './examples/lecture_2/EX5_ThemeProvider'; + +const photos = [ + 'https://picsum.photos/400/300?random=1', + 'https://picsum.photos/400/300?random=2', + 'https://picsum.photos/400/300?random=3', + 'https://picsum.photos/400/300?random=4', + 'https://picsum.photos/400/300?random=5', +]; + +export default function App() { + return ( + +
+ {/* + +
+ + +
+ + + {({ currentStep, goToPrev, goToNext }) => ( +
+

Current Step {currentStep}

+
+ + +
+
+ )} +
*/} + + {/* */} +
+ + ); +} diff --git a/src/App_lecture2.3.tsx b/src/App_lecture2.3.tsx new file mode 100644 index 0000000..b5e91a8 --- /dev/null +++ b/src/App_lecture2.3.tsx @@ -0,0 +1,67 @@ +import { Accordion } from './demo/ccp/Accordion'; +import Tabs from './examples/lecture_2/EX7_Tabs'; + +export default function App() { + return ( +
+
+ + Header + Content + + + Second Accordion + +

+ This is the content of the second accordion +

+
+
+
+
+ + + Tab 1 + Tab 2 + Tab 3 + Tab 4 + + +

I am Tab 1

+
+ +

I am Tab 2

+
+ +
+

I am Tab 3

+

+ Lorem ipsum dolor sit amet consectetur adipisicing elit. + Quisquam, quos. +

+ +
+
+ +

I am Tab 4

+
+
+
+
+ ); +} + +/** + * The **Compound Component Pattern** in React allows multiple related components to work together as a single unit. Instead of passing multiple props to control child components, compound components communicate implicitly through **context or React’s `children` prop**. + +This pattern is useful when you want to design **flexible, reusable UI components** that allow users to compose them in different ways. + +**When to Use Compound Components? (Use Cases)** + +- **Building UI libraries** – Tabs, Accordions, Dropdowns, Modals, etc. +- **Designing flexible, reusable components** – Form controls, Wizards. +- **When multiple components share a common state** – Controlled components. +- **Improving code readability & maintainability** – Reducing prop drilling. + */ diff --git a/src/demo/ccp/Accordion.tsx b/src/demo/ccp/Accordion.tsx new file mode 100644 index 0000000..73ba44c --- /dev/null +++ b/src/demo/ccp/Accordion.tsx @@ -0,0 +1,81 @@ +import { createContext, PropsWithChildren, useContext, useState } from 'react'; + +type AccordionContext = { + isOpen: boolean; + toggle: () => void; +}; + +const AccordionContext = createContext(null); + +export const Accordion = ({ children }: PropsWithChildren) => { + const [isOpen, setIsOpen] = useState(false); + const toggle = () => setIsOpen((prev) => !prev); + + return ( + {children} + ); +}; + +const useAccordion = () => { + const context = useContext(AccordionContext); + + if (!context) { + throw new Error('useAccordion must be used within an Accordion'); + } + + return context; +}; + +const AccordionHeader = ({ + children, + className = '', +}: PropsWithChildren<{ className?: string }>) => { + const { isOpen, toggle } = useAccordion(); + + return ( + + ); +}; + +const AccordionContent = ({ + children, + className = '', +}: PropsWithChildren<{ className?: string }>) => { + const { isOpen } = useAccordion(); + + return isOpen ? ( +
+ {children} +
+ ) : null; +}; + +Accordion.Header = AccordionHeader; +Accordion.Content = AccordionContent; + +export default Accordion; diff --git a/src/demo/ccp/FeatureFlag.tsx b/src/demo/ccp/FeatureFlag.tsx new file mode 100644 index 0000000..6775e56 --- /dev/null +++ b/src/demo/ccp/FeatureFlag.tsx @@ -0,0 +1,55 @@ +import { createContext, ReactNode, useContext } from 'react'; + +type FeatureFlagContext = { + isFeatureEnabled: (flag: string) => boolean; + userRole: string; +}; + +const FeatureFlagContext = createContext(null); + +type FeatureFlagProviderProps = { + flags: Record; + userRole: string; + children: ReactNode; +}; + +const FeatureFlagProvider = ({ + flags, + userRole, + children, +}: FeatureFlagProviderProps) => { + const isFeatureEnabled = (flag: string) => !!flags[flag]; + + return ( + + {children} + + ); +}; + +const useFeatureFlag = () => { + const context = useContext(FeatureFlagContext); + if (!context) { + throw new Error('useFeatureFlag must be used within a FeatureFlagProvider'); + } + return context; +}; + +type FeatureFlagProps = { + flag: string; + requiredRole?: string; + children: (enabled: boolean) => ReactNode; +}; + +const Flag = ({ flag, requiredRole, children }: FeatureFlagProps) => { + const { isFeatureEnabled, userRole } = useFeatureFlag(); + const enabled = + isFeatureEnabled(flag) && (!requiredRole || userRole === requiredRole); + + return children(enabled); +}; + +export const FeatureFlag = { + Provider: FeatureFlagProvider, + Flag: Flag, +}; diff --git a/src/demo/facp/ABTesting.tsx b/src/demo/facp/ABTesting.tsx new file mode 100644 index 0000000..716d53a --- /dev/null +++ b/src/demo/facp/ABTesting.tsx @@ -0,0 +1,15 @@ +import { ReactNode } from 'react'; + +type ABTestingProps = { + experiments: { [key: string]: number }; + children: (variant: string) => ReactNode; +}; + +export const ABTesting = ({ experiments, children }: ABTestingProps) => { + const variants = Object.keys(experiments); + + const random = Math.floor(Math.random() * variants.length); + const assignedVariant = variants[random]; + + return children(assignedVariant); +}; diff --git a/src/demo/facp/MouseTracker.tsx b/src/demo/facp/MouseTracker.tsx new file mode 100644 index 0000000..53940d8 --- /dev/null +++ b/src/demo/facp/MouseTracker.tsx @@ -0,0 +1,22 @@ +import { ReactNode, useState } from 'react'; + +type MouseTrackerProps = { + className?: string; + children: (x: number, y: number) => ReactNode; +}; + +export const MouseTracker = ({ + className = 'w-full h-full', + children, +}: MouseTrackerProps) => { + const [position, setPosition] = useState({ x: 0, y: 0 }); + + return ( +
setPosition({ x: e.clientX, y: e.clientY })} + > + {children(position.x, position.y)} +
+ ); +}; diff --git a/src/examples/lecture_2/EX3_StepWizard.tsx b/src/examples/lecture_2/EX3_StepWizard.tsx index 2f477ca..2d89f93 100644 --- a/src/examples/lecture_2/EX3_StepWizard.tsx +++ b/src/examples/lecture_2/EX3_StepWizard.tsx @@ -33,3 +33,101 @@ export const StepWizard = ({ steps, children }: StepWizardProps) => { goToStep, }); }; + + +/** + * Applications: + */ + +// Photo Viewer + +export const PhotoPreviewer = ({ photos }: { photos: string[] }) => { + return ( + + {({ currentStep, goToNext, goToPrev, goToStep, totalSteps }) => ( +
+
+ {`Photo + + +
+
+ {Array.from({ length: totalSteps }, (_, i) => ( +
+
+ )} +
+ ); +}; + +// Horizontal Timeline + +type TimelineProps = { + steps: number; +}; + +export const Timeline = ({ steps }: TimelineProps) => { + return ( + + {({ currentStep, totalSteps, goToNext, goToPrev }) => ( +
+
+ {Array.from({ length: totalSteps }, (_, i) => i + 1).map((step) => ( +
+
+ {step} +
+ {step < totalSteps && ( +
+ )} +
+ ))} +
+
+ + +
+
+ )} + + ); +}; \ No newline at end of file diff --git a/src/examples/lecture_2/EX4_Polling.tsx b/src/examples/lecture_2/EX4_Polling.tsx index d21cbca..36268f5 100644 --- a/src/examples/lecture_2/EX4_Polling.tsx +++ b/src/examples/lecture_2/EX4_Polling.tsx @@ -7,7 +7,7 @@ type PollingState = { }; type PollingProps = { - interval: number; + interval?: number; initialData?: T | null; fetcher: () => Promise; children: (props: { @@ -22,7 +22,7 @@ type PollingProps = { }; export const Polling = ({ - interval, + interval = 5000, fetcher, children, initialData = null, @@ -97,7 +97,7 @@ const PollingExample = () => {

Polling Example

- + {({ data, lastFetched, isError, error, refresh, pause, resume }) => (
diff --git a/src/examples/lecture_2/EX7_Tabs.tsx b/src/examples/lecture_2/EX7_Tabs.tsx index 399ef76..b4dcad2 100644 --- a/src/examples/lecture_2/EX7_Tabs.tsx +++ b/src/examples/lecture_2/EX7_Tabs.tsx @@ -127,8 +127,8 @@ const TabPane = ({ ) : null; }; -Tabs.TabList = TabList; +Tabs.List = TabList; Tabs.Tab = Tab; -Tabs.TabPane = TabPane; +Tabs.Pane = TabPane; export default Tabs; From e2378488683bdc97a662d7aadd0b817eef61702c Mon Sep 17 00:00:00 2001 From: HM Nayem Date: Tue, 18 Feb 2025 02:12:32 +0600 Subject: [PATCH 5/5] feat: lecture 3 completed --- package.json | 5 +- pnpm-lock.yaml | 213 ++++++++++++++++++ src/App.tsx | 105 ++++----- src/App_lecture2.4.tsx | 63 ++++++ src/demo/cpp/Accordion.tsx | 102 +++++++++ src/demo/cpp/Toggle.tsx | 44 ++++ src/demo/pp/PostList.tsx | 38 ++++ src/demo/pp/ServiceProvider.tsx | 33 +++ src/demo/pp/UserList.tsx | 42 ++++ src/demo/pp/apiClient.ts | 44 ++++ src/demo/pp/logger.ts | 13 ++ src/examples/lecture_3/EX1_Toggle.tsx | 39 ++++ src/examples/lecture_3/EX2_InputField.tsx | 78 +++++++ src/examples/lecture_3/EX3_Accordion.tsx | 98 ++++++++ .../lecture_3/EX4_DI/ServiceProvider.tsx | 32 +++ src/examples/lecture_3/EX4_DI/Users.tsx | 38 ++++ src/examples/lecture_3/EX4_DI/apiClient.ts | 45 ++++ src/examples/lecture_3/EX4_DI/logger.ts | 13 ++ 18 files changed, 988 insertions(+), 57 deletions(-) create mode 100644 src/App_lecture2.4.tsx create mode 100644 src/demo/cpp/Accordion.tsx create mode 100644 src/demo/cpp/Toggle.tsx create mode 100644 src/demo/pp/PostList.tsx create mode 100644 src/demo/pp/ServiceProvider.tsx create mode 100644 src/demo/pp/UserList.tsx create mode 100644 src/demo/pp/apiClient.ts create mode 100644 src/demo/pp/logger.ts create mode 100644 src/examples/lecture_3/EX1_Toggle.tsx create mode 100644 src/examples/lecture_3/EX2_InputField.tsx create mode 100644 src/examples/lecture_3/EX3_Accordion.tsx create mode 100644 src/examples/lecture_3/EX4_DI/ServiceProvider.tsx create mode 100644 src/examples/lecture_3/EX4_DI/Users.tsx create mode 100644 src/examples/lecture_3/EX4_DI/apiClient.ts create mode 100644 src/examples/lecture_3/EX4_DI/logger.ts diff --git a/package.json b/package.json index e344965..4afbad9 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,11 @@ }, "dependencies": { "@tailwindcss/vite": "^4.0.6", + "axios": "^1.7.9", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "reflect-metadata": "^0.2.2", + "tsyringe": "^4.8.0" }, "devDependencies": { "@eslint/js": "^9.19.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ced0499..775014f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,12 +11,21 @@ importers: '@tailwindcss/vite': specifier: ^4.0.6 version: 4.0.6(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)) + axios: + specifier: ^1.7.9 + version: 1.7.9 react: specifier: ^19.0.0 version: 19.0.0 react-dom: specifier: ^19.0.0 version: 19.0.0(react@19.0.0) + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + tsyringe: + specifier: ^4.8.0 + version: 4.8.0 devDependencies: '@eslint/js': specifier: ^9.19.0 @@ -703,6 +712,9 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + autoprefixer@10.4.20: resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} engines: {node: ^10 || ^12 || >=14} @@ -710,6 +722,9 @@ packages: peerDependencies: postcss: ^8.1.0 + axios@1.7.9: + resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==} + babel-plugin-react-compiler@19.0.0-beta-30d8a17-20250209: resolution: {integrity: sha512-0pQHlz5nmBiEQ8ZWWVLeaBzz/FkToAdXEXBBnd21uSrDtIzhSe+s3VMvqMsv6vYHNTr+0KmsvVfEqXQp0W0kzg==} @@ -731,6 +746,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -749,6 +768,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -774,11 +797,19 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + detect-libc@1.0.3: resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} engines: {node: '>=0.10'} hasBin: true + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + electron-to-chromium@1.5.101: resolution: {integrity: sha512-L0ISiQrP/56Acgu4/i/kfPwWSgrzYZUnQrC0+QPFuhqlLP1Ir7qzPPDVS9BcKIyWTRU8+o6CC8dKw38tSWhYIA==} @@ -786,6 +817,22 @@ packages: resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} engines: {node: '>=10.13.0'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.24.2: resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} engines: {node: '>=18'} @@ -893,6 +940,19 @@ packages: flatted@3.3.2: resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -901,10 +961,21 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-intrinsic@1.2.7: + resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -925,6 +996,10 @@ packages: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -935,6 +1010,18 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + hermes-estree@0.25.1: resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} @@ -1079,6 +1166,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1087,6 +1178,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1154,6 +1253,9 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1174,6 +1276,9 @@ packages: resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1239,6 +1344,13 @@ packages: peerDependencies: typescript: '>=4.8.4' + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tsyringe@4.8.0: + resolution: {integrity: sha512-YB1FG+axdxADa3ncEtRnQCFq/M0lALGLxSZeVNbTU8NqhOVc51nnv2CISTcvc1kyv6EGPtXVr0v6lWeDxiijOA==} + engines: {node: '>= 6.0.0'} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -1926,6 +2038,8 @@ snapshots: argparse@2.0.1: {} + asynckit@0.4.0: {} + autoprefixer@10.4.20(postcss@8.5.2): dependencies: browserslist: 4.24.4 @@ -1936,6 +2050,14 @@ snapshots: postcss: 8.5.2 postcss-value-parser: 4.2.0 + axios@1.7.9: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.2 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + babel-plugin-react-compiler@19.0.0-beta-30d8a17-20250209: dependencies: '@babel/types': 7.26.9 @@ -1962,6 +2084,11 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.2(browserslist@4.24.4) + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + callsites@3.1.0: {} caniuse-lite@1.0.30001699: {} @@ -1977,6 +2104,10 @@ snapshots: color-name@1.1.4: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + concat-map@0.0.1: {} convert-source-map@2.0.0: {} @@ -1995,8 +2126,16 @@ snapshots: deep-is@0.1.4: {} + delayed-stream@1.0.0: {} + detect-libc@1.0.3: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + electron-to-chromium@1.5.101: {} enhanced-resolve@5.18.1: @@ -2004,6 +2143,21 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.1 + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + esbuild@0.24.2: optionalDependencies: '@esbuild/aix-ppc64': 0.24.2 @@ -2162,13 +2316,42 @@ snapshots: flatted@3.3.2: {} + follow-redirects@1.15.9: {} + + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + fraction.js@4.3.7: {} fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + gensync@1.0.0-beta.2: {} + get-intrinsic@1.2.7: + 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 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -2183,12 +2366,24 @@ snapshots: globals@15.15.0: {} + gopd@1.2.0: {} + graceful-fs@4.2.11: {} graphemer@1.4.0: {} has-flag@4.0.0: {} + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + hermes-estree@0.25.1: {} hermes-parser@0.25.1: @@ -2296,6 +2491,8 @@ snapshots: dependencies: yallist: 3.1.1 + math-intrinsics@1.1.0: {} + merge2@1.4.1: {} micromatch@4.0.8: @@ -2303,6 +2500,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -2360,6 +2563,8 @@ snapshots: prelude-ls@1.2.1: {} + proxy-from-env@1.1.0: {} + punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -2373,6 +2578,8 @@ snapshots: react@19.0.0: {} + reflect-metadata@0.2.2: {} + resolve-from@4.0.0: {} reusify@1.0.4: {} @@ -2438,6 +2645,12 @@ snapshots: dependencies: typescript: 5.7.3 + tslib@1.14.1: {} + + tsyringe@4.8.0: + dependencies: + tslib: 1.14.1 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 diff --git a/src/App.tsx b/src/App.tsx index fd8847d..23e793f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,63 +1,56 @@ -import { FeatureFlag } from './demo/ccp/FeatureFlag'; +import { useState } from 'react'; +import { Accordion } from './demo/cpp/Accordion'; +import { ServiceProvider } from './demo/pp/ServiceProvider'; +import { UserList } from './demo/pp/UserList'; +import { PostList } from './demo/pp/PostList'; + +const items = [ + { + id: '1', + label: 'Item 1', + content: 'This is the content of item 1', + }, + { + id: '2', + label: 'Item 2', + content: 'This is the content of item 2', + }, + { + id: '3', + label: 'Item 3', + content: 'This is the content of item 3', + }, +]; export default function App() { - const flags = { - newDashboard: false, - betaFeature: true, - adminPanel: true, - }; + const [selectedItems, setSelectedItems] = useState([]); return ( -
- -
- - {(enabled) => (enabled ? : )} - - - {(enabled) => - enabled ? :

Beta feature is disabled

- } -
- - {(enabled) => (enabled ? : null)} - + +
+
+ {items.map((item) => ( + + setSelectedItems((prev) => + prev.includes(item.id) + ? prev.filter((id) => id !== item.id) + : [...prev, item.id] + ) + } + > + {item.label} + {item.content} + + ))} +
+
+ +
- -
+
+ ); } - -const NewDashboard = () => ( -
- πŸš€ New Dashboard Enabled! -
-); -const OldDashboard = () => ( -
- πŸ”™ Old Dashboard -
-); -const BetaFeature = () => ( -
- πŸ›  Beta Feature Activated! -
-); -const AdminPanel = () => ( -
- πŸ”‘ Admin Panel Access -
-); - -/** - * The **Compound Component Pattern** in React allows multiple related components to work together as a single unit. Instead of passing multiple props to control child components, compound components communicate implicitly through **context or React’s `children` prop**. - -This pattern is useful when you want to design **flexible, reusable UI components** that allow users to compose them in different ways. - -**When to Use Compound Components? (Use Cases)** - -- **Building UI libraries** – Tabs, Accordions, Dropdowns, Modals, etc. -- **Designing flexible, reusable components** – Form controls, Wizards. -- **When multiple components share a common state** – Controlled components. -- **Improving code readability & maintainability** – Reducing prop drilling. - */ diff --git a/src/App_lecture2.4.tsx b/src/App_lecture2.4.tsx new file mode 100644 index 0000000..fd8847d --- /dev/null +++ b/src/App_lecture2.4.tsx @@ -0,0 +1,63 @@ +import { FeatureFlag } from './demo/ccp/FeatureFlag'; + +export default function App() { + const flags = { + newDashboard: false, + betaFeature: true, + adminPanel: true, + }; + + return ( +
+ +
+ + {(enabled) => (enabled ? : )} + + + {(enabled) => + enabled ? :

Beta feature is disabled

+ } +
+ + {(enabled) => (enabled ? : null)} + +
+
+
+ ); +} + +const NewDashboard = () => ( +
+ πŸš€ New Dashboard Enabled! +
+); +const OldDashboard = () => ( +
+ πŸ”™ Old Dashboard +
+); +const BetaFeature = () => ( +
+ πŸ›  Beta Feature Activated! +
+); +const AdminPanel = () => ( +
+ πŸ”‘ Admin Panel Access +
+); + +/** + * The **Compound Component Pattern** in React allows multiple related components to work together as a single unit. Instead of passing multiple props to control child components, compound components communicate implicitly through **context or React’s `children` prop**. + +This pattern is useful when you want to design **flexible, reusable UI components** that allow users to compose them in different ways. + +**When to Use Compound Components? (Use Cases)** + +- **Building UI libraries** – Tabs, Accordions, Dropdowns, Modals, etc. +- **Designing flexible, reusable components** – Form controls, Wizards. +- **When multiple components share a common state** – Controlled components. +- **Improving code readability & maintainability** – Reducing prop drilling. + */ diff --git a/src/demo/cpp/Accordion.tsx b/src/demo/cpp/Accordion.tsx new file mode 100644 index 0000000..bb1cbd4 --- /dev/null +++ b/src/demo/cpp/Accordion.tsx @@ -0,0 +1,102 @@ +import { createContext, PropsWithChildren, useContext, useState } from 'react'; + +type AccordionContext = { + isOpen: boolean; + toggle: () => void; +}; + +const AccordionContext = createContext(null); + +type AccordionProps = PropsWithChildren<{ + isOpen?: boolean; + defaultOpen?: boolean; + onToggle?: (isOpen: boolean) => void; +}>; + +export const Accordion = ({ + isOpen, + defaultOpen = false, + onToggle, + children, +}: AccordionProps) => { + const [isOpenInternal, setIsOpenInternal] = useState(defaultOpen); + + const isControlled = isOpen !== undefined; + const currentIsOpen = isControlled ? isOpen : isOpenInternal; + + const toggle = () => { + if (isControlled) { + onToggle?.(!currentIsOpen); + } else { + setIsOpenInternal(!isOpenInternal); + } + }; + + return ( + +
{children}
+
+ ); +}; + +const useAccordion = () => { + const context = useContext(AccordionContext); + if (!context) { + throw new Error('useAccordion must be used within an Accordion'); + } + return context; +}; + +const AccordionHeader = ({ + children, + className = '', +}: PropsWithChildren & { className?: string }) => { + const { isOpen, toggle } = useAccordion(); + return ( + + ); +}; + +const AccordionContent = ({ + children, + className = '', + lazy = true, +}: PropsWithChildren & { className?: string; lazy?: boolean }) => { + const { isOpen } = useAccordion(); + + if (lazy && !isOpen) return null; + + return isOpen ? ( +
+ {children} +
+ ) : null; +}; + +Accordion.Header = AccordionHeader; +Accordion.Content = AccordionContent; diff --git a/src/demo/cpp/Toggle.tsx b/src/demo/cpp/Toggle.tsx new file mode 100644 index 0000000..82d446a --- /dev/null +++ b/src/demo/cpp/Toggle.tsx @@ -0,0 +1,44 @@ +import { ReactNode, useState } from 'react'; + +type ToggleProps = { + enabled?: boolean; + defaultEnabled?: boolean; + onChange?: (enabled: boolean) => void; + children?: ReactNode; +}; + +export const Toggle = ({ + defaultEnabled = false, + enabled, + onChange, + children, +}: ToggleProps) => { + const [internalEnabled, setInternalEnabled] = useState(defaultEnabled); + + const isControlled = enabled !== undefined; + const currentState = isControlled ? enabled : internalEnabled; + + const handleToggle = () => { + if (isControlled) { + onChange?.(!currentState); + } else { + setInternalEnabled(!internalEnabled); + } + }; + + const label = children ? children : currentState ? 'On' : 'Off'; + + return ( + + ); +}; diff --git a/src/demo/pp/PostList.tsx b/src/demo/pp/PostList.tsx new file mode 100644 index 0000000..a8efd00 --- /dev/null +++ b/src/demo/pp/PostList.tsx @@ -0,0 +1,38 @@ +import { useEffect, useState } from 'react'; +import { useService } from './ServiceProvider'; + +type Post = { + id: number; + title: string; +}; + +export const PostList = () => { + const { api, logger } = useService(); + const [posts, setPosts] = useState([]); + + const fetchPosts = async () => { + try { + const posts = await api.get('/posts'); + setPosts(posts); + logger.log(`Posts fetched successfully: Total ${posts.length} posts`); + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + logger.error(`Failed to fetch posts: ${message}`); + } + }; + + useEffect(() => { + fetchPosts(); + }, []); + + return ( +
+

Post List

+
    + {posts.map((post) => ( +
  • {post.title}
  • + ))} +
+
+ ); +}; diff --git a/src/demo/pp/ServiceProvider.tsx b/src/demo/pp/ServiceProvider.tsx new file mode 100644 index 0000000..a9f84d9 --- /dev/null +++ b/src/demo/pp/ServiceProvider.tsx @@ -0,0 +1,33 @@ +import { createContext, PropsWithChildren, useContext } from 'react'; +import { ApiClient, AxiosApiClient, FetchApiClient } from './apiClient'; +import { ConsoleLogger, Logger } from './logger'; + +type ServiceProviderContext = { + api: ApiClient; + logger: Logger; +}; + +const ServiceContext = createContext(null); + +export const ServiceProvider = ({ children }: PropsWithChildren) => { + return ( + + {children} + + ); +}; + +export const useService = () => { + const context = useContext(ServiceContext); + + if (!context) { + throw new Error('useService must be used within a ServiceProvider'); + } + + return context; +}; diff --git a/src/demo/pp/UserList.tsx b/src/demo/pp/UserList.tsx new file mode 100644 index 0000000..5efc52a --- /dev/null +++ b/src/demo/pp/UserList.tsx @@ -0,0 +1,42 @@ +import { useEffect, useState } from 'react'; +import { useService } from './ServiceProvider'; + +type User = { + id: number; + name: string; + email: string; +}; + +export const UserList = () => { + const { api, logger } = useService(); + const [users, setUsers] = useState([]); + + const fetchUsers = async () => { + try { + const users = await api.get('/users'); + setUsers(users); + logger.log(`Users fetched successfully: Total ${users.length} users`); + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + logger.error(`Failed to fetch users: ${message}`); + } + }; + + useEffect(() => { + fetchUsers(); + }, []); + + return ( +
+

User List

+
    + {users.map((user) => ( +
  • +

    {user.name}

    +

    {user.email}

    +
  • + ))} +
+
+ ); +}; diff --git a/src/demo/pp/apiClient.ts b/src/demo/pp/apiClient.ts new file mode 100644 index 0000000..7eb1e98 --- /dev/null +++ b/src/demo/pp/apiClient.ts @@ -0,0 +1,44 @@ +import axios from 'axios'; + +export type ApiClient = { + baseUrl: string; + get: (path: string) => Promise; + post: ( + path: string, + data: T + ) => Promise; +}; + +export const FetchApiClient: ApiClient = { + baseUrl: 'https://jsonplaceholder.typicode.com', + + get: async (path: string) => { + const response = await fetch(`${FetchApiClient.baseUrl}${path}`); + return response.json() as Promise; + }, + + post: async (path: string, data: T) => { + const response = await fetch(`${FetchApiClient.baseUrl}${path}`, { + method: 'POST', + body: JSON.stringify(data), + }); + return response.json() as Promise; + }, +}; + +export const AxiosApiClient: ApiClient = { + baseUrl: 'https://jsonplaceholder.typicode.com', + + get: async (path: string) => { + const response = await axios.get(`${AxiosApiClient.baseUrl}${path}`); + return response.data; + }, + + post: async (path: string, data: T) => { + const response = await axios.post( + `${AxiosApiClient.baseUrl}${path}`, + data + ); + return response.data; + }, +}; diff --git a/src/demo/pp/logger.ts b/src/demo/pp/logger.ts new file mode 100644 index 0000000..1623b10 --- /dev/null +++ b/src/demo/pp/logger.ts @@ -0,0 +1,13 @@ +export type Logger = { + log: (message: string) => void; + error: (message: string) => void; +}; + +export const ConsoleLogger: Logger = { + log: (message: string) => { + console.log(message); + }, + error: (message: string) => { + console.error(message); + }, +}; diff --git a/src/examples/lecture_3/EX1_Toggle.tsx b/src/examples/lecture_3/EX1_Toggle.tsx new file mode 100644 index 0000000..4f29678 --- /dev/null +++ b/src/examples/lecture_3/EX1_Toggle.tsx @@ -0,0 +1,39 @@ +import { useState } from 'react'; + +type ToggleProps = { + enabled?: boolean; + onChange?: (enabled: boolean) => void; + children?: React.ReactNode; +}; + +export const Toggle = ({ enabled, onChange, children }: ToggleProps) => { + const [internalState, setInternalState] = useState(false); + + const isControlled = enabled !== undefined; + + const currentState = isControlled ? enabled : internalState; + + const handleToggle = () => { + if (isControlled) { + onChange?.(!currentState); + } else { + setInternalState(!currentState); + } + }; + + const label = children ? children : currentState ? 'On' : 'Off'; + + return ( + + ); +}; diff --git a/src/examples/lecture_3/EX2_InputField.tsx b/src/examples/lecture_3/EX2_InputField.tsx new file mode 100644 index 0000000..9c867ec --- /dev/null +++ b/src/examples/lecture_3/EX2_InputField.tsx @@ -0,0 +1,78 @@ +import { useState } from 'react'; + +type InputFieldProps = { + name: string; + label?: string; + placeholder?: string; + value?: string; + onChange?: (e: React.ChangeEvent) => void; + ref?: React.RefObject; +}; + +export const InputField = ({ + name, + label, + placeholder, + value, + onChange, + ref, +}: InputFieldProps) => { + const [internalState, setInternalState] = useState(''); + + const isControlled = value !== undefined; + const currentValue = isControlled ? value : internalState; + + const handleChange = (e: React.ChangeEvent) => { + if (isControlled) { + onChange?.(e); + } else { + setInternalState(e.target.value); + } + }; + + return ( +
+ {label && ( + + )} + +
+ ); +}; + +export const FormExample = () => { + const [name, setName] = useState(''); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + console.log(name); + }; + + return ( +
+

Contact Form

+ setName(e.target.value)} + /> + + + ); +}; diff --git a/src/examples/lecture_3/EX3_Accordion.tsx b/src/examples/lecture_3/EX3_Accordion.tsx new file mode 100644 index 0000000..9ad7e55 --- /dev/null +++ b/src/examples/lecture_3/EX3_Accordion.tsx @@ -0,0 +1,98 @@ +import { createContext, PropsWithChildren, useContext, useState } from 'react'; + +type AccordionContext = { + isOpen: boolean; + toggle: () => void; +}; + +const AccordionContext = createContext(null); + +type AccordionProps = PropsWithChildren<{ + isOpen?: boolean; + defaultOpen?: boolean; + onToggle?: (isOpen: boolean) => void; +}>; + +export const Accordion = ({ + isOpen, + defaultOpen = false, + onToggle, + children, +}: AccordionProps) => { + const [isOpenInternal, setIsOpenInternal] = useState(defaultOpen); + + const isControlled = isOpen !== undefined; + + const currentIsOpen = isControlled ? isOpen : isOpenInternal; + const handleToggle = () => { + if (isControlled) { + onToggle?.(isOpenInternal); + } else { + setIsOpenInternal((prev) => !prev); + } + }; + + return ( + +
{children}
+
+ ); +}; + +const useAccordion = () => { + const context = useContext(AccordionContext); + if (!context) { + throw new Error('useAccordion must be used within an Accordion'); + } + return context; +}; + +const AccordionHeader = ({ + children, + className = '', +}: PropsWithChildren & { className?: string }) => { + const { isOpen, toggle } = useAccordion(); + return ( + + ); +}; + +const AccordionContent = ({ + children, + className = '', +}: PropsWithChildren & { className?: string }) => { + const { isOpen } = useAccordion(); + return isOpen ? ( +
+ {children} +
+ ) : null; +}; + +Accordion.Header = AccordionHeader; +Accordion.Content = AccordionContent; diff --git a/src/examples/lecture_3/EX4_DI/ServiceProvider.tsx b/src/examples/lecture_3/EX4_DI/ServiceProvider.tsx new file mode 100644 index 0000000..0ef0358 --- /dev/null +++ b/src/examples/lecture_3/EX4_DI/ServiceProvider.tsx @@ -0,0 +1,32 @@ +import { PropsWithChildren, useContext } from 'react'; +import { createContext } from 'react'; +import { ApiClient, FetchApiClient } from './apiClient'; +import { consoleLogger, Logger } from './logger'; + +type ServiceProviderContext = { + apiClient: ApiClient; + logger: Logger; +}; + +const ServiceContext = createContext(null); + +export const ServiceProvider = ({ children }: PropsWithChildren) => { + return ( + + {children} + + ); +}; + +export const useService = () => { + const context = useContext(ServiceContext); + if (!context) { + throw new Error('useService must be used within a ServiceProvider'); + } + return context; +}; diff --git a/src/examples/lecture_3/EX4_DI/Users.tsx b/src/examples/lecture_3/EX4_DI/Users.tsx new file mode 100644 index 0000000..0781b91 --- /dev/null +++ b/src/examples/lecture_3/EX4_DI/Users.tsx @@ -0,0 +1,38 @@ +import { useEffect, useState } from 'react'; +import { useService } from './ServiceProvider'; + +type User = { + id: number; + name: string; + email: string; +}; + +export const Users = () => { + const { apiClient, logger } = useService(); + const [users, setUsers] = useState([]); + + const fetchUsers = async () => { + try { + const users = await apiClient.get('/users'); + logger.log(`Fetched ${users.length} users`); + setUsers(users); + } catch (error) { + logger.error(`Error fetching users: ${error}`); + } + }; + + useEffect(() => { + fetchUsers(); + }, [fetchUsers]); + + return ( +
+

Users

+
    + {users.map((user) => ( +
  • {user.name}
  • + ))} +
+
+ ); +}; diff --git a/src/examples/lecture_3/EX4_DI/apiClient.ts b/src/examples/lecture_3/EX4_DI/apiClient.ts new file mode 100644 index 0000000..e8dff45 --- /dev/null +++ b/src/examples/lecture_3/EX4_DI/apiClient.ts @@ -0,0 +1,45 @@ +import axios from 'axios'; + +export type ApiClient = { + baseUrl: string; + get(url: string): Promise; + post(url: string, data: T): Promise; +}; + +export const FetchApiClient: ApiClient = { + baseUrl: 'https://jsonplaceholder.typicode.com', + + get: async (url: string): Promise => { + const response = await fetch(`${FetchApiClient.baseUrl}${url}`); + return response.json(); + }, + + post: async ( + url: string, + data: T + ): Promise => { + const response = await fetch(`${FetchApiClient.baseUrl}${url}`, { + method: 'POST', + body: JSON.stringify(data), + }); + return response.json(); + }, +}; + +export const AxiosApiClient: ApiClient = { + baseUrl: 'https://jsonplaceholder.typicode.com', + get: async (url: string): Promise => { + const response = await axios.get(`${AxiosApiClient.baseUrl}${url}`); + return response.data; + }, + post: async ( + url: string, + data: T + ): Promise => { + const response = await axios.post( + `${AxiosApiClient.baseUrl}${url}`, + data + ); + return response.data; + }, +}; diff --git a/src/examples/lecture_3/EX4_DI/logger.ts b/src/examples/lecture_3/EX4_DI/logger.ts new file mode 100644 index 0000000..a192709 --- /dev/null +++ b/src/examples/lecture_3/EX4_DI/logger.ts @@ -0,0 +1,13 @@ +export type Logger = { + log: (message: string) => void; + error: (message: string) => void; +}; + +export const consoleLogger: Logger = { + log: (message: string) => { + console.log(message); + }, + error: (message: string) => { + console.error(message); + }, +};