diff --git a/index.html b/index.html
index e4b78ea..6693102 100644
--- a/index.html
+++ b/index.html
@@ -4,7 +4,11 @@
-
Vite + React + TS
+
+ React Design Patterns
+
+
+
diff --git a/package.json b/package.json
index 83ccf11..e344965 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
+ "@tailwindcss/vite": "^4.0.6",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
@@ -19,12 +20,15 @@
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4",
+ "autoprefixer": "^10.4.20",
"babel-plugin-react-compiler": "19.0.0-beta-30d8a17-20250209",
"eslint": "^9.19.0",
"eslint-plugin-react-compiler": "19.0.0-beta-30d8a17-20250209",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.18",
"globals": "^15.14.0",
+ "postcss": "^8.5.2",
+ "tailwindcss": "^4.0.6",
"typescript": "~5.7.2",
"typescript-eslint": "^8.22.0",
"vite": "^6.1.0"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 24289dc..ced0499 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,9 @@ importers:
.:
dependencies:
+ '@tailwindcss/vite':
+ specifier: ^4.0.6
+ version: 4.0.6(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1))
react:
specifier: ^19.0.0
version: 19.0.0
@@ -29,34 +32,43 @@ importers:
version: 19.0.3(@types/react@19.0.8)
'@vitejs/plugin-react':
specifier: ^4.3.4
- version: 4.3.4(vite@6.1.0(@types/node@22.13.4))
+ version: 4.3.4(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1))
+ autoprefixer:
+ specifier: ^10.4.20
+ version: 10.4.20(postcss@8.5.2)
babel-plugin-react-compiler:
specifier: 19.0.0-beta-30d8a17-20250209
version: 19.0.0-beta-30d8a17-20250209
eslint:
specifier: ^9.19.0
- version: 9.20.1
+ version: 9.20.1(jiti@2.4.2)
eslint-plugin-react-compiler:
specifier: 19.0.0-beta-30d8a17-20250209
- version: 19.0.0-beta-30d8a17-20250209(eslint@9.20.1)
+ version: 19.0.0-beta-30d8a17-20250209(eslint@9.20.1(jiti@2.4.2))
eslint-plugin-react-hooks:
specifier: ^5.0.0
- version: 5.1.0(eslint@9.20.1)
+ version: 5.1.0(eslint@9.20.1(jiti@2.4.2))
eslint-plugin-react-refresh:
specifier: ^0.4.18
- version: 0.4.19(eslint@9.20.1)
+ version: 0.4.19(eslint@9.20.1(jiti@2.4.2))
globals:
specifier: ^15.14.0
version: 15.15.0
+ postcss:
+ specifier: ^8.5.2
+ version: 8.5.2
+ tailwindcss:
+ specifier: ^4.0.6
+ version: 4.0.6
typescript:
specifier: ~5.7.2
version: 5.7.3
typescript-eslint:
specifier: ^8.22.0
- version: 8.24.0(eslint@9.20.1)(typescript@5.7.3)
+ version: 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)
vite:
specifier: ^6.1.0
- version: 6.1.0(@types/node@22.13.4)
+ version: 6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)
packages:
@@ -511,6 +523,84 @@ packages:
cpu: [x64]
os: [win32]
+ '@tailwindcss/node@4.0.6':
+ resolution: {integrity: sha512-jb6E0WeSq7OQbVYcIJ6LxnZTeC4HjMvbzFBMCrQff4R50HBlo/obmYNk6V2GCUXDeqiXtvtrQgcIbT+/boB03Q==}
+
+ '@tailwindcss/oxide-android-arm64@4.0.6':
+ resolution: {integrity: sha512-xDbym6bDPW3D2XqQqX3PjqW3CKGe1KXH7Fdkc60sX5ZLVUbzPkFeunQaoP+BuYlLc2cC1FoClrIRYnRzof9Sow==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [android]
+
+ '@tailwindcss/oxide-darwin-arm64@4.0.6':
+ resolution: {integrity: sha512-1f71/ju/tvyGl5c2bDkchZHy8p8EK/tDHCxlpYJ1hGNvsYihZNurxVpZ0DefpN7cNc9RTT8DjrRoV8xXZKKRjg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-darwin-x64@4.0.6':
+ resolution: {integrity: sha512-s/hg/ZPgxFIrGMb0kqyeaqZt505P891buUkSezmrDY6lxv2ixIELAlOcUVTkVh245SeaeEiUVUPiUN37cwoL2g==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-freebsd-x64@4.0.6':
+ resolution: {integrity: sha512-Z3Wo8FWZnmio8+xlcbb7JUo/hqRMSmhQw8IGIRoRJ7GmLR0C+25Wq+bEX/135xe/yEle2lFkhu9JBHd4wZYiig==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.6':
+ resolution: {integrity: sha512-SNSwkkim1myAgmnbHs4EjXsPL7rQbVGtjcok5EaIzkHkCAVK9QBQsWeP2Jm2/JJhq4wdx8tZB9Y7psMzHYWCkA==}
+ engines: {node: '>= 10'}
+ cpu: [arm]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.0.6':
+ resolution: {integrity: sha512-tJ+mevtSDMQhKlwCCuhsFEFg058kBiSy4TkoeBG921EfrHKmexOaCyFKYhVXy4JtkaeeOcjJnCLasEeqml4i+Q==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.0.6':
+ resolution: {integrity: sha512-IoArz1vfuTR4rALXMUXI/GWWfx2EaO4gFNtBNkDNOYhlTD4NVEwE45nbBoojYiTulajI4c2XH8UmVEVJTOJKxA==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.0.6':
+ resolution: {integrity: sha512-QtsUfLkEAeWAC3Owx9Kg+7JdzE+k9drPhwTAXbXugYB9RZUnEWWx5x3q/au6TvUYcL+n0RBqDEO2gucZRvRFgQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-x64-musl@4.0.6':
+ resolution: {integrity: sha512-QthvJqIji2KlGNwLcK/PPYo7w1Wsi/8NK0wAtRGbv4eOPdZHkQ9KUk+oCoP20oPO7i2a6X1aBAFQEL7i08nNMA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.0.6':
+ resolution: {integrity: sha512-+oka+dYX8jy9iP00DJ9Y100XsqvbqR5s0yfMZJuPR1H/lDVtDfsZiSix1UFBQ3X1HWxoEEl6iXNJHWd56TocVw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.0.6':
+ resolution: {integrity: sha512-+o+juAkik4p8Ue/0LiflQXPmVatl6Av3LEZXpBTfg4qkMIbZdhCGWFzHdt2NjoMiLOJCFDddoV6GYaimvK1Olw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@tailwindcss/oxide@4.0.6':
+ resolution: {integrity: sha512-lVyKV2y58UE9CeKVcYykULe9QaE1dtKdxDEdrTPIdbzRgBk6bdxHNAoDqvcqXbIGXubn3VOl1O/CFF77v/EqSA==}
+ engines: {node: '>= 10'}
+
+ '@tailwindcss/vite@4.0.6':
+ resolution: {integrity: sha512-O25vZ/URWbZ2JHdk2o8wH7jOKqEGCsYmX3GwGmYS5DjE4X3mpf93a72Rn7VRnefldNauBzr5z2hfZptmBNtTUQ==}
+ peerDependencies:
+ vite: ^5.2.0 || ^6
+
'@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
@@ -613,6 +703,13 @@ packages:
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+ autoprefixer@10.4.20:
+ resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==}
+ engines: {node: ^10 || ^12 || >=14}
+ hasBin: true
+ peerDependencies:
+ postcss: ^8.1.0
+
babel-plugin-react-compiler@19.0.0-beta-30d8a17-20250209:
resolution: {integrity: sha512-0pQHlz5nmBiEQ8ZWWVLeaBzz/FkToAdXEXBBnd21uSrDtIzhSe+s3VMvqMsv6vYHNTr+0KmsvVfEqXQp0W0kzg==}
@@ -677,9 +774,18 @@ packages:
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+ detect-libc@1.0.3:
+ resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
+ engines: {node: '>=0.10'}
+ hasBin: true
+
electron-to-chromium@1.5.101:
resolution: {integrity: sha512-L0ISiQrP/56Acgu4/i/kfPwWSgrzYZUnQrC0+QPFuhqlLP1Ir7qzPPDVS9BcKIyWTRU8+o6CC8dKw38tSWhYIA==}
+ enhanced-resolve@5.18.1:
+ resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
+ engines: {node: '>=10.13.0'}
+
esbuild@0.24.2:
resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==}
engines: {node: '>=18'}
@@ -787,6 +893,9 @@ packages:
flatted@3.3.2:
resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==}
+ fraction.js@4.3.7:
+ resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -816,6 +925,9 @@ packages:
resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==}
engines: {node: '>=18'}
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
@@ -856,6 +968,10 @@ packages:
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+ jiti@2.4.2:
+ resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
+ hasBin: true
+
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -889,6 +1005,70 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
+ lightningcss-darwin-arm64@1.29.1:
+ resolution: {integrity: sha512-HtR5XJ5A0lvCqYAoSv2QdZZyoHNttBpa5EP9aNuzBQeKGfbyH5+UipLWvVzpP4Uml5ej4BYs5I9Lco9u1fECqw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.29.1:
+ resolution: {integrity: sha512-k33G9IzKUpHy/J/3+9MCO4e+PzaFblsgBjSGlpAaFikeBFm8B/CkO3cKU9oI4g+fjS2KlkLM/Bza9K/aw8wsNA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.29.1:
+ resolution: {integrity: sha512-0SUW22fv/8kln2LnIdOCmSuXnxgxVC276W5KLTwoehiO0hxkacBxjHOL5EtHD8BAXg2BvuhsJPmVMasvby3LiQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.29.1:
+ resolution: {integrity: sha512-sD32pFvlR0kDlqsOZmYqH/68SqUMPNj+0pucGxToXZi4XZgZmqeX/NkxNKCPsswAXU3UeYgDSpGhu05eAufjDg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.29.1:
+ resolution: {integrity: sha512-0+vClRIZ6mmJl/dxGuRsE197o1HDEeeRk6nzycSy2GofC2JsY4ifCRnvUWf/CUBQmlrvMzt6SMQNMSEu22csWQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-arm64-musl@1.29.1:
+ resolution: {integrity: sha512-UKMFrG4rL/uHNgelBsDwJcBqVpzNJbzsKkbI3Ja5fg00sgQnHw/VrzUTEc4jhZ+AN2BvQYz/tkHu4vt1kLuJyw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-x64-gnu@1.29.1:
+ resolution: {integrity: sha512-u1S+xdODy/eEtjADqirA774y3jLcm8RPtYztwReEXoZKdzgsHYPl0s5V52Tst+GKzqjebkULT86XMSxejzfISw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-linux-x64-musl@1.29.1:
+ resolution: {integrity: sha512-L0Tx0DtaNUTzXv0lbGCLB/c/qEADanHbu4QdcNOXLIe1i8i22rZRpbT3gpWYsCh9aSL9zFujY/WmEXIatWvXbw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-win32-arm64-msvc@1.29.1:
+ resolution: {integrity: sha512-QoOVnkIEFfbW4xPi+dpdft/zAKmgLgsRHfJalEPYuJDOWf7cLQzYg0DEh8/sn737FaeMJxHZRc1oBreiwZCjog==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.29.1:
+ resolution: {integrity: sha512-NygcbThNBe4JElP+olyTI/doBNGJvLs3bFCRPdvuCcxZCcCZ71B858IHpdm7L1btZex0FvCmM17FK98Y9MRy1Q==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.29.1:
+ resolution: {integrity: sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q==}
+ engines: {node: '>= 12.0.0'}
+
locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
@@ -928,6 +1108,10 @@ packages:
node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
+ normalize-range@0.1.2:
+ resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
+ engines: {node: '>=0.10.0'}
+
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@@ -959,6 +1143,9 @@ packages:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
+ postcss-value-parser@4.2.0:
+ resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
+
postcss@8.5.2:
resolution: {integrity: sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==}
engines: {node: ^10 || ^12 || >=14}
@@ -1035,6 +1222,13 @@ packages:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
+ tailwindcss@4.0.6:
+ resolution: {integrity: sha512-mysewHYJKaXgNOW6pp5xon/emCsfAMnO8WMaGKZZ35fomnR/T5gYnRg2/yRTTrtXiEl1tiVkeRt0eMO6HxEZqw==}
+
+ tapable@2.2.1:
+ resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
+ engines: {node: '>=6'}
+
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -1382,9 +1576,9 @@ snapshots:
'@esbuild/win32-x64@0.24.2':
optional: true
- '@eslint-community/eslint-utils@4.4.1(eslint@9.20.1)':
+ '@eslint-community/eslint-utils@4.4.1(eslint@9.20.1(jiti@2.4.2))':
dependencies:
- eslint: 9.20.1
+ eslint: 9.20.1(jiti@2.4.2)
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.1': {}
@@ -1527,6 +1721,67 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.34.7':
optional: true
+ '@tailwindcss/node@4.0.6':
+ dependencies:
+ enhanced-resolve: 5.18.1
+ jiti: 2.4.2
+ tailwindcss: 4.0.6
+
+ '@tailwindcss/oxide-android-arm64@4.0.6':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-arm64@4.0.6':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-x64@4.0.6':
+ optional: true
+
+ '@tailwindcss/oxide-freebsd-x64@4.0.6':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.6':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.0.6':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.0.6':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.0.6':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-musl@4.0.6':
+ optional: true
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.0.6':
+ optional: true
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.0.6':
+ optional: true
+
+ '@tailwindcss/oxide@4.0.6':
+ optionalDependencies:
+ '@tailwindcss/oxide-android-arm64': 4.0.6
+ '@tailwindcss/oxide-darwin-arm64': 4.0.6
+ '@tailwindcss/oxide-darwin-x64': 4.0.6
+ '@tailwindcss/oxide-freebsd-x64': 4.0.6
+ '@tailwindcss/oxide-linux-arm-gnueabihf': 4.0.6
+ '@tailwindcss/oxide-linux-arm64-gnu': 4.0.6
+ '@tailwindcss/oxide-linux-arm64-musl': 4.0.6
+ '@tailwindcss/oxide-linux-x64-gnu': 4.0.6
+ '@tailwindcss/oxide-linux-x64-musl': 4.0.6
+ '@tailwindcss/oxide-win32-arm64-msvc': 4.0.6
+ '@tailwindcss/oxide-win32-x64-msvc': 4.0.6
+
+ '@tailwindcss/vite@4.0.6(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1))':
+ dependencies:
+ '@tailwindcss/node': 4.0.6
+ '@tailwindcss/oxide': 4.0.6
+ lightningcss: 1.29.1
+ tailwindcss: 4.0.6
+ vite: 6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)
+
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.26.9
@@ -1564,15 +1819,15 @@ snapshots:
dependencies:
csstype: 3.1.3
- '@typescript-eslint/eslint-plugin@8.24.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1)(typescript@5.7.3))(eslint@9.20.1)(typescript@5.7.3)':
+ '@typescript-eslint/eslint-plugin@8.24.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
- '@typescript-eslint/parser': 8.24.0(eslint@9.20.1)(typescript@5.7.3)
+ '@typescript-eslint/parser': 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)
'@typescript-eslint/scope-manager': 8.24.0
- '@typescript-eslint/type-utils': 8.24.0(eslint@9.20.1)(typescript@5.7.3)
- '@typescript-eslint/utils': 8.24.0(eslint@9.20.1)(typescript@5.7.3)
+ '@typescript-eslint/type-utils': 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)
+ '@typescript-eslint/utils': 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)
'@typescript-eslint/visitor-keys': 8.24.0
- eslint: 9.20.1
+ eslint: 9.20.1(jiti@2.4.2)
graphemer: 1.4.0
ignore: 5.3.2
natural-compare: 1.4.0
@@ -1581,14 +1836,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/parser@8.24.0(eslint@9.20.1)(typescript@5.7.3)':
+ '@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.24.0
'@typescript-eslint/types': 8.24.0
'@typescript-eslint/typescript-estree': 8.24.0(typescript@5.7.3)
'@typescript-eslint/visitor-keys': 8.24.0
debug: 4.4.0
- eslint: 9.20.1
+ eslint: 9.20.1(jiti@2.4.2)
typescript: 5.7.3
transitivePeerDependencies:
- supports-color
@@ -1598,12 +1853,12 @@ snapshots:
'@typescript-eslint/types': 8.24.0
'@typescript-eslint/visitor-keys': 8.24.0
- '@typescript-eslint/type-utils@8.24.0(eslint@9.20.1)(typescript@5.7.3)':
+ '@typescript-eslint/type-utils@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)':
dependencies:
'@typescript-eslint/typescript-estree': 8.24.0(typescript@5.7.3)
- '@typescript-eslint/utils': 8.24.0(eslint@9.20.1)(typescript@5.7.3)
+ '@typescript-eslint/utils': 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)
debug: 4.4.0
- eslint: 9.20.1
+ eslint: 9.20.1(jiti@2.4.2)
ts-api-utils: 2.0.1(typescript@5.7.3)
typescript: 5.7.3
transitivePeerDependencies:
@@ -1625,13 +1880,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/utils@8.24.0(eslint@9.20.1)(typescript@5.7.3)':
+ '@typescript-eslint/utils@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)':
dependencies:
- '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1)
+ '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1(jiti@2.4.2))
'@typescript-eslint/scope-manager': 8.24.0
'@typescript-eslint/types': 8.24.0
'@typescript-eslint/typescript-estree': 8.24.0(typescript@5.7.3)
- eslint: 9.20.1
+ eslint: 9.20.1(jiti@2.4.2)
typescript: 5.7.3
transitivePeerDependencies:
- supports-color
@@ -1641,14 +1896,14 @@ snapshots:
'@typescript-eslint/types': 8.24.0
eslint-visitor-keys: 4.2.0
- '@vitejs/plugin-react@4.3.4(vite@6.1.0(@types/node@22.13.4))':
+ '@vitejs/plugin-react@4.3.4(vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1))':
dependencies:
'@babel/core': 7.26.9
'@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.9)
'@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.9)
'@types/babel__core': 7.20.5
react-refresh: 0.14.2
- vite: 6.1.0(@types/node@22.13.4)
+ vite: 6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)
transitivePeerDependencies:
- supports-color
@@ -1671,6 +1926,16 @@ snapshots:
argparse@2.0.1: {}
+ autoprefixer@10.4.20(postcss@8.5.2):
+ dependencies:
+ browserslist: 4.24.4
+ caniuse-lite: 1.0.30001699
+ fraction.js: 4.3.7
+ normalize-range: 0.1.2
+ picocolors: 1.1.1
+ postcss: 8.5.2
+ postcss-value-parser: 4.2.0
+
babel-plugin-react-compiler@19.0.0-beta-30d8a17-20250209:
dependencies:
'@babel/types': 7.26.9
@@ -1730,8 +1995,15 @@ snapshots:
deep-is@0.1.4: {}
+ detect-libc@1.0.3: {}
+
electron-to-chromium@1.5.101: {}
+ enhanced-resolve@5.18.1:
+ dependencies:
+ graceful-fs: 4.2.11
+ tapable: 2.2.1
+
esbuild@0.24.2:
optionalDependencies:
'@esbuild/aix-ppc64': 0.24.2
@@ -1764,25 +2036,25 @@ snapshots:
escape-string-regexp@4.0.0: {}
- eslint-plugin-react-compiler@19.0.0-beta-30d8a17-20250209(eslint@9.20.1):
+ eslint-plugin-react-compiler@19.0.0-beta-30d8a17-20250209(eslint@9.20.1(jiti@2.4.2)):
dependencies:
'@babel/core': 7.26.9
'@babel/parser': 7.26.9
'@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.26.9)
- eslint: 9.20.1
+ eslint: 9.20.1(jiti@2.4.2)
hermes-parser: 0.25.1
zod: 3.24.2
zod-validation-error: 3.4.0(zod@3.24.2)
transitivePeerDependencies:
- supports-color
- eslint-plugin-react-hooks@5.1.0(eslint@9.20.1):
+ eslint-plugin-react-hooks@5.1.0(eslint@9.20.1(jiti@2.4.2)):
dependencies:
- eslint: 9.20.1
+ eslint: 9.20.1(jiti@2.4.2)
- eslint-plugin-react-refresh@0.4.19(eslint@9.20.1):
+ eslint-plugin-react-refresh@0.4.19(eslint@9.20.1(jiti@2.4.2)):
dependencies:
- eslint: 9.20.1
+ eslint: 9.20.1(jiti@2.4.2)
eslint-scope@8.2.0:
dependencies:
@@ -1793,9 +2065,9 @@ snapshots:
eslint-visitor-keys@4.2.0: {}
- eslint@9.20.1:
+ eslint@9.20.1(jiti@2.4.2):
dependencies:
- '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1)
+ '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1(jiti@2.4.2))
'@eslint-community/regexpp': 4.12.1
'@eslint/config-array': 0.19.2
'@eslint/core': 0.11.0
@@ -1829,6 +2101,8 @@ snapshots:
minimatch: 3.1.2
natural-compare: 1.4.0
optionator: 0.9.4
+ optionalDependencies:
+ jiti: 2.4.2
transitivePeerDependencies:
- supports-color
@@ -1888,6 +2162,8 @@ snapshots:
flatted@3.3.2: {}
+ fraction.js@4.3.7: {}
+
fsevents@2.3.3:
optional: true
@@ -1907,6 +2183,8 @@ snapshots:
globals@15.15.0: {}
+ graceful-fs@4.2.11: {}
+
graphemer@1.4.0: {}
has-flag@4.0.0: {}
@@ -1936,6 +2214,8 @@ snapshots:
isexe@2.0.0: {}
+ jiti@2.4.2: {}
+
js-tokens@4.0.0: {}
js-yaml@4.1.0:
@@ -1961,6 +2241,51 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
+ lightningcss-darwin-arm64@1.29.1:
+ optional: true
+
+ lightningcss-darwin-x64@1.29.1:
+ optional: true
+
+ lightningcss-freebsd-x64@1.29.1:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.29.1:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.29.1:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.29.1:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.29.1:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.29.1:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.29.1:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.29.1:
+ optional: true
+
+ lightningcss@1.29.1:
+ dependencies:
+ detect-libc: 1.0.3
+ optionalDependencies:
+ lightningcss-darwin-arm64: 1.29.1
+ lightningcss-darwin-x64: 1.29.1
+ lightningcss-freebsd-x64: 1.29.1
+ lightningcss-linux-arm-gnueabihf: 1.29.1
+ lightningcss-linux-arm64-gnu: 1.29.1
+ lightningcss-linux-arm64-musl: 1.29.1
+ lightningcss-linux-x64-gnu: 1.29.1
+ lightningcss-linux-x64-musl: 1.29.1
+ lightningcss-win32-arm64-msvc: 1.29.1
+ lightningcss-win32-x64-msvc: 1.29.1
+
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
@@ -1994,6 +2319,8 @@ snapshots:
node-releases@2.0.19: {}
+ normalize-range@0.1.2: {}
+
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@@ -2023,6 +2350,8 @@ snapshots:
picomatch@2.3.1: {}
+ postcss-value-parser@4.2.0: {}
+
postcss@8.5.2:
dependencies:
nanoid: 3.3.8
@@ -2097,6 +2426,10 @@ snapshots:
dependencies:
has-flag: 4.0.0
+ tailwindcss@4.0.6: {}
+
+ tapable@2.2.1: {}
+
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@@ -2109,12 +2442,12 @@ snapshots:
dependencies:
prelude-ls: 1.2.1
- typescript-eslint@8.24.0(eslint@9.20.1)(typescript@5.7.3):
+ typescript-eslint@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3):
dependencies:
- '@typescript-eslint/eslint-plugin': 8.24.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1)(typescript@5.7.3))(eslint@9.20.1)(typescript@5.7.3)
- '@typescript-eslint/parser': 8.24.0(eslint@9.20.1)(typescript@5.7.3)
- '@typescript-eslint/utils': 8.24.0(eslint@9.20.1)(typescript@5.7.3)
- eslint: 9.20.1
+ '@typescript-eslint/eslint-plugin': 8.24.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)
+ '@typescript-eslint/parser': 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)
+ '@typescript-eslint/utils': 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)
+ eslint: 9.20.1(jiti@2.4.2)
typescript: 5.7.3
transitivePeerDependencies:
- supports-color
@@ -2133,7 +2466,7 @@ snapshots:
dependencies:
punycode: 2.3.1
- vite@6.1.0(@types/node@22.13.4):
+ vite@6.1.0(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1):
dependencies:
esbuild: 0.24.2
postcss: 8.5.2
@@ -2141,6 +2474,8 @@ snapshots:
optionalDependencies:
'@types/node': 22.13.4
fsevents: 2.3.3
+ jiti: 2.4.2
+ lightningcss: 1.29.1
which@2.0.2:
dependencies:
diff --git a/src/App.css b/src/App.css
deleted file mode 100644
index b9d355d..0000000
--- a/src/App.css
+++ /dev/null
@@ -1,42 +0,0 @@
-#root {
- max-width: 1280px;
- margin: 0 auto;
- padding: 2rem;
- text-align: center;
-}
-
-.logo {
- height: 6em;
- padding: 1.5em;
- will-change: filter;
- transition: filter 300ms;
-}
-.logo:hover {
- filter: drop-shadow(0 0 2em #646cffaa);
-}
-.logo.react:hover {
- filter: drop-shadow(0 0 2em #61dafbaa);
-}
-
-@keyframes logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
-
-@media (prefers-reduced-motion: no-preference) {
- a:nth-of-type(2) .logo {
- animation: logo-spin infinite 20s linear;
- }
-}
-
-.card {
- padding: 2em;
-}
-
-.read-the-docs {
- color: #888;
-}
diff --git a/src/App.tsx b/src/App.tsx
index 3d7ded3..fd8847d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,35 +1,63 @@
-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 { 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_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/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/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/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/index.css b/src/index.css
index 6119ad9..14325be 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,68 +1,10 @@
-:root {
- font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
- line-height: 1.5;
- font-weight: 400;
-
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
-
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-a {
- font-weight: 500;
- color: #646cff;
- text-decoration: inherit;
-}
-a:hover {
- color: #535bf2;
-}
-
-body {
- margin: 0;
- display: flex;
- place-items: center;
- min-width: 320px;
- min-height: 100vh;
-}
-
-h1 {
- font-size: 3.2em;
- line-height: 1.1;
-}
-
-button {
- border-radius: 8px;
- border: 1px solid transparent;
- padding: 0.6em 1.2em;
- font-size: 1em;
- font-weight: 500;
- font-family: inherit;
- background-color: #1a1a1a;
- cursor: pointer;
- transition: border-color 0.25s;
-}
-button:hover {
- border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
- outline: 4px auto -webkit-focus-ring-color;
-}
-
-@media (prefers-color-scheme: light) {
- :root {
- color: #213547;
- background-color: #ffffff;
- }
- a:hover {
- color: #747bff;
- }
- button {
- background-color: #f9f9f9;
- }
-}
+@import 'tailwindcss';
+
+@theme {
+ --default-font-family: 'Poppins', sans-serif;
+ --default-font-size: 16px;
+ --default-font-weight: 400;
+ --default-line-height: 1.5;
+ --default-letter-spacing: 0.01em;
+ --default-text-color: #333;
+}
\ No newline at end of file
diff --git a/src/main.tsx b/src/main.tsx
index bef5202..bb7941a 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,7 +1,7 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
-import App from './App.tsx'
+import App from './App.tsx';
createRoot(document.getElementById('root')!).render(
diff --git a/vite.config.ts b/vite.config.ts
index 01b2051..3a6a3ea 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,6 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
+import tailwindcss from '@tailwindcss/vite';
// https://vite.dev/config/
export default defineConfig({
@@ -10,6 +11,7 @@ export default defineConfig({
plugins: ['babel-plugin-react-compiler'],
},
}),
+ tailwindcss(),
],
resolve: {
alias: {