diff --git a/.gitignore b/.gitignore index a547bf3..ebcc2a1 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ dist-ssr *.njsproj *.sln *.sw? + +# Sentry Config File +.env.sentry-build-plugin 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..2630d8d 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,16 @@ "preview": "vite preview" }, "dependencies": { + "@sentry/browser": "^9.1.0", + "@sentry/react": "^9.1.0", + "@sentry/vite-plugin": "^3.2.0", + "@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", + "swr": "^2.3.2", + "tsyringe": "^4.8.0" }, "devDependencies": { "@eslint/js": "^9.19.0", @@ -19,12 +27,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..ab5bd9a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,36 @@ importers: .: dependencies: + '@sentry/browser': + specifier: ^9.1.0 + version: 9.1.0 + '@sentry/react': + specifier: ^9.1.0 + version: 9.1.0(react@19.0.0) + '@sentry/vite-plugin': + specifier: ^3.2.0 + version: 3.2.0 + '@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 + swr: + specifier: ^2.3.2 + version: 2.3.2(react@19.0.0) + tsyringe: + specifier: ^4.8.0 + version: 4.8.0 devDependencies: '@eslint/js': specifier: ^9.19.0 @@ -29,34 +53,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 +544,172 @@ packages: cpu: [x64] os: [win32] + '@sentry-internal/browser-utils@9.1.0': + resolution: {integrity: sha512-S1uT+kkFlstWpwnaBTIJSwwAID8PS3aA0fIidOjNezeoUE5gOvpsjDATo9q+sl6FbGWynxMz6EnYSrq/5tuaBQ==} + engines: {node: '>=18'} + + '@sentry-internal/feedback@9.1.0': + resolution: {integrity: sha512-jTDCqkqH3QDC8m9WO4mB06hqnBRsl3p7ozoh0E774UvNB6blOEZjShhSGMMEy5jbbJajPWsOivCofUtFAwbfGw==} + engines: {node: '>=18'} + + '@sentry-internal/replay-canvas@9.1.0': + resolution: {integrity: sha512-gxredVe+mOgfNqDJ3dTLiRON3FK1rZ8d0LHp7TICK/umLkWFkuso0DbNeyKU+3XCEjCr9VM7ZRqTDMzmY6zyVg==} + engines: {node: '>=18'} + + '@sentry-internal/replay@9.1.0': + resolution: {integrity: sha512-E2xrUoms90qvm0BVOuaZ8QfkMoTUEgoIW/35uOeaqNcL7uOIj8c5cSEQQKit2Dr7CL6W+Ci5c6Khdyd5C0NL5w==} + engines: {node: '>=18'} + + '@sentry/babel-plugin-component-annotate@3.2.0': + resolution: {integrity: sha512-Sg7nLRP1yiJYl/KdGGxYGbjvLq5rswyeB5yESgfWX34XUNZaFgmNvw4pU/QEKVeYgcPyOulgJ+y80ewujyffTA==} + engines: {node: '>= 14'} + + '@sentry/browser@9.1.0': + resolution: {integrity: sha512-G55e5j77DqRW3LkalJLAjRRfuyKrjHaKTnwIYXa6ycO+Q1+l14pEUxu+eK5Abu2rtSdViwRSb5/G6a/miSUlYA==} + engines: {node: '>=18'} + + '@sentry/bundler-plugin-core@3.2.0': + resolution: {integrity: sha512-Q/ogVylue3XaFawyIxzuiic+7Dp4w63eJtRtVH8VBebNURyJ/re4GVoP1QNGccE1R243tXY1y2GiwqiJkAONOg==} + engines: {node: '>= 14'} + + '@sentry/cli-darwin@2.41.1': + resolution: {integrity: sha512-7pS3pu/SuhE6jOn3wptstAg6B5nUP878O6s+2svT7b5fKNfYUi/6NPK6dAveh2Ca0rwVq40TO4YFJabWMgTpdQ==} + engines: {node: '>=10'} + os: [darwin] + + '@sentry/cli-linux-arm64@2.41.1': + resolution: {integrity: sha512-EzYCEnnENBnS5kpNW+2dBcrPZn1MVfywh2joGVQZTpmgDL5YFJ59VOd+K0XuEwqgFI8BSNI14KXZ75s4DD1/Vw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux, freebsd] + + '@sentry/cli-linux-arm@2.41.1': + resolution: {integrity: sha512-wNUvquD6qjOCczvuBGf9OiD29nuQ6yf8zzfyPJa5Bdx1QXuteKsKb6HBrMwuIR3liyuu0duzHd+H/+p1n541Hg==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux, freebsd] + + '@sentry/cli-linux-i686@2.41.1': + resolution: {integrity: sha512-urpQCWrdYnSAsZY3udttuMV88wTJzKZL10xsrp7sjD/Hd+O6qSLVLkxebIlxts70jMLLFHYrQ2bkRg5kKuX6Fg==} + engines: {node: '>=10'} + cpu: [x86, ia32] + os: [linux, freebsd] + + '@sentry/cli-linux-x64@2.41.1': + resolution: {integrity: sha512-ZqpYwHXAaK4MMEFlyaLYr6mJTmpy9qP6n30jGhLTW7kHKS3s6GPLCSlNmIfeClrInEt0963fM633ZRnXa04VPw==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux, freebsd] + + '@sentry/cli-win32-i686@2.41.1': + resolution: {integrity: sha512-AuRimCeVsx99DIOr9cwdYBHk39tlmAuPDdy2r16iNzY0InXs4xOys4gGzM7N4vlFQvFkzuc778Su0HkfasgprA==} + engines: {node: '>=10'} + cpu: [x86, ia32] + os: [win32] + + '@sentry/cli-win32-x64@2.41.1': + resolution: {integrity: sha512-6JcPvXGye61+wPp0xdzfc2YLE/Dcud8JdaK8VxLM3b/8+Em7E+UyliDu3uF8+YGUqizY5JYTd3fs17DC8DZhLw==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@sentry/cli@2.41.1': + resolution: {integrity: sha512-0GVmDiTV7R1492wkVY4bGcfC0fSmRmQjuxaaPI8CIV9B2VP9pBVCUizi1mevXaaE4I3fM60LI+XYrKFEneuVog==} + engines: {node: '>= 10'} + hasBin: true + + '@sentry/core@9.1.0': + resolution: {integrity: sha512-djWEzSBpMgqdF3GQuxO+kXCUX+Mgq42G4Uah/HSUBvPDHKipMmyWlutGRoFyVPPOnCDgpHu3wCt83wbpEyVmDw==} + engines: {node: '>=18'} + + '@sentry/react@9.1.0': + resolution: {integrity: sha512-aP2sXHH+erbomuzU762ktg340IGDh8zD7ueuqwBwGu98fhCpTYsLXiS85I29tUvPLljwNU9puLPmxbgW4iZ2tQ==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.14.0 || 17.x || 18.x || 19.x + + '@sentry/vite-plugin@3.2.0': + resolution: {integrity: sha512-IVBoAzZmpoX9+mnmIMq2ndxlFPoWMuYSE5Mek5zOWpYh+GbPxvkrxvM+vg0HeLH4r5v9Tm0FWcEZDgDIZqtoSg==} + engines: {node: '>= 14'} + + '@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==} @@ -603,6 +802,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -610,15 +813,36 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + 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} + hasBin: true + 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==} balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -634,6 +858,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'} @@ -645,6 +873,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -652,6 +884,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==} @@ -677,9 +913,50 @@ 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'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + + 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==} + enhanced-resolve@5.18.1: + 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'} @@ -787,15 +1064,45 @@ 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==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 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'} @@ -804,6 +1111,10 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@9.3.5: + resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} + engines: {node: '>=16 || 14 >=14.17'} + globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -816,6 +1127,13 @@ 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==} + graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} @@ -823,12 +1141,31 @@ 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==} hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -841,6 +1178,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -856,6 +1197,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 +1234,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'} @@ -896,9 +1305,20 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string@0.30.8: + resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} + engines: {node: '>=12'} + + 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'} @@ -907,13 +1327,33 @@ 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==} + minimatch@8.0.4: + resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} + engines: {node: '>=16 || 14 >=14.17'} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} + minipass@4.2.8: + resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} + engines: {node: '>=8'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -925,9 +1365,26 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + 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'} @@ -952,6 +1409,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -959,6 +1420,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} @@ -967,6 +1431,13 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.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'} @@ -979,6 +1450,9 @@ packages: peerDependencies: react: ^19.0.0 + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -987,6 +1461,13 @@ packages: resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.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'} @@ -1035,16 +1516,38 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + swr@2.3.2: + resolution: {integrity: sha512-RosxFpiabojs75IwQ316DGoDRmOqtiAj0tg8wCcbEu4CiLZBs/a9QNtHV7TUfDXmmlgqij/NqzKq/eLelyv9xA==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + 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'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + ts-api-utils@2.0.1: resolution: {integrity: sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==} engines: {node: '>=18.12'} 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'} @@ -1064,6 +1567,9 @@ packages: undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + unplugin@1.0.1: + resolution: {integrity: sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==} + update-browserslist-db@1.1.2: resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==} hasBin: true @@ -1073,6 +1579,11 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-sync-external-store@1.4.0: + resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + vite@6.1.0: resolution: {integrity: sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -1113,6 +1624,19 @@ packages: yaml: optional: true + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + + webpack-virtual-modules@0.5.0: + resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1382,9 +1906,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 +2051,166 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.34.7': optional: true + '@sentry-internal/browser-utils@9.1.0': + dependencies: + '@sentry/core': 9.1.0 + + '@sentry-internal/feedback@9.1.0': + dependencies: + '@sentry/core': 9.1.0 + + '@sentry-internal/replay-canvas@9.1.0': + dependencies: + '@sentry-internal/replay': 9.1.0 + '@sentry/core': 9.1.0 + + '@sentry-internal/replay@9.1.0': + dependencies: + '@sentry-internal/browser-utils': 9.1.0 + '@sentry/core': 9.1.0 + + '@sentry/babel-plugin-component-annotate@3.2.0': {} + + '@sentry/browser@9.1.0': + dependencies: + '@sentry-internal/browser-utils': 9.1.0 + '@sentry-internal/feedback': 9.1.0 + '@sentry-internal/replay': 9.1.0 + '@sentry-internal/replay-canvas': 9.1.0 + '@sentry/core': 9.1.0 + + '@sentry/bundler-plugin-core@3.2.0': + dependencies: + '@babel/core': 7.26.9 + '@sentry/babel-plugin-component-annotate': 3.2.0 + '@sentry/cli': 2.41.1 + dotenv: 16.4.7 + find-up: 5.0.0 + glob: 9.3.5 + magic-string: 0.30.8 + unplugin: 1.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@sentry/cli-darwin@2.41.1': + optional: true + + '@sentry/cli-linux-arm64@2.41.1': + optional: true + + '@sentry/cli-linux-arm@2.41.1': + optional: true + + '@sentry/cli-linux-i686@2.41.1': + optional: true + + '@sentry/cli-linux-x64@2.41.1': + optional: true + + '@sentry/cli-win32-i686@2.41.1': + optional: true + + '@sentry/cli-win32-x64@2.41.1': + optional: true + + '@sentry/cli@2.41.1': + dependencies: + https-proxy-agent: 5.0.1 + node-fetch: 2.7.0 + progress: 2.0.3 + proxy-from-env: 1.1.0 + which: 2.0.2 + optionalDependencies: + '@sentry/cli-darwin': 2.41.1 + '@sentry/cli-linux-arm': 2.41.1 + '@sentry/cli-linux-arm64': 2.41.1 + '@sentry/cli-linux-i686': 2.41.1 + '@sentry/cli-linux-x64': 2.41.1 + '@sentry/cli-win32-i686': 2.41.1 + '@sentry/cli-win32-x64': 2.41.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@sentry/core@9.1.0': {} + + '@sentry/react@9.1.0(react@19.0.0)': + dependencies: + '@sentry/browser': 9.1.0 + '@sentry/core': 9.1.0 + hoist-non-react-statics: 3.3.2 + react: 19.0.0 + + '@sentry/vite-plugin@3.2.0': + dependencies: + '@sentry/bundler-plugin-core': 3.2.0 + unplugin: 1.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@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 +2248,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 +2265,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 +2282,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 +2309,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 +2325,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 @@ -1658,6 +2342,12 @@ snapshots: acorn@8.14.0: {} + agent-base@6.0.2: + dependencies: + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -1669,14 +2359,41 @@ snapshots: dependencies: color-convert: 2.0.1 + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + argparse@2.0.1: {} + asynckit@0.4.0: {} + + 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 + + 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 balanced-match@1.0.2: {} + binary-extensions@2.3.0: {} + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -1697,6 +2414,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: {} @@ -1706,12 +2428,28 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + color-convert@2.0.1: dependencies: color-name: 1.1.4 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: {} @@ -1730,8 +2468,42 @@ snapshots: deep-is@0.1.4: {} + delayed-stream@1.0.0: {} + + dequal@2.0.3: {} + + detect-libc@1.0.3: {} + + dotenv@16.4.7: {} + + 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: + dependencies: + 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 @@ -1764,25 +2536,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 +2565,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 +2601,8 @@ snapshots: minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.4 + optionalDependencies: + jiti: 2.4.2 transitivePeerDependencies: - supports-color @@ -1888,11 +2662,44 @@ 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: {} + + fs.realpath@1.0.0: {} + 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 @@ -1901,22 +2708,54 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@9.3.5: + dependencies: + fs.realpath: 1.0.0 + minimatch: 8.0.4 + minipass: 4.2.8 + path-scurry: 1.11.1 + globals@11.12.0: {} globals@14.0.0: {} 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: dependencies: hermes-estree: 0.25.1 + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + ignore@5.3.2: {} import-fresh@3.3.1: @@ -1926,6 +2765,10 @@ snapshots: imurmurhash@0.1.4: {} + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + is-extglob@2.1.1: {} is-glob@4.0.3: @@ -1936,6 +2779,8 @@ snapshots: isexe@2.0.0: {} + jiti@2.4.2: {} + js-tokens@4.0.0: {} js-yaml@4.1.0: @@ -1961,16 +2806,69 @@ 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 lodash.merge@4.6.2: {} + lru-cache@10.4.3: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 + magic-string@0.30.8: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + math-intrinsics@1.1.0: {} + merge2@1.4.1: {} micromatch@4.0.8: @@ -1978,22 +2876,44 @@ 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 + minimatch@8.0.4: + dependencies: + brace-expansion: 2.0.1 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.1 + minipass@4.2.8: {} + + minipass@7.1.2: {} + ms@2.1.3: {} nanoid@3.3.8: {} natural-compare@1.4.0: {} + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + node-releases@2.0.19: {} + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -2019,10 +2939,17 @@ snapshots: path-key@3.1.1: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + picocolors@1.1.1: {} picomatch@2.3.1: {} + postcss-value-parser@4.2.0: {} + postcss@8.5.2: dependencies: nanoid: 3.3.8 @@ -2031,6 +2958,10 @@ snapshots: prelude-ls@1.2.1: {} + progress@2.0.3: {} + + proxy-from-env@1.1.0: {} + punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -2040,10 +2971,18 @@ snapshots: react: 19.0.0 scheduler: 0.25.0 + react-is@16.13.1: {} + react-refresh@0.14.2: {} react@19.0.0: {} + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + reflect-metadata@0.2.2: {} + resolve-from@4.0.0: {} reusify@1.0.4: {} @@ -2097,24 +3036,42 @@ snapshots: dependencies: has-flag: 4.0.0 + swr@2.3.2(react@19.0.0): + dependencies: + dequal: 2.0.3 + react: 19.0.0 + use-sync-external-store: 1.4.0(react@19.0.0) + + tailwindcss@4.0.6: {} + + tapable@2.2.1: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + tr46@0.0.3: {} + ts-api-utils@2.0.1(typescript@5.7.3): 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 - 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 @@ -2123,6 +3080,13 @@ snapshots: undici-types@6.20.0: {} + unplugin@1.0.1: + dependencies: + acorn: 8.14.0 + chokidar: 3.6.0 + webpack-sources: 3.2.3 + webpack-virtual-modules: 0.5.0 + update-browserslist-db@1.1.2(browserslist@4.24.4): dependencies: browserslist: 4.24.4 @@ -2133,7 +3097,11 @@ snapshots: dependencies: punycode: 2.3.1 - vite@6.1.0(@types/node@22.13.4): + use-sync-external-store@1.4.0(react@19.0.0): + dependencies: + react: 19.0.0 + + 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 +3109,19 @@ snapshots: optionalDependencies: '@types/node': 22.13.4 fsevents: 2.3.3 + jiti: 2.4.2 + lightningcss: 1.29.1 + + webidl-conversions@3.0.1: {} + + webpack-sources@3.2.3: {} + + webpack-virtual-modules@0.5.0: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.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..978c9fe 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,35 +1,43 @@ -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 { Suspense } from 'react'; +import { UserList, UserSkeleton } from './examples/lecture_5/UserList'; +import { fetchUsers, fetchPosts } from './examples/lecture_5/lib'; +import { PostList, PostSkeleton } from './examples/lecture_5/PostList'; +import { ErrorBoundary } from './examples/lecture_5/ErrorBoundary'; +import { ErrorFallback } from './examples/lecture_5/ErrorFallback'; + +export default function App() { + return ( +
+
+ +
+ {/*
*/} + {/* } + > + }> + + + */} + + +
+ } + > + }> + + + + {/*
*/} + + ); +} 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/App_lecture1.tsx b/src/App_lecture1.tsx new file mode 100644 index 0000000..e04cc32 --- /dev/null +++ b/src/App_lecture1.tsx @@ -0,0 +1,70 @@ +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 ( +
+
+ ( +
+

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/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/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/App_lecture3.tsx b/src/App_lecture3.tsx new file mode 100644 index 0000000..23e793f --- /dev/null +++ b/src/App_lecture3.tsx @@ -0,0 +1,56 @@ +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 [selectedItems, setSelectedItems] = useState([]); + + return ( + +
+
+ {items.map((item) => ( + + setSelectedItems((prev) => + prev.includes(item.id) + ? prev.filter((id) => id !== item.id) + : [...prev, item.id] + ) + } + > + {item.label} + {item.content} + + ))} +
+
+ + +
+
+
+ ); +} 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/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/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/custom-hook/Counter.tsx b/src/demo/custom-hook/Counter.tsx new file mode 100644 index 0000000..652c54e --- /dev/null +++ b/src/demo/custom-hook/Counter.tsx @@ -0,0 +1,102 @@ +import { useState } from 'react'; + +export const BadCounter = () => { + const [count, setCount] = useState(0); + + const increment = () => setCount((prev) => prev + 1); + const decrement = () => setCount((prev) => prev - 1); + const reset = () => setCount(0); + + return ( +
+

Count: {count}

+
+ + + +
+
+ ); +}; + +const useCounter = (initialState = 0) => { + const [count, setCount] = useState(initialState); + + const increment = () => setCount((prev) => prev + 1); + const decrement = () => setCount((prev) => prev - 1); + const reset = () => setCount(initialState); + + return { count, increment, decrement, reset }; +}; + +const Counter = () => { + const { count, increment, decrement, reset } = useCounter(); + const { + count: count2, + increment: increment2, + decrement: decrement2, + reset: reset2, + } = useCounter(10); + + return ( +
+

Count: {count}

+
+ + + +
+

Count: {count2}

+
+ + + +
+
+ ); +}; diff --git a/src/demo/custom-hook/UserList.tsx b/src/demo/custom-hook/UserList.tsx new file mode 100644 index 0000000..5fb0c1a --- /dev/null +++ b/src/demo/custom-hook/UserList.tsx @@ -0,0 +1,28 @@ +import { useFetch } from './useFetch'; + +type User = { + id: number; + name: string; + email: string; +}; + +export const UserList = () => { + const { data, loading, error, refetch } = useFetch( + 'https://jsonplaceholder.typicode.com/users' + ); + + if (loading) return
Loading...
; + if (error) return
Error: {error}
; + + return ( +
+

User List

+ +
    + {data?.map((user) => ( +
  • {user.name}
  • + ))} +
+
+ ); +}; diff --git a/src/demo/custom-hook/useClickOutside.tsx b/src/demo/custom-hook/useClickOutside.tsx new file mode 100644 index 0000000..5c39c0a --- /dev/null +++ b/src/demo/custom-hook/useClickOutside.tsx @@ -0,0 +1,67 @@ +import { useEffect, useRef, useState } from 'react'; + +export const useClickOutside = (cb: () => void) => { + const ref = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (ref.current && !ref.current.contains(event.target as Node)) { + cb(); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [cb]); + + return ref; +}; + +export const Dropdown = () => { + const [open, setOpen] = useState(false); + const dropdownRef = useClickOutside(() => setOpen(false)); + + return ( +
+ + {open && ( +
+
    +
  • + Option 1 +
  • +
  • + Option 2 +
  • +
  • + Option 3 +
  • +
+
+ )} +
+ ); +}; diff --git a/src/demo/custom-hook/useDebounce.tsx b/src/demo/custom-hook/useDebounce.tsx new file mode 100644 index 0000000..7c82049 --- /dev/null +++ b/src/demo/custom-hook/useDebounce.tsx @@ -0,0 +1,37 @@ +import { useEffect, useState } from 'react'; + +export const useDebounce = (value: T, delay = 500): T => { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const timeoutId = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => clearTimeout(timeoutId); + }, [value, delay]); + + return debouncedValue; +}; + +export const SearchBar = () => { + const [search, setSearch] = useState(''); + const debouncedSearch = useDebounce(search, 300); + + useEffect(() => { + if (debouncedSearch) { + // DO API Call + console.log('Searching for:', debouncedSearch); + } + }, [debouncedSearch]); + + return ( + setSearch(e.target.value)} + placeholder='Search...' + className='border border-gray-300 rounded-md p-2' + /> + ); +}; diff --git a/src/demo/custom-hook/useFetch.ts b/src/demo/custom-hook/useFetch.ts new file mode 100644 index 0000000..828f2c0 --- /dev/null +++ b/src/demo/custom-hook/useFetch.ts @@ -0,0 +1,42 @@ +import axios from 'axios'; +import { useEffect, useState } from 'react'; + +export const useFetch = (url: string) => { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = async () => { + setLoading(true); + + try { + const response = await axios.get(url); + setData(response.data); + setError(null); + } catch (error) { + const message = + error instanceof Error ? error.message : 'An unknown error occurred'; + setError(message); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + let isMounted = true; + if (isMounted) { + fetchData(); + } + + return () => { + isMounted = false; + }; + }, [url]); + + return { + data, + loading, + error, + refetch: fetchData, + }; +}; 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/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/demo/hook-factory/PostList.tsx b/src/demo/hook-factory/PostList.tsx new file mode 100644 index 0000000..2c19bc7 --- /dev/null +++ b/src/demo/hook-factory/PostList.tsx @@ -0,0 +1,16 @@ +import { usePosts } from './createApiHook'; + +export const UserList = () => { + const { data, loading, error } = usePosts(); + + if (loading) return
Loading...
; + if (error) return
Error: {error}
; + + return ( +
+ {data?.map((post) => ( +
{post.title}
+ ))} +
+ ); +}; diff --git a/src/demo/hook-factory/UserList.tsx b/src/demo/hook-factory/UserList.tsx new file mode 100644 index 0000000..8fbab90 --- /dev/null +++ b/src/demo/hook-factory/UserList.tsx @@ -0,0 +1,16 @@ +import { useUsers } from './createApiHook'; + +export const UserList = () => { + const { data, loading, error } = useUsers(); + + if (loading) return
Loading...
; + if (error) return
Error: {error}
; + + return ( +
+ {data?.map((user) => ( +
{user.name}
+ ))} +
+ ); +}; diff --git a/src/demo/hook-factory/createApiHook.ts b/src/demo/hook-factory/createApiHook.ts new file mode 100644 index 0000000..ef46e14 --- /dev/null +++ b/src/demo/hook-factory/createApiHook.ts @@ -0,0 +1,65 @@ +import axios from 'axios'; +import { useEffect, useState } from 'react'; + +export const createApiHook = (url: string) => { + return () => { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = async () => { + setLoading(true); + + try { + const response = await axios.get(url); + setData(response.data); + setError(null); + } catch (error) { + const message = + error instanceof Error ? error.message : 'An unknown error occurred'; + setError(message); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + let isMounted = true; + if (isMounted) { + fetchData(); + } + + return () => { + isMounted = false; + }; + }, [url]); + + return { + data, + loading, + error, + refetch: fetchData, + }; + }; +}; + +export type User = { + id: number; + name: string; + email: string; +}; + +export const useUsers = createApiHook( + 'https://jsonplaceholder.typicode.com/users' +); + +export type Post = { + id: number; + title: string; + body: string; + userId: number; +}; + +export const usePosts = createApiHook( + 'https://jsonplaceholder.typicode.com/posts' +); diff --git a/src/demo/hook-factory/createPersistedState.ts b/src/demo/hook-factory/createPersistedState.ts new file mode 100644 index 0000000..6da38a3 --- /dev/null +++ b/src/demo/hook-factory/createPersistedState.ts @@ -0,0 +1,25 @@ +import { useEffect, useState } from 'react'; + +export const createPersistedState = (key: string, initialValue: T) => { + return () => { + const [state, setState] = useState(() => { + const storedValue = localStorage.getItem(key); + return storedValue ? JSON.parse(storedValue) : initialValue; + }); + + useEffect(() => { + localStorage.setItem(key, JSON.stringify(state)); + }, [state]); + + return [state, setState] as const; + }; +}; + +type CartItem = { + id: number; + name: string; + price: number; + quantity: number; +}; + +export const useCartState = createPersistedState('cart', []); diff --git a/src/demo/hook-factory/createWebSocketHook.ts b/src/demo/hook-factory/createWebSocketHook.ts new file mode 100644 index 0000000..5eac603 --- /dev/null +++ b/src/demo/hook-factory/createWebSocketHook.ts @@ -0,0 +1,50 @@ +import { useEffect, useState } from 'react'; + +type CreateWebSocketOptions = { + url?: string; +}; + +export const createWebSocketHook = ( + eventName: string, + options: CreateWebSocketOptions = {} +) => { + const url = options.url || 'wss://stacklearner.com/ws'; + return () => { + const [data, setData] = useState(null); + + useEffect(() => { + const ws = new WebSocket(url); + + ws.onopen = () => { + ws.send(JSON.stringify({ subscribe: eventName })); + }; + + ws.onmessage = (event) => setData(JSON.parse(event.data)); + + return () => ws.close(); + }, [eventName]); + + return data; + }; +}; + +type Message = { + id: number; + content: string; + timestamp: number; +}; + +export const useChatMessages = createWebSocketHook('chatMessage'); + +type Notification = { + id: number; + message: string; + timestamp: number; +}; + +export const useNotifications = createWebSocketHook( + 'notification', + { + url: 'wss://stacklearner.com/ws', + } +); 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_1/EX1_SRP.tsx b/src/examples/lecture_1/EX1_SRP.tsx new file mode 100644 index 0000000..6002d51 --- /dev/null +++ b/src/examples/lecture_1/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/lecture_1/EX2_SOC.tsx b/src/examples/lecture_1/EX2_SOC.tsx new file mode 100644 index 0000000..210d755 --- /dev/null +++ b/src/examples/lecture_1/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/lecture_1/EX3_ProjectDashboard.tsx b/src/examples/lecture_1/EX3_ProjectDashboard.tsx new file mode 100644 index 0000000..dc3f0ed --- /dev/null +++ b/src/examples/lecture_1/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/lecture_1/EX4_HOC.tsx b/src/examples/lecture_1/EX4_HOC.tsx new file mode 100644 index 0000000..ccae9b6 --- /dev/null +++ b/src/examples/lecture_1/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/lecture_1/EX5_RP1.tsx b/src/examples/lecture_1/EX5_RP1.tsx new file mode 100644 index 0000000..9c3c0a6 --- /dev/null +++ b/src/examples/lecture_1/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/lecture_1/EX6_RP2.tsx b/src/examples/lecture_1/EX6_RP2.tsx new file mode 100644 index 0000000..27b818d --- /dev/null +++ b/src/examples/lecture_1/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/lecture_1/EX7_RP3.tsx b/src/examples/lecture_1/EX7_RP3.tsx new file mode 100644 index 0000000..13f45be --- /dev/null +++ b/src/examples/lecture_1/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/lecture_1/EX8_RP4.tsx b/src/examples/lecture_1/EX8_RP4.tsx new file mode 100644 index 0000000..37d11a0 --- /dev/null +++ b/src/examples/lecture_1/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/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..2d89f93 --- /dev/null +++ b/src/examples/lecture_2/EX3_StepWizard.tsx @@ -0,0 +1,133 @@ +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, + }); +}; + + +/** + * 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 new file mode 100644 index 0000000..36268f5 --- /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 = 5000, + 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..b4dcad2 --- /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.List = TabList; +Tabs.Tab = Tab; +Tabs.Pane = 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; 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); + }, +}; diff --git a/src/examples/lecture_4/EX1_BasicHook.tsx b/src/examples/lecture_4/EX1_BasicHook.tsx new file mode 100644 index 0000000..c4a6a0a --- /dev/null +++ b/src/examples/lecture_4/EX1_BasicHook.tsx @@ -0,0 +1,36 @@ +import { useState } from 'react'; + +// Bad Code +export const BadCounter = () => { + const [count, setCount] = useState(0); + + const increment = () => setCount((prev) => prev + 1); + const decrement = () => setCount((prev) => prev - 1); + const reset = () => setCount(0); + + return ( +
+

Count: {count}

+
+ + + +
+
+ ); +}; diff --git a/src/examples/lecture_4/EX2_UseFetch.ts b/src/examples/lecture_4/EX2_UseFetch.ts new file mode 100644 index 0000000..16387f1 --- /dev/null +++ b/src/examples/lecture_4/EX2_UseFetch.ts @@ -0,0 +1,35 @@ +import axios from 'axios'; +import { useEffect, useState } from 'react'; + +export const useFetch = (url: string) => { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = async () => { + setLoading(true); + try { + const response = await axios.get(url); + setData(response.data); + setError(null); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'An unknown error occurred'; + setError(errorMessage); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + let isMounted = true; + if (isMounted) { + fetchData(); + } + return () => { + isMounted = false; + }; + }, [url]); + + return { data, loading, error, refetch: fetchData }; +}; diff --git a/src/examples/lecture_4/EX3_UseDebounce.tsx b/src/examples/lecture_4/EX3_UseDebounce.tsx new file mode 100644 index 0000000..1f339a7 --- /dev/null +++ b/src/examples/lecture_4/EX3_UseDebounce.tsx @@ -0,0 +1,34 @@ +import { useState, useEffect } from 'react'; + +export const useDebounce = (value: T, delay: number = 500): T => { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const timeoutId = setTimeout(() => { + setDebouncedValue(value); + }, delay); + return () => clearTimeout(timeoutId); + }, [value, delay]); + + return debouncedValue; +}; + +export const SearchBar = () => { + const [search, setSearch] = useState(''); + const debouncedSearch = useDebounce(search); + + useEffect(() => { + if (debouncedSearch) { + console.log('Fetching results for:', debouncedSearch); + } + }, [debouncedSearch]); + + return ( + setSearch(e.target.value)} + placeholder='Search...' + /> + ); +}; diff --git a/src/examples/lecture_4/EX4_UseClickOutside.tsx b/src/examples/lecture_4/EX4_UseClickOutside.tsx new file mode 100644 index 0000000..2876583 --- /dev/null +++ b/src/examples/lecture_4/EX4_UseClickOutside.tsx @@ -0,0 +1,67 @@ +import { useEffect, useRef, useState } from 'react'; + +export const useClickOutside = (cb: () => void) => { + const ref = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (ref.current && !ref.current.contains(event.target as Node)) { + cb(); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [cb]); + + return ref; +}; + +export const Dropdown = () => { + const [open, setOpen] = useState(false); + const dropdownRef = useClickOutside(() => setOpen(false)); + + return ( +
+ + {open && ( +
+
    +
  • + Option 1 +
  • +
  • + Option 2 +
  • +
  • + Option 3 +
  • +
+
+ )} +
+ ); +}; diff --git a/src/examples/lecture_4/EX5_BadCode.tsx b/src/examples/lecture_4/EX5_BadCode.tsx new file mode 100644 index 0000000..4ecc2e2 --- /dev/null +++ b/src/examples/lecture_4/EX5_BadCode.tsx @@ -0,0 +1,91 @@ +import { useState, useEffect, useRef } from 'react'; +import axios from 'axios'; + +type User = { + id: number; + name: string; + username: string; + email: string; +}; + +type Theme = 'light' | 'dark'; + +// Messy component with everything inside +export const UserSearch = () => { + const [users, setUsers] = useState([]); + const [search, setSearch] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [theme, setTheme] = useState('light'); + const dropdownRef = useRef(null); + const [isDropdownOpen, setDropdownOpen] = useState(false); + + // Fetch Users API Call + useEffect(() => { + setLoading(true); + axios + .get('https://jsonplaceholder.typicode.com/users') + .then((res) => setUsers(res.data)) + .catch((err) => setError(err.message)) + .finally(() => setLoading(false)); + }, []); + + // Debounce Search + useEffect(() => { + const handler = setTimeout(() => { + console.log('Searching:', search); + }, 500); + return () => clearTimeout(handler); + }, [search]); + + // Handle click outside dropdown + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setDropdownOpen(false); + } + } + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + // Toggle theme + const toggleTheme = () => + setTheme((prev) => (prev === 'light' ? 'dark' : 'light')); + + return ( +
+ + + setSearch(e.target.value)} + /> + + {loading &&

Loading...

} + {error &&

Error: {error}

} + +
    + {users + .filter((user) => + user.name.toLowerCase().includes(search.toLowerCase()) + ) + .map((user) => ( +
  • {user.name}
  • + ))} +
+ +
+ + {isDropdownOpen &&
Dropdown Content
} +
+
+ ); +}; diff --git a/src/examples/lecture_4/EX5_GoodCode.tsx b/src/examples/lecture_4/EX5_GoodCode.tsx new file mode 100644 index 0000000..beac3df --- /dev/null +++ b/src/examples/lecture_4/EX5_GoodCode.tsx @@ -0,0 +1,88 @@ +import { useState } from 'react'; +import { useDebounce } from './EX3_UseDebounce'; +import { useFetch } from './EX2_UseFetch'; +import { useClickOutside } from './EX4_UseClickOutside'; + +type User = { + id: number; + name: string; + username: string; + email: string; +}; + +type Theme = 'light' | 'dark'; + +// Custom hook for theme management +const useTheme = (initialTheme: Theme = 'light') => { + const [theme, setTheme] = useState(initialTheme); + const toggleTheme = () => + setTheme((prev) => (prev === 'light' ? 'dark' : 'light')); + return { theme, toggleTheme }; +}; + +// Custom hook for filtered users +const useFilteredUsers = (users: User[], searchTerm: string) => { + return users.filter((user) => + user.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); +}; + +// Clean component using custom hooks +export const UserSearch = () => { + const [search, setSearch] = useState(''); + const debouncedSearch = useDebounce(search); + const { theme, toggleTheme } = useTheme('light'); + const [isDropdownOpen, setDropdownOpen] = useState(false); + + const { + data: users = [], + loading, + error, + } = useFetch('https://jsonplaceholder.typicode.com/users'); + + const dropdownRef = useClickOutside(() => + setDropdownOpen(false) + ); + const filteredUsers = useFilteredUsers(users ?? [], debouncedSearch); + + return ( +
+
+ +
+ + {isDropdownOpen &&
Dropdown Content
} +
+
+ +
+ setSearch(e.target.value)} + className='border border-gray-300 rounded-md p-2' + /> +
+ + {loading &&

Loading...

} + {error &&

Error: {error}

} + +
    + {filteredUsers.map((user) => ( +
  • {user.name}
  • + ))} +
+
+ ); +}; diff --git a/src/examples/lecture_4/EX6_CreateAPIHook.ts b/src/examples/lecture_4/EX6_CreateAPIHook.ts new file mode 100644 index 0000000..6a7a885 --- /dev/null +++ b/src/examples/lecture_4/EX6_CreateAPIHook.ts @@ -0,0 +1,70 @@ +import axios from 'axios'; +import { useEffect, useState } from 'react'; + +export const createApiHook = (url: string) => { + return () => { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = async () => { + setLoading(true); + try { + const response = await axios.get(url); + setData(response.data); + setError(null); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'An unknown error occurred'; + setError(errorMessage); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + let isMounted = true; + if (isMounted) { + fetchData(); + } + return () => { + isMounted = false; + }; + }, [url]); + + return { data, loading, error, refetch: fetchData }; + }; +}; + +export type User = { + id: number; + name: string; + username: string; + email: string; +}; + +export const useUsers = createApiHook( + 'https://jsonplaceholder.typicode.com/users' +); + +type Post = { + id: number; + title: string; + body: string; + userId: number; +}; + +export const usePosts = createApiHook( + 'https://jsonplaceholder.typicode.com/posts' +); + +type Comment = { + id: number; + postId: number; + body: string; + userId: number; +}; + +export const useComments = createApiHook( + 'https://jsonplaceholder.typicode.com/comments' +); diff --git a/src/examples/lecture_4/EX7_CreateWebSocketHook.ts b/src/examples/lecture_4/EX7_CreateWebSocketHook.ts new file mode 100644 index 0000000..90f6d26 --- /dev/null +++ b/src/examples/lecture_4/EX7_CreateWebSocketHook.ts @@ -0,0 +1,55 @@ +import { useEffect, useState } from 'react'; + +export const createWebSocketHook = (url: string, eventName: string) => { + return () => { + const [data, setData] = useState(null); + + useEffect(() => { + const ws = new WebSocket(url); + + ws.onopen = () => { + ws.send(JSON.stringify({ subscribe: eventName })); + }; + + ws.onmessage = (event) => setData(JSON.parse(event.data)); + + return () => ws.close(); + }, [eventName]); + + return data; + }; +}; + +type Message = { + id: number; + content: string; + timestamp: string; +}; + +export const useChatMessages = createWebSocketHook( + 'wss://example.com/ws', + 'chatMessages' +); + +type Notification = { + id: number; + message: string; + timestamp: string; +}; + +export const useNotifications = createWebSocketHook( + 'wss://example.com/ws', + 'notifications' +); + +type StockPrice = { + id: number; + symbol: string; + price: number; + timestamp: string; +}; + +export const useStockPrices = createWebSocketHook( + 'wss://example.com/ws', + 'stockPrices' +); diff --git a/src/examples/lecture_4/EX8_CreatePersistedState.ts b/src/examples/lecture_4/EX8_CreatePersistedState.ts new file mode 100644 index 0000000..bd00bef --- /dev/null +++ b/src/examples/lecture_4/EX8_CreatePersistedState.ts @@ -0,0 +1,42 @@ +import { useEffect, useState } from 'react'; + +const createPersistedState = (key: string, initialValue: T) => { + return () => { + const [state, setState] = useState(() => { + const storedValue = localStorage.getItem(key); + return storedValue ? JSON.parse(storedValue) : initialValue; + }); + + useEffect(() => { + localStorage.setItem(key, JSON.stringify(state)); // side effect + }, [state]); + + // const persist = (state: T) => { + // setState(() => { + // localStorage.setItem(key, JSON.stringify(state)); + // return state; + // }); + // }; + + return [state, setState] as const; + }; +}; + +export const useTheme = createPersistedState('theme', 'light'); + +type User = { + id: number; + name: string; + email: string; +}; + +export const useUser = createPersistedState('user', null); + +type CartItem = { + id: number; + name: string; + price: number; + quantity: number; +}; + +export const useCart = createPersistedState('cart', []); diff --git a/src/examples/lecture_4/EX9_DI/ServiceProvider.tsx b/src/examples/lecture_4/EX9_DI/ServiceProvider.tsx new file mode 100644 index 0000000..1cff5b3 --- /dev/null +++ b/src/examples/lecture_4/EX9_DI/ServiceProvider.tsx @@ -0,0 +1,53 @@ +import { PropsWithChildren, useContext } from 'react'; +import { createContext } from 'react'; +import { ApiClient, FetchApiClient } from './apiClient'; +import { consoleLogger, Logger } from './logger'; +import useSWR, { SWRResponse } from 'swr'; + +type ServiceProviderContext = { + apiClient: ApiClient; + logger: Logger; + createFetcher: (key: string) => () => SWRResponse; +}; + +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; +}; + +const createFetcher = (key: string) => { + return () => { + const { apiClient, logger } = useService(); + const fetcher = async () => { + logger.log(`Fetching data for ${key}`); + return apiClient.get(key); + }; + + const response = useSWR(key, fetcher); // can be swapped with useQuery from react-query + + if (response.error) { + logger.error(`Error fetching data for ${key}: ${response.error}`); + } + + return response; + }; +}; diff --git a/src/examples/lecture_4/EX9_DI/Users.tsx b/src/examples/lecture_4/EX9_DI/Users.tsx new file mode 100644 index 0000000..7845418 --- /dev/null +++ b/src/examples/lecture_4/EX9_DI/Users.tsx @@ -0,0 +1,26 @@ +import { useService } from './ServiceProvider'; + +type User = { + id: number; + name: string; + email: string; +}; + +export const Users = () => { + const { createFetcher } = useService(); + const useFetchUsers = createFetcher('/users'); + const { data: users, error, isLoading } = useFetchUsers(); + + return ( +
+

Users

+ {isLoading &&

Loading...

} + {error &&

Error: {error.message}

} +
    + {users?.map((user) => ( +
  • {user.name}
  • + ))} +
+
+ ); +}; diff --git a/src/examples/lecture_4/EX9_DI/apiClient.ts b/src/examples/lecture_4/EX9_DI/apiClient.ts new file mode 100644 index 0000000..e8dff45 --- /dev/null +++ b/src/examples/lecture_4/EX9_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_4/EX9_DI/logger.ts b/src/examples/lecture_4/EX9_DI/logger.ts new file mode 100644 index 0000000..a192709 --- /dev/null +++ b/src/examples/lecture_4/EX9_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); + }, +}; diff --git a/src/examples/lecture_5/EffectSyncronization.md b/src/examples/lecture_5/EffectSyncronization.md new file mode 100644 index 0000000..dfe170b --- /dev/null +++ b/src/examples/lecture_5/EffectSyncronization.md @@ -0,0 +1,235 @@ +## Effect Synchronization + +--- + +Effect synchronization in React refers to ensuring that **side effects (such as API calls, event listeners, or subscriptions) remain in sync with the component’s state, props, and lifecycle**. It is essential because React's rendering behavior does not guarantee when or how often an effect will run. + +**Why Is Effect Synchronization Needed?** + +1. **React’s Rendering Model is Asynchronous** + - Components can **re-render multiple times** due to state updates, props changes, or context changes. + - If effects are not properly synchronized, they might **use outdated state or props**, causing bugs. +2. **Effects Can Cause Performance Issues** + - Running an effect **too often** (e.g., on every render) can cause unnecessary API calls or event bindings. + - Not cleaning up effects properly can lead to **memory leaks**, especially with event listeners and subscriptions. +3. **React’s Strict Mode Runs Effects Twice (for Dev Mode only)** + - In development, React **mounts, unmounts, and remounts** components to detect issues. + - If effects are not properly handled, **this can lead to duplicate API calls** or event bindings. + +### Key Concepts of Effect Synchronization + +1. **Dependencies and React’s Dependency Array** +React’s `useEffect` has a **dependency array** that determines when the effect should run. + + ```tsx + useEffect(() => { + console.log('Effect ran!'); + }, [someState]); // Effect runs only when 'someState' changes + ``` + + - If **no dependencies** are provided: + + → Effect runs **on every render** (bad practice unless intentional). + + - If an **empty array `[]`** is provided: + + → Effect runs **only once (on mount)**. + + - If **state or props are added to the dependency array**: + + → Effect runs **whenever that state or prop changes**. + + +1. **Stale Closures and Effect Dependencies** +A **stale closure** happens when an effect **captures outdated values** due to how JavaScript closures work. + + ```tsx + + const [count, setCount] = useState(0); + + // Incorrect + useEffect(() => { + setTimeout(() => { + alert(`Current count: ${count}`); // Always 0 due to closure + }, 3000); + }, []); // Effect only captures initial 'count' + + // Correct Approach + useEffect(() => { + const timer = setTimeout(() => { + alert(`Current count: ${count}`); // Always up-to-date + }, 3000); + + return () => clearTimeout(timer); // Cleanup to avoid memory leaks + }, [count]); // Effect runs when 'count' changes + + ``` + + +1. **Cleanup Function and Preventing Memory Leaks** +If an effect **adds an event listener, opens a WebSocket, or starts an interval**, **it should clean up when the component unmounts**. + + ```tsx + useEffect(() => { + const handleScroll = () => console.log('Scrolling...'); + + window.addEventListener('scroll', handleScroll); + + return () => { + window.removeEventListener('scroll', handleScroll); // ✅ Cleanup + }; + }, []); + ``` + + Without cleanup, event listeners and intervals **keep running even after the component is unmounted**, leading to **memory leaks and performance issues**. + +2. **Synchronizing Effects with External Systems** + + In real-world apps, we often interact with **external systems** like: + + - APIs (data fetching) + - WebSockets (real-time updates) + - Browser Events (resize, visibility change) + - State Managers (Redux, Zustand) + + ```tsx + import { useEffect, useState } from 'react'; + + function FetchData({ userId }) { + const [data, setData] = useState(null); + + useEffect(() => { + let isActive = true; // Prevent race conditions + const controller = new AbortController(); + + const fetchData = async () => { + const response = await fetch(`/api/user/${userId}`, { signal: controller.signal }); + const result = await response.json(); + if (isActive) setData(result); + }; + + fetchData(); + + return () => { + isActive = false; // ✅ Prevent setting state on unmounted component + controller.abort(); + }; + }, [userId]); // ✅ Effect runs when 'userId' changes + + return
{data ? JSON.stringify(data) : 'Loading...'}
; + } + + ``` + + - **Prevents unnecessary API calls** by only fetching data when `userId` changes. + - **Prevents race conditions** by using `isActive` flag. + - **Cleans up side effects** by stopping the fetch if the component unmounts. + +## Avoid Using useEffect + +--- + + + +Don’t use useEffect unless this is the absolute necessity + +- Use Event Handler +- You don’t need useEffect for transforming data +- You don’t need useEffect for subscribing to external store (useSyncExternalStore) +- You don’t need useEffect for fetching data (use) + +1. **Synchronizing State with Props** +Developers often use `useEffect` to synchronize state with props, which leads to **unnecessary re-renders**. + + ```tsx + + // Bad Implementation + const [userId, setUserId] = useState(props.userId); + + useEffect(() => { + setUserId(props.userId); + }, [props.userId]); + + // Good Implementation + const userId = props.userId; // ✅ No need for state duplication + ``` + + +1. **Storing Derived State** + + ```tsx + // Bad Implementation + + const [fullName, setFullName] = useState(''); + + useEffect(() => { + setFullName(`${user.firstName} ${user.lastName}`); + }, [user]); + + // Good Implementation + const fullName = useMemo(() => `${user.firstName} ${user.lastName}`, [user]); + const fullName2 = `${user.firstName} ${user.lastName}` + ``` + + +1. **Fetching Data on Component Mount** + + ```tsx + + // Bad Implementation + const [data, setData] = useState(null); + + useEffect(() => { + fetch('/api/data') + .then(res => res.json()) + .then(setData); + }, []); + + // Good Implementation + + import useSWR from 'swr'; + + const { data } = useSWR('/api/data', url => fetch(url).then(res => res.json())); + + const data = use(fetch(url).then(res => res.json())) + + ``` + + +1. **Dealing with Dom Elements** + + ```tsx + // Bad Implementation + useEffect(() => { + document.title = `User: ${user.name}`; + }, [user]); + + // Good Implementation + useLayoutEffect(() => { + document.title = `User: ${user.name}`; + }, [user]); + + // Why? useLayoutEffect ensures the title updates before painting. + ``` + + +1. **Managing State From Local Storage** + + ```tsx + + // Bad Implementation + const [theme, setTheme] = useState(''); + + useEffect(() => { + setTheme(localStorage.getItem('theme') || 'light'); + }, []); // Unnecessary effect + + // Good Implementation + const [theme, setTheme] = useState(() => localStorage.getItem('theme') || 'light'); + + ``` \ No newline at end of file diff --git a/src/examples/lecture_5/ErrorBoundary.tsx b/src/examples/lecture_5/ErrorBoundary.tsx new file mode 100644 index 0000000..1334d35 --- /dev/null +++ b/src/examples/lecture_5/ErrorBoundary.tsx @@ -0,0 +1,44 @@ +import { Component, ErrorInfo, ReactNode } from 'react'; +import * as Sentry from '@sentry/react'; + +type Props = { + children: ReactNode; + fallback?: ReactNode; +}; + +type State = { + hasError: boolean; +}; + +export class ErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError() { + return { hasError: true }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo): void { + Sentry.captureException(error, { + extra: { + errorInfo, + }, + }); + console.log('Error Capture'); + console.error(error, errorInfo); + } + + render() { + if (this.state.hasError) { + return ( + this.props.fallback ?? ( +
Something went wrong
+ ) + ); + } + + return this.props.children; + } +} diff --git a/src/examples/lecture_5/ErrorFallback.tsx b/src/examples/lecture_5/ErrorFallback.tsx new file mode 100644 index 0000000..7c67686 --- /dev/null +++ b/src/examples/lecture_5/ErrorFallback.tsx @@ -0,0 +1,46 @@ +interface ErrorFallbackProps { + title?: string; + errorMessage?: string; + resetError?: () => void; +} + +export const ErrorFallback = ({ + title = 'Oops! Something went wrong', + errorMessage = 'An unexpected error occurred', + resetError, +}: ErrorFallbackProps) => { + return ( +
+
+
+ + + +
+
+

{title}

+

{errorMessage}

+ {resetError && ( + + )} +
+
+
+ ); +}; diff --git a/src/examples/lecture_5/PostList.tsx b/src/examples/lecture_5/PostList.tsx new file mode 100644 index 0000000..8bc95f9 --- /dev/null +++ b/src/examples/lecture_5/PostList.tsx @@ -0,0 +1,104 @@ +import { Suspense, use, useState } from 'react'; +import { User } from './UserList'; +import { fetchPost, fetchUser } from './lib'; +import { ErrorBoundary } from './ErrorBoundary'; +import { ErrorFallback } from './ErrorFallback'; + +export type Post = { + id: number; + title: string; + userId: number; +}; + +type Props = { + postPromise: Promise; +}; + +export const PostList = ({ postPromise }: Props) => { + const posts = use(postPromise); + const [selectedPost, setSelectedPost] = useState(null); + + return ( +
+
+

Post List

+
+
+
+ {posts.slice(0, 10).map((post) => ( +
setSelectedPost(post.id)} + > +

+ {post.title} +

+
+ ))} +
+
+ {selectedPost ? ( + }> + Loading Post...
}> + + + + ) : ( +
+

No post selected

+
+ )} +
+
+
+ ); +}; + +export const PostSkeleton = () => { + return ( +
+
+
+
+
+ ); +}; + +type ShowAuthorProps = { + authorPromise: Promise; +}; + +const ShowAuthor = ({ authorPromise }: ShowAuthorProps) => { + const author = use(authorPromise); + + return ( +
+

{author.name}

+
+ ); +}; + +type PostDetailProps = { + postPromise: Promise; +}; + +const PostDetail = ({ postPromise }: PostDetailProps) => { + const post = use(postPromise); + + return ( +
+

{post.title}

+

{post.body}

+ Failed to Fetch Author

+ } + > + Loading Author...
}> + + + +
+ ); +}; diff --git a/src/examples/lecture_5/UserList.tsx b/src/examples/lecture_5/UserList.tsx new file mode 100644 index 0000000..96f92f5 --- /dev/null +++ b/src/examples/lecture_5/UserList.tsx @@ -0,0 +1,43 @@ +import { use } from 'react'; + +export type User = { + id: number; + name: string; + email: string; +}; + +type Props = { + userPromise: Promise; +}; + +export const UserList = ({ userPromise }: Props) => { + const users = use(userPromise); + return ( +
+
+

User List

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

+ {user.name} +

+

{user.email}

+
+ ))} +
+ ); +}; + +export const UserSkeleton = () => { + return ( +
+
+
+
+
+ ); +}; diff --git a/src/examples/lecture_5/lib.ts b/src/examples/lecture_5/lib.ts new file mode 100644 index 0000000..13a07e8 --- /dev/null +++ b/src/examples/lecture_5/lib.ts @@ -0,0 +1,37 @@ +import { Post } from './PostList'; +import { User } from './UserList'; + +export const sleep = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + +export const fetchUsers = async () => { + await sleep(2000); + const response = await fetch('https://jsonplaceholder.typicode.com/users'); + const data = await response.json(); + return data as User[]; +}; + +export const fetchPosts = async () => { + await sleep(2000); + const response = await fetch('https://jsonplaceholder.typicode.com/posts'); + const data = await response.json(); + return data as Post[]; +}; + +export const fetchPost = async (id: number) => { + await sleep(2000); + const response = await fetch( + `https://jsonplaceholder.typicode.com/posts/${id}` + ); + const data = await response.json(); + return data as Post & { body: string }; +}; + +export const fetchUser = async (id: number) => { + await sleep(1000); + const response = await fetch( + `https://jsonplaceholder.typicode.com/users/${id}` + ); + const data = await response.json(); + return data as User; +}; 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..521ad80 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,7 +1,25 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' -import App from './App.tsx' +import App from './App.tsx'; + +import * as Sentry from '@sentry/react'; + +Sentry.init({ + dsn: 'https://fa05d22e315890eeba916e760ca7306e@o4508103575076864.ingest.de.sentry.io/4508846027964496', + integrations: [ + Sentry.browserTracingIntegration(), + Sentry.replayIntegration(), + ], + // Tracing + tracesSampleRate: 1.0, // Capture 100% of the transactions + // Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled + tracePropagationTargets: ['localhost', /^https:\/\/yourserver\.io\/api/], + // Session Replay + replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production. + replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur. + environment: 'development', +}); createRoot(document.getElementById('root')!).render( diff --git a/vite.config.ts b/vite.config.ts index 01b2051..4b468a4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,19 +1,31 @@ -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -import path from 'path'; - -// https://vite.dev/config/ -export default defineConfig({ - plugins: [ - react({ - babel: { - plugins: ['babel-plugin-react-compiler'], - }, - }), - ], - resolve: { - alias: { - '@': path.resolve(__dirname, 'src'), - }, - }, -}); +import { sentryVitePlugin } from '@sentry/vite-plugin'; +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({ + plugins: [ + react({ + babel: { + plugins: ['babel-plugin-react-compiler'], + }, + }), + tailwindcss(), + sentryVitePlugin({ + org: 'stack-learner', + project: 'javascript-react', + }), + ], + + resolve: { + alias: { + '@': path.resolve(__dirname, 'src'), + }, + }, + + build: { + sourcemap: true, + }, +});