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 + React
-
-
setCount((count) => count + 1)}>
- count is {count}
-
-
- 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 (
+
+
+ {
+ throw new Error('Test Error');
+ }}
+ >
+ Throw Error
+
+
+ {/*
*/}
+ {/* }
+ >
+ }>
+
+
+ */}
+
+
+
+ }
+ >
+
}>
+
+
+
+ {/*
*/}
+
+ );
+}
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}
+
+ Prev
+ Next
+
+
+ )}
+ */}
+
+ {/*
*/}
+
+
+ );
+}
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.
+
+
+ Click me
+
+
+
+
+ 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 (
+
+ {children}
+
+
+
+
+ );
+};
+
+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 (
+
+ {children}
+
+
+
+
+ );
+};
+
+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 (
+
+ {label}
+
+ );
+};
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}
+
+
+ Increment
+
+
+ Decrement
+
+
+ Reset
+
+
+
+ );
+};
+
+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}
+
+
+ Increment
+
+
+ Decrement
+
+
+ Reset
+
+
+
Count: {count2}
+
+
+ Increment
+
+
+ Decrement
+
+
+ Reset
+
+
+
+ );
+};
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
+
Refetch
+
+ {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 (
+
+
setOpen((prev) => !prev)}
+ className='w-full px-4 py-2 text-left bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500'
+ >
+
+
Select an option
+
+
+
+
+
+ {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}
+
+ onUpdate({ name: 'John Doe', email: 'john@example.com' })
+ }
+ >
+ Update
+
+
+ );
+};
+
+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
+
+ Pay Now
+
+
+ );
+};
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}
+
updateStatus('Completed')}>
+ Mark as Completed
+
+
+ ) : (
+
Loading...
+ )}
+
+ {/* Team Members */}
+
Team Members
+
+ {team.map((member) => (
+
+ {' '}
+ {member.name}
+
+ ))}
+
+
+ {/* Comments Section */}
+
Comments
+
+ {comments.map((comment) => (
+ {comment.text}
+ ))}
+
+
setNewComment(e.target.value)}
+ />
+
Post
+
+ );
+};
+
+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;
+}) => (
+
+ onUpdate('Completed')}>Mark as Completed
+
+);
+
+const TeamList = ({ team }: { team: Team[] }) => (
+
+
Team Members
+
+ {team.map((member) => (
+
+ {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)}
+ />
+
Post
+
+ );
+};
+
+/**
+ * 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 }) => (
+
+
+
+
+ Previous
+
+
+ Next
+
+
+
+ {Array.from({ length: totalSteps }, (_, i) => (
+ goToStep(i + 1)}
+ className={`w-3 h-3 rounded-full ${
+ currentStep === i + 1 ? 'bg-blue-500' : 'bg-gray-300'
+ }`}
+ aria-label={`Go to photo ${i + 1}`}
+ />
+ ))}
+
+
+ )}
+
+ );
+};
+
+// 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 && (
+
+ )}
+
+ ))}
+
+
+
+ Previous
+
+
+ Next
+
+
+
+ )}
+
+ );
+};
\ 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}
+ )}
+
+
+
+ Refresh Now
+
+
+ Pause
+
+
+ Resume
+
+
+
+ )}
+
+
+ );
+};
+
+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 (
+
+ {context.theme === 'light' ? 'Dark Mode' : 'Light Mode'}
+
+ );
+};
+
+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 (
+
+ {children}
+
+
+
+
+ );
+};
+
+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 (
+ !disabled && setActiveTab(id)}
+ disabled={disabled}
+ >
+ {children}
+
+ );
+};
+
+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 (
+
+ {label}
+
+ );
+};
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 && (
+
+ {label}
+
+ )}
+
+
+ );
+};
+
+export const FormExample = () => {
+ const [name, setName] = useState('');
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ console.log(name);
+ };
+
+ return (
+
+ );
+};
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 (
+
+ {children}
+
+
+
+
+ );
+};
+
+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}
+
+
+ Increment
+
+
+ Decrement
+
+
+ Reset
+
+
+
+ );
+};
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 (
+
+
setOpen((prev) => !prev)}
+ className='w-full px-4 py-2 text-left bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500'
+ >
+
+
Select an option
+
+
+
+
+
+ {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 (
+
+
Toggle Theme
+
+
setSearch(e.target.value)}
+ />
+
+ {loading &&
Loading...
}
+ {error &&
Error: {error}
}
+
+
+ {users
+ .filter((user) =>
+ user.name.toLowerCase().includes(search.toLowerCase())
+ )
+ .map((user) => (
+ {user.name}
+ ))}
+
+
+
+
setDropdownOpen(!isDropdownOpen)}>
+ Open Dropdown
+
+ {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 (
+
+
+
+ {theme === 'light' ? 'Dark Mode' : 'Light Mode'}
+
+
+
setDropdownOpen(!isDropdownOpen)}
+ className='bg-blue-500 text-white p-2 rounded-md'
+ >
+ Open Dropdown
+
+ {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
+
+---
+
+
+💡
+
+- useEffect is not for all effects. It should rename as useSynchronize
+- useEffect is not a life cycle method, the mental model of useEffect is synchronization
+
+
+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 && (
+
+ Try again
+
+ )}
+
+
+
+ );
+};
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...
}>
+
+
+
+ ) : (
+
+ )}
+
+
+
+ );
+};
+
+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,
+ },
+});