diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml
new file mode 100644
index 00000000..74edea0e
--- /dev/null
+++ b/.github/workflows/production.yml
@@ -0,0 +1,29 @@
+name: Vercel Production Deployment
+
+env:
+ VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
+ VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
+
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ Deploy-Production:
+ runs-on: ubuntu-latest
+ steps:
+
+ - uses: actions/checkout@v3
+
+ - name: Install Vercel CLI
+ run: npm install --global vercel
+
+ - name: Pull Vercel Environment Information
+ run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
+
+ - name: Build Project Artifacts
+ run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
+
+ - name: Deploy Project Artifacts
+ run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
diff --git a/package-lock.json b/package-lock.json
index 8d91ee86..7fe023bd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,30 +8,35 @@
"name": "neurolearn_frontend",
"version": "0.1.0",
"dependencies": {
+ "@fortawesome/fontawesome-svg-core": "^6.7.2",
+ "@fortawesome/free-brands-svg-icons": "^6.7.2",
+ "@fortawesome/react-fontawesome": "^0.2.2",
"@supabase/supabase-js": "^2.49.4",
"autoprefixer": "^10.4.21",
"axios": "^1.8.4",
+ "framer-motion": "^12.12.1",
"next": "15.3.1",
"postcss": "^8.5.3",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
- "@commitlint/cli": "^19.8.0",
- "@commitlint/config-conventional": "^19.8.0",
+ "@commitlint/cli": "^19.2.1",
+ "@commitlint/config-conventional": "^19.1.0",
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
+ "@types/webpack": "^5.28.5",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"eslint": "^9",
"eslint-config-next": "15.3.1",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.2.6",
- "husky": "^9.1.7",
- "lint-staged": "^15.5.1",
+ "husky": "^9.0.11",
+ "lint-staged": "^15.2.2",
"prettier": "^3.5.3",
"tailwindcss": "^4.1.4",
"typescript": "^5"
@@ -665,6 +670,52 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@fortawesome/fontawesome-common-types": {
+ "version": "6.7.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
+ "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/fontawesome-svg-core": {
+ "version": "6.7.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
+ "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "6.7.2"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/free-brands-svg-icons": {
+ "version": "6.7.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.7.2.tgz",
+ "integrity": "sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q==",
+ "license": "(CC-BY-4.0 AND MIT)",
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "6.7.2"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/react-fontawesome": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz",
+ "integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==",
+ "license": "MIT",
+ "dependencies": {
+ "prop-types": "^15.8.1"
+ },
+ "peerDependencies": {
+ "@fortawesome/fontawesome-svg-core": "~1 || ~6",
+ "react": ">=16.3"
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -1108,6 +1159,70 @@
"url": "https://opencollective.com/libvips"
}
},
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
+ "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/set-array": "^1.2.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/source-map": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
+ "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.25",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+ "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.9.tgz",
@@ -1718,6 +1833,28 @@
"@types/node": "*"
}
},
+ "node_modules/@types/eslint": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
+ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*",
+ "@types/json-schema": "*"
+ }
+ },
+ "node_modules/@types/eslint-scope": {
+ "version": "3.7.7",
+ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
+ "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/eslint": "*",
+ "@types/estree": "*"
+ }
+ },
"node_modules/@types/estree": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
@@ -1774,6 +1911,18 @@
"@types/react": "^19.0.0"
}
},
+ "node_modules/@types/webpack": {
+ "version": "5.28.5",
+ "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz",
+ "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "tapable": "^2.2.0",
+ "webpack": "^5"
+ }
+ },
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
@@ -2242,6 +2391,181 @@
"win32"
]
},
+ "node_modules/@webassemblyjs/ast": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
+ "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/helper-numbers": "1.13.2",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/floating-point-hex-parser": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
+ "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-api-error": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
+ "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-buffer": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
+ "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-numbers": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz",
+ "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/floating-point-hex-parser": "1.13.2",
+ "@webassemblyjs/helper-api-error": "1.13.2",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webassemblyjs/helper-wasm-bytecode": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
+ "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-wasm-section": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz",
+ "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/wasm-gen": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/ieee754": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz",
+ "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@xtuc/ieee754": "^1.2.0"
+ }
+ },
+ "node_modules/@webassemblyjs/leb128": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz",
+ "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webassemblyjs/utf8": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
+ "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/wasm-edit": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz",
+ "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/helper-wasm-section": "1.14.1",
+ "@webassemblyjs/wasm-gen": "1.14.1",
+ "@webassemblyjs/wasm-opt": "1.14.1",
+ "@webassemblyjs/wasm-parser": "1.14.1",
+ "@webassemblyjs/wast-printer": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-gen": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz",
+ "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/ieee754": "1.13.2",
+ "@webassemblyjs/leb128": "1.13.2",
+ "@webassemblyjs/utf8": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-opt": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz",
+ "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/wasm-gen": "1.14.1",
+ "@webassemblyjs/wasm-parser": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-parser": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz",
+ "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-api-error": "1.13.2",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/ieee754": "1.13.2",
+ "@webassemblyjs/leb128": "1.13.2",
+ "@webassemblyjs/utf8": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/wast-printer": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz",
+ "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@xtuc/ieee754": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
+ "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@xtuc/long": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
+ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
"node_modules/acorn": {
"version": "8.14.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
@@ -2282,6 +2606,61 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/ajv-formats": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
+ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ajv-formats/node_modules/ajv": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ajv-keywords": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
+ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3"
+ },
+ "peerDependencies": {
+ "ajv": "^8.8.2"
+ }
+ },
"node_modules/ansi-escapes": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz",
@@ -2689,6 +3068,13 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@@ -2796,6 +3182,16 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/chrome-trace-event": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
+ "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
"node_modules/cli-cursor": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
@@ -3513,6 +3909,13 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
@@ -4070,6 +4473,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
"node_modules/execa": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
@@ -4307,6 +4720,33 @@
"url": "https://github.com/sponsors/rawify"
}
},
+ "node_modules/framer-motion": {
+ "version": "12.12.1",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.12.1.tgz",
+ "integrity": "sha512-PFw4/GCREHI2suK/NlPSUxd+x6Rkp80uQsfCRFSOQNrm5pZif7eGtmG1VaD/UF1fW9tRBy5AaS77StatB3OJDg==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.12.1",
+ "motion-utils": "^12.12.1",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -4482,6 +4922,13 @@
"node": ">=10.13.0"
}
},
+ "node_modules/glob-to-regexp": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
"node_modules/global-directory": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz",
@@ -5241,6 +5688,37 @@
"node": ">= 0.4"
}
},
+ "node_modules/jest-worker": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
+ "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
"node_modules/jiti": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
@@ -5255,7 +5733,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
@@ -5717,6 +6194,16 @@
"node": ">=18.0.0"
}
},
+ "node_modules/loader-runner": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
+ "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.11.5"
+ }
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -5866,7 +6353,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
- "dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
@@ -5998,6 +6484,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/motion-dom": {
+ "version": "12.12.1",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.12.1.tgz",
+ "integrity": "sha512-GXq/uUbZBEiFFE+K1Z/sxdPdadMdfJ/jmBALDfIuHGi0NmtealLOfH9FqT+6aNPgVx8ilq0DtYmyQlo6Uj9LKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.12.1"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "12.12.1",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.12.1.tgz",
+ "integrity": "sha512-f9qiqUHm7hWSLlNW8gS9pisnsN7CRFRD58vNjptKdsqFLpkVnX00TNeD6Q0d27V9KzT7ySFyK1TZ/DShfVOv6w==",
+ "license": "MIT"
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -6046,6 +6547,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/neo-async": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/next": {
"version": "15.3.1",
"resolved": "https://registry.npmjs.org/next/-/next-15.3.1.tgz",
@@ -6176,7 +6684,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -6567,7 +7074,6 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
@@ -6612,6 +7118,16 @@
],
"license": "MIT"
},
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
"node_modules/react": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
@@ -6637,7 +7153,6 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/reflect.getprototypeof": {
@@ -6840,6 +7355,27 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/safe-push-apply": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
@@ -6881,6 +7417,50 @@
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
"license": "MIT"
},
+ "node_modules/schema-utils": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
+ "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/json-schema": "^7.0.9",
+ "ajv": "^8.9.0",
+ "ajv-formats": "^2.1.1",
+ "ajv-keywords": "^5.1.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/schema-utils/node_modules/ajv": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/schema-utils/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
@@ -6894,6 +7474,16 @@
"node": ">=10"
}
},
+ "node_modules/serialize-javascript": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
+ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -7146,6 +7736,16 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -7155,6 +7755,17 @@
"node": ">=0.10.0"
}
},
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
@@ -7463,6 +8074,67 @@
"node": ">=6"
}
},
+ "node_modules/terser": {
+ "version": "5.39.1",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.1.tgz",
+ "integrity": "sha512-Mm6+uad0ZuDtcV8/4uOZQDQ8RuiC5Pu+iZRedJtF7yA/27sPL7d++In/AJKpWZlU3SYMPPkVfwetn6sgZ66pUA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@jridgewell/source-map": "^0.3.3",
+ "acorn": "^8.8.2",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/terser-webpack-plugin": {
+ "version": "5.3.14",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz",
+ "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "jest-worker": "^27.4.5",
+ "schema-utils": "^4.3.0",
+ "serialize-javascript": "^6.0.2",
+ "terser": "^5.31.1"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "uglify-js": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/terser/node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/text-extensions": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz",
@@ -7801,12 +8473,108 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/watchpack": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
+ "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.1.2"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
+ "node_modules/webpack": {
+ "version": "5.99.8",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz",
+ "integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/eslint-scope": "^3.7.7",
+ "@types/estree": "^1.0.6",
+ "@types/json-schema": "^7.0.15",
+ "@webassemblyjs/ast": "^1.14.1",
+ "@webassemblyjs/wasm-edit": "^1.14.1",
+ "@webassemblyjs/wasm-parser": "^1.14.1",
+ "acorn": "^8.14.0",
+ "browserslist": "^4.24.0",
+ "chrome-trace-event": "^1.0.2",
+ "enhanced-resolve": "^5.17.1",
+ "es-module-lexer": "^1.2.1",
+ "eslint-scope": "5.1.1",
+ "events": "^3.2.0",
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.2.11",
+ "json-parse-even-better-errors": "^2.3.1",
+ "loader-runner": "^4.2.0",
+ "mime-types": "^2.1.27",
+ "neo-async": "^2.6.2",
+ "schema-utils": "^4.3.2",
+ "tapable": "^2.1.1",
+ "terser-webpack-plugin": "^5.3.11",
+ "watchpack": "^2.4.1",
+ "webpack-sources": "^3.2.3"
+ },
+ "bin": {
+ "webpack": "bin/webpack.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependenciesMeta": {
+ "webpack-cli": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-sources": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
+ "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/webpack/node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/webpack/node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
diff --git a/package.json b/package.json
index e20b867b..9e07f72d 100644
--- a/package.json
+++ b/package.json
@@ -15,9 +15,13 @@
"test-all": "npm run check-format && npm run check-lint && npm run check-types && npm run build"
},
"dependencies": {
+ "@fortawesome/fontawesome-svg-core": "^6.7.2",
+ "@fortawesome/free-brands-svg-icons": "^6.7.2",
+ "@fortawesome/react-fontawesome": "^0.2.2",
"@supabase/supabase-js": "^2.49.4",
"autoprefixer": "^10.4.21",
"axios": "^1.8.4",
+ "framer-motion": "^12.12.1",
"next": "15.3.1",
"postcss": "^8.5.3",
"react": "^19.0.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 70b7442b..d45e67c4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,15 @@ importers:
.:
dependencies:
+ '@fortawesome/fontawesome-svg-core':
+ specifier: ^6.7.2
+ version: 6.7.2
+ '@fortawesome/free-brands-svg-icons':
+ specifier: ^6.7.2
+ version: 6.7.2
+ '@fortawesome/react-fontawesome':
+ specifier: ^0.2.2
+ version: 0.2.2(@fortawesome/fontawesome-svg-core@6.7.2)(react@19.1.0)
'@supabase/supabase-js':
specifier: ^2.49.4
version: 2.49.4
@@ -17,6 +26,9 @@ importers:
axios:
specifier: ^1.8.4
version: 1.8.4
+ framer-motion:
+ specifier: ^12.12.1
+ version: 12.12.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
next:
specifier: 15.3.1
version: 15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -218,6 +230,24 @@ packages:
resolution: {integrity: sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@fortawesome/fontawesome-common-types@6.7.2':
+ resolution: {integrity: sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==}
+ engines: {node: '>=6'}
+
+ '@fortawesome/fontawesome-svg-core@6.7.2':
+ resolution: {integrity: sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==}
+ engines: {node: '>=6'}
+
+ '@fortawesome/free-brands-svg-icons@6.7.2':
+ resolution: {integrity: sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q==}
+ engines: {node: '>=6'}
+
+ '@fortawesome/react-fontawesome@0.2.2':
+ resolution: {integrity: sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==}
+ peerDependencies:
+ '@fortawesome/fontawesome-svg-core': ~1 || ~6
+ react: '>=16.3'
+
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@@ -1444,6 +1474,20 @@ packages:
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
+ framer-motion@12.12.1:
+ resolution: {integrity: sha512-PFw4/GCREHI2suK/NlPSUxd+x6Rkp80uQsfCRFSOQNrm5pZif7eGtmG1VaD/UF1fW9tRBy5AaS77StatB3OJDg==}
+ peerDependencies:
+ '@emotion/is-prop-valid': '*'
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@emotion/is-prop-valid':
+ optional: true
+ react:
+ optional: true
+ react-dom:
+ optional: true
+
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
@@ -1949,6 +1993,12 @@ packages:
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+ motion-dom@12.12.1:
+ resolution: {integrity: sha512-GXq/uUbZBEiFFE+K1Z/sxdPdadMdfJ/jmBALDfIuHGi0NmtealLOfH9FqT+6aNPgVx8ilq0DtYmyQlo6Uj9LKQ==}
+
+ motion-utils@12.12.1:
+ resolution: {integrity: sha512-f9qiqUHm7hWSLlNW8gS9pisnsN7CRFRD58vNjptKdsqFLpkVnX00TNeD6Q0d27V9KzT7ySFyK1TZ/DShfVOv6w==}
+
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -2801,6 +2851,22 @@ snapshots:
'@eslint/core': 0.13.0
levn: 0.4.1
+ '@fortawesome/fontawesome-common-types@6.7.2': {}
+
+ '@fortawesome/fontawesome-svg-core@6.7.2':
+ dependencies:
+ '@fortawesome/fontawesome-common-types': 6.7.2
+
+ '@fortawesome/free-brands-svg-icons@6.7.2':
+ dependencies:
+ '@fortawesome/fontawesome-common-types': 6.7.2
+
+ '@fortawesome/react-fontawesome@0.2.2(@fortawesome/fontawesome-svg-core@6.7.2)(react@19.1.0)':
+ dependencies:
+ '@fortawesome/fontawesome-svg-core': 6.7.2
+ prop-types: 15.8.1
+ react: 19.1.0
+
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.6':
@@ -4152,6 +4218,15 @@ snapshots:
fraction.js@4.3.7: {}
+ framer-motion@12.12.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
+ dependencies:
+ motion-dom: 12.12.1
+ motion-utils: 12.12.1
+ tslib: 2.8.1
+ optionalDependencies:
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+
function-bind@1.1.2: {}
function.prototype.name@1.1.8:
@@ -4627,6 +4702,12 @@ snapshots:
minimist@1.2.8: {}
+ motion-dom@12.12.1:
+ dependencies:
+ motion-utils: 12.12.1
+
+ motion-utils@12.12.1: {}
+
ms@2.1.3: {}
nanoid@3.3.11: {}
diff --git a/public/assets/home/Alarm.svg b/public/assets/home/Alarm.svg
new file mode 100644
index 00000000..eb6dbd3d
--- /dev/null
+++ b/public/assets/home/Alarm.svg
@@ -0,0 +1,10 @@
+
diff --git a/public/assets/home/Arrow.svg b/public/assets/home/Arrow.svg
new file mode 100644
index 00000000..450b8221
--- /dev/null
+++ b/public/assets/home/Arrow.svg
@@ -0,0 +1,4 @@
+
diff --git a/public/assets/home/Facebook.svg b/public/assets/home/Facebook.svg
new file mode 100644
index 00000000..9eee60f4
--- /dev/null
+++ b/public/assets/home/Facebook.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/assets/home/Heart.svg b/public/assets/home/Heart.svg
new file mode 100644
index 00000000..34ce4597
--- /dev/null
+++ b/public/assets/home/Heart.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/assets/home/HeroSection.png b/public/assets/home/HeroSection.png
new file mode 100644
index 00000000..73530d80
Binary files /dev/null and b/public/assets/home/HeroSection.png differ
diff --git a/public/assets/home/Linkedin.svg b/public/assets/home/Linkedin.svg
new file mode 100644
index 00000000..7d33079e
--- /dev/null
+++ b/public/assets/home/Linkedin.svg
@@ -0,0 +1,4 @@
+
diff --git a/public/assets/home/Mail.svg b/public/assets/home/Mail.svg
new file mode 100644
index 00000000..751c0659
--- /dev/null
+++ b/public/assets/home/Mail.svg
@@ -0,0 +1,7 @@
+
diff --git a/public/assets/home/TopTrend.png b/public/assets/home/TopTrend.png
new file mode 100644
index 00000000..2dbe78b0
Binary files /dev/null and b/public/assets/home/TopTrend.png differ
diff --git a/src/app/ClientLayout.tsx b/src/app/ClientLayout.tsx
new file mode 100644
index 00000000..f78aa5a7
--- /dev/null
+++ b/src/app/ClientLayout.tsx
@@ -0,0 +1,16 @@
+'use client';
+
+import ModalContainer from '@/components/ModalContainer';
+import { ModalProvider } from '@/context/ModalContext';
+import '@/lib/fontawesome';
+
+export default function ClientLayout({ children }: { readonly children: React.ReactNode }) {
+ return (
+ <>
+
+ {children}
+
+
+ >
+ );
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index d3ef21d1..418b07ec 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,6 +1,7 @@
import type { Metadata } from 'next';
import { Geist, Geist_Mono } from 'next/font/google';
import './globals.css';
+import ClientLayout from './ClientLayout';
const geistSans = Geist({
variable: '--font-geist-sans',
@@ -13,8 +14,8 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
- title: 'Create Next App',
- description: 'Generated by create next app',
+ title: 'Academix',
+ description: 'AI-Powered Training & Market',
};
export default function RootLayout({
@@ -24,7 +25,9 @@ export default function RootLayout({
}>) {
return (
-
{children}
+
+ {children}
+
);
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 2561492e..7d8aff93 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,8 +1,19 @@
-import Image from 'next/image';
-import Link from 'next/link';
+'use client';
-// Sample course data
-const courses = [
+import { Course } from '@/types/course';
+import Header from '@/components/layout/Header';
+import HeroSection from '@/components/home/HeroSection';
+import CourseGrid from '@/components/home/CourseGrid';
+import WhyStudySection from '@/components/home/WhyStudySection';
+import LearningFocusSection from '@/components/home/LearningFocusSection';
+import ExpertsSection from '@/components/home/ExpertsSection';
+import FutureOfWorkSection from '@/components/home/FutureOfWorkSection';
+import PersonalityTestSection from '@/components/home/PersonalityTestSection';
+import VideoSection from '@/components/home/VideoSection';
+import CategoriesSection from '@/components/home/CategoriesSection';
+import Footer from '@/components/layout/Footer';
+
+const courses: Course[] = [
{
id: '1',
title: 'Thiết kế đồ họa',
@@ -69,358 +80,45 @@ const courses = [
},
];
-interface Course {
- id: string;
- title: string;
- description: string;
- imageUrl: string;
- price: number;
- teacherId: string;
- teacherName: string;
- rating: number;
- totalStudents: number;
- level: string;
- category: string;
- topics: string[];
- createdAt: string;
- updatedAt: string;
-}
-
-// Component for course card grid
-const CourseGrid = ({ title, courses }: { title: string; courses: Course[] }) => (
-
-
{title}
-
- {courses.map((course: Course) => (
-
-
-
-
-
-
{course.title}
-
bởi {course.teacherName}
-
-
- ★
- {course.rating?.toFixed(1)}
-
-
{course.totalStudents} học viên
-
-
- ${course.price.toFixed(2)}
-
- Chi tiết
-
-
-
-
- ))}
-
-
-);
-
export default function Home() {
+
return (
{/* Header */}
-
+
{/* Hero Section */}
-
-
-
-
- All The Skills You Need In One Place
-
-
- From coding skills to business topics.
- Udemy helps expand your professional development.
-
-
- Khám phá
-
-
-
-
-
+
{/* Categories */}
-
-
-
- {['Tất cả', 'IT Certifications', 'Leadership', 'Web Development', 'Communication', 'Business Analytics & Intelligence'].map((category) => (
-
- ))}
-
-
-
+
- {/* Course Listings */}
-
-
-
- {/* Why Study Section */}
-
- Why do you study on EDUIO?
-
-
-
- 01
-
-
-
Learn from Top Industry Experts
-
-
-
- 02
-
-
-
Reasonable Cost, Exceptional Value
-
-
-
- 03
-
-
-
Flexible Learning, Anytime, Anywhere
-
-
-
-
-
-
- {/* Learning Focused */}
-
- Learning focused on your goals
-
-
-
- Hands-on training
-
-
-
-
-
- Certification prep
-
-
-
-
-
- Containerization
-
-
-
-
-
-
- {/* Our Experts */}
-
- Our Experts
-
- {[1, 2, 3].map((i) => (
-
-
-
-
-
Tuyết Trinh
-
Academic Director
-
-
-
-
-
-
-
View Profile
-
-
-
- ))}
-
-
-
- {/* Future of Work */}
-
- Top Trends For The Future Of Work
- Get the skills needed to stay current in today's rapidly changing workplace.
-
- Get Started
-
-
-
- {/* Personality Test */}
-
-
-
Test your personality and interests
-
Explore your strengths, preferences, and discover your growth path with this quick personality quiz.
-
-
-
-
-
-
-
-
-
-
- REALISTIC
- CONVENTIONAL
- INVESTIGATIVE
- ENTERPRISING
- ARTISTIC
- SOCIAL
-
-
- Explore Now
-
-
-
-
- {/* Video Section */}
-
- Top Trends For The Future Of Work
-
-
-
+ {/* Popular Courses */}
+
+
+ {/* Why Study Section */}
+
+
+ {/* Learners are viewing */}
+
+
+ {/* Learning Focused */}
+
+
+ {/* Our Experts */}
+
+
+ {/* Future of Work Section */}
+
+
+ {/* Personality Test Section */}
+
+
+ {/* Video Section */}
+
{/* Footer */}
-
+
);
}
diff --git a/src/components/BubbleButton.tsx b/src/components/BubbleButton.tsx
new file mode 100644
index 00000000..e53a7573
--- /dev/null
+++ b/src/components/BubbleButton.tsx
@@ -0,0 +1,97 @@
+import React, { ReactNode, MouseEvent } from 'react';
+
+interface BubbleButtonProps {
+ children: ReactNode;
+ onClick?: (e: MouseEvent) => void;
+ type?: 'button' | 'submit' | 'reset';
+ disabled?: boolean;
+}
+
+const BubbleButton: React.FC = ({
+ children,
+ onClick,
+ type = 'button',
+ disabled = false,
+}) => {
+ const handleClick = (e: MouseEvent) => {
+ if (!disabled && onClick) {
+ onClick(e);
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default BubbleButton;
diff --git a/src/components/CTAButton.tsx b/src/components/CTAButton.tsx
new file mode 100644
index 00000000..545e547b
--- /dev/null
+++ b/src/components/CTAButton.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+
+interface CtaButtonProps {
+ text: string;
+ onClick?: () => void;
+}
+
+export default function CtaButton({ text, onClick }: CtaButtonProps) {
+ return (
+
+ );
+}
diff --git a/src/components/CourseCard.tsx b/src/components/CourseCard.tsx
deleted file mode 100644
index 11f5fc09..00000000
--- a/src/components/CourseCard.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import Image from 'next/image';
-import Link from 'next/link';
-import { Course } from '@/types/course';
-
-interface CourseCardProps {
- readonly course: Course;
- readonly isTeacher?: boolean;
- readonly onEdit?: (id: string) => void;
- readonly onDelete?: (id: string) => void;
- readonly onEnroll?: (id: string) => void;
-}
-
-export default function CourseCard({
- course,
- isTeacher = false,
- onEdit,
- onDelete,
- onEnroll,
-}: CourseCardProps) {
- return (
-
-
-
-
-
-
{course.title}
-
by {course.teacherName}
-
- {course.rating && (
-
- ★
- {course.rating.toFixed(1)}
-
- )}
- {course.totalStudents && (
-
{course.totalStudents} students
- )}
-
-
{course.description}
-
-
${course.price.toFixed(2)}
-
- {isTeacher ? (
- <>
-
-
- >
- ) : (
-
- )}
-
- Details
-
-
-
-
-
- );
-}
diff --git a/src/components/ModalContainer.tsx b/src/components/ModalContainer.tsx
new file mode 100644
index 00000000..1167c7a1
--- /dev/null
+++ b/src/components/ModalContainer.tsx
@@ -0,0 +1,16 @@
+import { useModal } from '@/context/ModalContext';
+import SignInUpForm from './SignInSignUp';
+
+export default function ModalContainer() {
+ const { modalType, hideModal } = useModal();
+
+ if (!modalType) return null;
+
+ return (
+
+ {(modalType === 'register' || modalType === 'login') && (
+
+ )}
+
+ );
+}
diff --git a/src/components/SignInSignUp.tsx b/src/components/SignInSignUp.tsx
new file mode 100644
index 00000000..bb73f978
--- /dev/null
+++ b/src/components/SignInSignUp.tsx
@@ -0,0 +1,160 @@
+import { useState } from 'react';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faGoogle } from '@fortawesome/free-brands-svg-icons';
+
+interface SignInUpFormProps {
+ onClose: () => void;
+ defaultToSignUp?: boolean;
+}
+
+export default function SignInUpForm({ onClose, defaultToSignUp }: SignInUpFormProps) {
+ const [isRightPanelActive, setIsRightPanelActive] = useState(defaultToSignUp ?? false);
+
+ const handleSignUpClick = () => {
+ setIsRightPanelActive(true);
+ };
+
+ const handleSignInClick = () => {
+ setIsRightPanelActive(false);
+ };
+
+ return (
+
+
+
+ {/* Sign Up Form */}
+
+
+ {/* Sign In Form */}
+
+
+ {/* Overlay Container */}
+
+
+
+
Welcome Back!
+
+ To keep connected with us please login with your personal info
+
+
+
+
+
Hello, Friend!
+
+ Enter your personal details and start journey with us
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/animations/AnimatedSection.tsx b/src/components/animations/AnimatedSection.tsx
new file mode 100644
index 00000000..17a403ea
--- /dev/null
+++ b/src/components/animations/AnimatedSection.tsx
@@ -0,0 +1,36 @@
+import { ReactNode } from 'react';
+import { motion, Variants } from 'framer-motion';
+import { useInView } from 'framer-motion';
+import { useRef } from 'react';
+
+interface AnimatedSectionProps {
+ children: ReactNode;
+ variants: Variants;
+ className?: string;
+ delay?: number;
+ once?: boolean;
+}
+
+export default function AnimatedSection({
+ children,
+ variants,
+ className = '',
+ delay = 0,
+ once = true
+}: AnimatedSectionProps) {
+ const ref = useRef(null);
+ const isInView = useInView(ref, { once });
+
+ return (
+
+ {children}
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/common/Button.tsx b/src/components/common/Button.tsx
new file mode 100644
index 00000000..13dcde35
--- /dev/null
+++ b/src/components/common/Button.tsx
@@ -0,0 +1,56 @@
+"use client";
+
+import Link from 'next/link';
+import { ReactNode } from 'react';
+
+interface ButtonProps {
+ href?: string;
+ onClick?: () => void;
+ children: ReactNode;
+ variant?: 'primary' | 'secondary' | 'outline';
+ className?: string;
+ showArrow?: boolean;
+}
+
+const Button = ({
+ href,
+ onClick,
+ children,
+ variant = 'primary',
+ className = '',
+ showArrow = true
+}: ButtonProps) => {
+ const baseStyles = 'inline-flex items-center rounded-full text-center';
+
+ const variantStyles = {
+ primary: 'bg-blue-600 text-white hover:bg-blue-700',
+ secondary: 'bg-gray-100 text-gray-700 hover:bg-gray-200',
+ outline: 'border border-blue-600 text-blue-600 hover:bg-blue-50'
+ };
+
+ const buttonStyles = `${baseStyles} ${variantStyles[variant]} ${className}`;
+
+ const arrowIcon = showArrow ? (
+
+ ) : null;
+
+ if (href) {
+ return (
+
+ {children}
+ {arrowIcon}
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+export default Button;
\ No newline at end of file
diff --git a/src/components/common/CourseCard.tsx b/src/components/common/CourseCard.tsx
new file mode 100644
index 00000000..4b769ec1
--- /dev/null
+++ b/src/components/common/CourseCard.tsx
@@ -0,0 +1,97 @@
+"use client";
+
+import Image from 'next/image';
+import Link from 'next/link';
+import { Course } from '@/types/course';
+import { motion } from 'framer-motion';
+import { hoverScale } from '@/utils/animations';
+
+interface CourseCardProps {
+ readonly course: Course;
+}
+
+export default function CourseCard({
+ course,
+}: CourseCardProps) {
+ return (
+
+
+
+ Hot Sale!
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {course.level ?? 'All Levels'}
+
+
+
{course.title}
+
+ By {course.teacherName}
+
+
+ {course.rating && (
+
+ ★
+ ★
+ ★
+ ★
+ ★
+ {course.rating.toFixed(1)}
+
+ )}
+ {course.totalStudents && (
+
+ {course.totalStudents} students
+
+ )}
+
+
+ ${course.price.toFixed(2)}
+
+
+ View Details
+
+
+
+
+
+ );
+}
diff --git a/src/components/common/ExpertCard.tsx b/src/components/common/ExpertCard.tsx
new file mode 100644
index 00000000..80a8bd65
--- /dev/null
+++ b/src/components/common/ExpertCard.tsx
@@ -0,0 +1,65 @@
+"use client";
+
+import Image from 'next/image';
+import Link from 'next/link';
+import { motion } from 'framer-motion';
+import { hoverScale } from '@/utils/animations';
+
+interface ExpertCardProps {
+ name: string;
+ role: string;
+ imageUrl: string;
+ profileUrl?: string;
+}
+
+const ExpertCard = ({ name, role, imageUrl, profileUrl = '#' }: ExpertCardProps) => {
+ return (
+
+
+
+
+ {name}
+ {role}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ View Profile
+
+
+
+
+ );
+};
+
+export default ExpertCard;
\ No newline at end of file
diff --git a/src/components/home/CategoriesSection.tsx b/src/components/home/CategoriesSection.tsx
new file mode 100644
index 00000000..68bc7b16
--- /dev/null
+++ b/src/components/home/CategoriesSection.tsx
@@ -0,0 +1,48 @@
+"use client";
+
+import { motion } from 'framer-motion';
+import AnimatedSection from '@/components/animations/AnimatedSection';
+import { fadeIn, staggerContainer } from '@/utils/animations';
+
+const CategoriesSection = () => {
+ const categories = [
+ 'Tất cả',
+ 'IT Certifications',
+ 'Leadership',
+ 'Web Development',
+ 'Communication',
+ 'Business Analytics & Intelligence'
+ ];
+
+ return (
+
+
+
+ {categories.map((category, index) => (
+
+ {category}
+
+ ))}
+
+
+
+ );
+};
+
+export default CategoriesSection;
\ No newline at end of file
diff --git a/src/components/home/CourseGrid.tsx b/src/components/home/CourseGrid.tsx
new file mode 100644
index 00000000..0e769194
--- /dev/null
+++ b/src/components/home/CourseGrid.tsx
@@ -0,0 +1,43 @@
+"use client";
+
+import { Course } from '@/types/course';
+import CourseCard from '@/components/common/CourseCard';
+import AnimatedSection from '@/components/animations/AnimatedSection';
+import { motion } from 'framer-motion';
+import { fadeIn, staggerContainer } from '@/utils/animations';
+
+interface CourseGridProps {
+ title: string;
+ courses: Course[];
+}
+
+const CourseGrid = ({ title, courses }: CourseGridProps) => {
+ return (
+
+
+
+ {title}
+
+
+ {courses.map((course, index) => (
+
+
+
+ ))}
+
+
+
+ );
+};
+
+export default CourseGrid;
\ No newline at end of file
diff --git a/src/components/home/ExpertsSection.tsx b/src/components/home/ExpertsSection.tsx
new file mode 100644
index 00000000..9acd932b
--- /dev/null
+++ b/src/components/home/ExpertsSection.tsx
@@ -0,0 +1,74 @@
+"use client";
+
+import ExpertCard from '@/components/common/ExpertCard';
+import { motion } from 'framer-motion';
+import AnimatedSection from '@/components/animations/AnimatedSection';
+import { fadeIn, staggerContainer } from '@/utils/animations';
+
+interface ExpertsProps {
+ experts?: Array<{
+ id: string;
+ name: string;
+ role: string;
+ imageUrl: string;
+ }>;
+}
+
+const ExpertsSection = ({ experts }: ExpertsProps) => {
+ const defaultExperts = [
+ {
+ id: '1',
+ name: 'Tuyết Trinh',
+ role: 'Academic Director',
+ imageUrl: '/placeholder-course.jpg',
+ },
+ {
+ id: '2',
+ name: 'Tuyết Trinh',
+ role: 'Senior Instructor',
+ imageUrl: '/placeholder-course.jpg',
+ },
+ {
+ id: '3',
+ name: 'Tuyết Trinh',
+ role: 'Technology Lead',
+ imageUrl: '/placeholder-course.jpg',
+ },
+ ];
+
+ const displayExperts = experts || defaultExperts;
+
+ return (
+
+
+
+ Our Experts
+
+
+ {displayExperts.map((expert, index) => (
+
+
+
+ ))}
+
+
+
+ );
+};
+
+export default ExpertsSection;
\ No newline at end of file
diff --git a/src/components/home/FutureOfWorkSection.tsx b/src/components/home/FutureOfWorkSection.tsx
new file mode 100644
index 00000000..3e2ade76
--- /dev/null
+++ b/src/components/home/FutureOfWorkSection.tsx
@@ -0,0 +1,50 @@
+"use client";
+
+import Button from '@/components/common/Button';
+
+interface FutureOfWorkSectionProps {
+ title?: string;
+ subtitle?: string;
+ description?: string;
+ buttonText?: string;
+ buttonUrl?: string;
+}
+
+const FutureOfWorkSection = ({
+ title = 'Top Trends For',
+ subtitle = 'The Future Of Work',
+ description = 'Our 2025 Global Learning & Skills Trends Report is out now! Find out how to build the skills to keep pace with change.',
+ buttonText = 'Get Started',
+ buttonUrl = '/trends'
+}: FutureOfWorkSectionProps) => {
+ return (
+
+
+
+
+
+
+
+
{title}
+
{subtitle}
+
{description}
+
+
+
+
+
+
+ );
+};
+
+export default FutureOfWorkSection;
\ No newline at end of file
diff --git a/src/components/home/HeroSection.tsx b/src/components/home/HeroSection.tsx
new file mode 100644
index 00000000..01551c9c
--- /dev/null
+++ b/src/components/home/HeroSection.tsx
@@ -0,0 +1,67 @@
+"use client";
+
+import Image from 'next/image';
+import Button from '@/components/common/Button';
+import { motion } from 'framer-motion';
+import AnimatedSection from '@/components/animations/AnimatedSection';
+import { slideFromLeft, slideFromRight } from '@/utils/animations';
+
+const HeroSection = () => {
+ return (
+
+
+
+
+ All The Skills You
+ Need In One Place
+
+
+ From coding skills to business topics. Udemy helps
+ expand your professional development.
+
+
+
+
+
+
+
+
+
+ 800
+ +
+
+
+
+ Growth rate
+
+
+
+
+
+ );
+};
+
+export default HeroSection;
\ No newline at end of file
diff --git a/src/components/home/LearningFocusSection.tsx b/src/components/home/LearningFocusSection.tsx
new file mode 100644
index 00000000..6a4c2ee6
--- /dev/null
+++ b/src/components/home/LearningFocusSection.tsx
@@ -0,0 +1,73 @@
+"use client";
+
+import Image from 'next/image';
+import { motion } from 'framer-motion';
+import AnimatedSection from '@/components/animations/AnimatedSection';
+import { fadeIn, slideUp, staggerContainer } from '@/utils/animations';
+
+interface FocusCardProps {
+ title: string;
+ index: number;
+}
+
+const FocusCard = ({ title, index }: FocusCardProps) => (
+
+
+
+
+
+
{title}
+
+
+
+
+
+);
+
+const LearningFocusSection = () => {
+ const focusAreas = [
+ 'Hands-on training',
+ 'Certification prep',
+ 'Containerization'
+ ];
+
+ return (
+
+
+
+ Learning focused on your goals
+
+
+ {focusAreas.map((area, index) => (
+
+ ))}
+
+
+
+ );
+};
+
+export default LearningFocusSection;
\ No newline at end of file
diff --git a/src/components/home/PersonalityTestSection.tsx b/src/components/home/PersonalityTestSection.tsx
new file mode 100644
index 00000000..8fde7077
--- /dev/null
+++ b/src/components/home/PersonalityTestSection.tsx
@@ -0,0 +1,129 @@
+"use client";
+
+import { motion } from 'framer-motion';
+import AnimatedSection from '@/components/animations/AnimatedSection';
+import { slideUp } from '@/utils/animations';
+import Link from 'next/link';
+
+interface PersonalityTestSectionProps {
+ title?: string;
+ buttonText?: string;
+ buttonUrl?: string;
+}
+
+const PersonalityTestSection = ({
+ title = 'Test your personality and interests',
+ buttonText = 'Explore Now',
+ buttonUrl = '/personality-test'
+}: PersonalityTestSectionProps) => {
+ return (
+
+
+
+ {/* Header with title and button */}
+
+
+ {title}
+
+
+
+
+
+ {buttonText} →
+
+
+
+
+
+ {/* Main content */}
+
+ {/* Left side - Empty space */}
+
+
+ {/* Right side - Description */}
+
+
+ Explore your strengths, preferences, and discover your growth path with this quick personality quiz.
+
+
+
+
+ {/* Progress indicator */}
+
+
+ {[0, 1, 2, 3, 4, 5].map((index) => (
+
+ ))}
+
+
+
+
+ {/* Personality types */}
+
+
+ REALISTIC
+ Practical, hands-on, tangible work
+
+
+ CONVENTIONAL
+ Structured, organized, careful
+
+
+ INVESTIGATIVE
+ Practical, hands-on, tangible work
+
+
+ ENTERPRISING
+ Practical, hands-on, tangible work
+
+
+ ARTISTIC
+ Practical, hands-on, tangible work
+
+
+ SOCIAL
+ Practical, hands-on, tangible work
+
+
+
+
+
+ );
+};
+
+export default PersonalityTestSection;
\ No newline at end of file
diff --git a/src/components/home/VideoSection.tsx b/src/components/home/VideoSection.tsx
new file mode 100644
index 00000000..f90f68ec
--- /dev/null
+++ b/src/components/home/VideoSection.tsx
@@ -0,0 +1,100 @@
+"use client";
+
+import React from 'react';
+import Image from 'next/image';
+import { motion } from 'framer-motion';
+import AnimatedSection from '@/components/animations/AnimatedSection';
+import { fadeIn, slideUp } from '@/utils/animations';
+
+const VideoSection: React.FC = () => {
+ return (
+
+
+
+
+ Top Trends For
+
+
+ The Future Of Work
+
+
+
+
+
+
+
+
+
+ 7:32
+
+
+
+
+
+
+
+ Our latest course learning trends have made trainers more efficient with data-driven insights and personalized content.
+
+
+
+
+ );
+};
+
+export default VideoSection;
\ No newline at end of file
diff --git a/src/components/home/WhyStudySection.tsx b/src/components/home/WhyStudySection.tsx
new file mode 100644
index 00000000..4a903b13
--- /dev/null
+++ b/src/components/home/WhyStudySection.tsx
@@ -0,0 +1,89 @@
+"use client";
+
+import Image from 'next/image';
+import { motion } from 'framer-motion';
+import AnimatedSection from '@/components/animations/AnimatedSection';
+import { fadeIn, slideUp, staggerContainer } from '@/utils/animations';
+
+interface ReasonCardProps {
+ number: string;
+ title: string;
+ description: string;
+ index: number;
+}
+
+const ReasonCard = ({ number, title, description, index }: ReasonCardProps) => (
+
+
+
+ {number}
+
+
+
+
+
+
+
+ {title}
+ {description}
+
+);
+
+const WhyStudySection = () => {
+ const reasons = [
+ {
+ number: '01',
+ title: 'Learn from Top Industry Experts',
+ description: 'Get access to high-quality content from industry professionals'
+ },
+ {
+ number: '02',
+ title: 'Reasonable Cost, Exceptional Value',
+ description: 'Affordable pricing with premium features and lifetime access'
+ },
+ {
+ number: '03',
+ title: 'Flexible Learning, Anytime, Anywhere',
+ description: 'Study at your own pace and according to your personal schedule'
+ },
+ ];
+
+ return (
+
+
+
+ Why do you study on EDUIO?
+
+
+ {reasons.map((reason, index) => (
+
+ ))}
+
+
+
+ );
+};
+
+export default WhyStudySection;
\ No newline at end of file
diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx
new file mode 100644
index 00000000..d68e53ba
--- /dev/null
+++ b/src/components/layout/Footer.tsx
@@ -0,0 +1,78 @@
+"use client";
+
+import Link from 'next/link';
+import Image from 'next/image';
+import Button from '@/components/common/Button';
+import { useModal } from '@/context/ModalContext';
+
+const Footer = () => {
+ const { showModal } = useModal();
+
+ return (
+
+ );
+};
+
+export default Footer;
\ No newline at end of file
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx
new file mode 100644
index 00000000..22c96919
--- /dev/null
+++ b/src/components/layout/Header.tsx
@@ -0,0 +1,145 @@
+"use client";
+
+import { useEffect, useRef, useState } from 'react';
+import Link from 'next/link';
+import Button from '@/components/common/Button';
+import { useModal } from '@/context/ModalContext';
+
+interface Category {
+ name: string;
+ href: string;
+}
+
+const Header: React.FC = () => {
+ const [isOpen, setIsOpen] = useState(false);
+ const [isSearchActive, setIsSearchActive] = useState(false);
+ const [isCartOpen, setIsCartOpen] = useState(false);
+ const searchRef = useRef(null);
+ const { showModal } = useModal();
+
+ const categories: Category[] = [
+ { name: "Programming", href: "/categories/programming" },
+ { name: "Business", href: "/categories/business" },
+ { name: "Design", href: "/categories/design" },
+ { name: "Marketing", href: "/categories/marketing" },
+ ];
+
+ useEffect(() => {
+ if (isSearchActive && searchRef.current) {
+ searchRef.current.focus();
+ }
+ }, [isSearchActive]);
+
+ useEffect(() => {
+ const handleEsc = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') setIsSearchActive(false);
+ };
+ window.addEventListener("keydown", handleEsc);
+ return () => window.removeEventListener("keydown", handleEsc);
+ }, []);
+
+ return (
+ <>
+
+
+ {/* Cart Drawer (Push layout) */}
+
+ {/* Nội dung trang ở đây nếu bạn muốn đẩy sang trái khi cart mở */}
+
+
+ {isCartOpen && (
+
+
+
Your Cart
+
+
+
Your cart is empty.
+
+
+ )}
+ >
+ );
+};
+
+export default Header;
diff --git a/src/context/ModalContext.tsx b/src/context/ModalContext.tsx
new file mode 100644
index 00000000..5712a606
--- /dev/null
+++ b/src/context/ModalContext.tsx
@@ -0,0 +1,32 @@
+'use client';
+
+import React, { createContext, useContext, useState } from 'react';
+
+interface ModalContextType {
+ showModal: (type: string) => void;
+ hideModal: () => void;
+ modalType: string | null;
+}
+
+const ModalContext = createContext(undefined);
+
+export const ModalProvider = ({ children }: { children: React.ReactNode }) => {
+ const [modalType, setModalType] = useState(null);
+
+ const showModal = (type: string) => setModalType(type);
+ const hideModal = () => setModalType(null);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useModal = () => {
+ const context = useContext(ModalContext);
+ if (!context) {
+ throw new Error('useModal must be used within a ModalProvider');
+ }
+ return context;
+};
diff --git a/src/lib/fontawesome.ts b/src/lib/fontawesome.ts
new file mode 100644
index 00000000..19a56379
--- /dev/null
+++ b/src/lib/fontawesome.ts
@@ -0,0 +1,5 @@
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faGoogle } from '@fortawesome/free-brands-svg-icons';
+
+// Add icons to the library so they're available in the whole app
+library.add(faGoogle);
\ No newline at end of file
diff --git a/src/utils/animations.ts b/src/utils/animations.ts
new file mode 100644
index 00000000..a89b97a2
--- /dev/null
+++ b/src/utils/animations.ts
@@ -0,0 +1,83 @@
+import { Variants } from 'framer-motion';
+
+// Fade in animation
+export const fadeIn: Variants = {
+ hidden: { opacity: 0 },
+ visible: {
+ opacity: 1,
+ transition: { duration: 0.5 }
+ }
+};
+
+// Slide up animation
+export const slideUp: Variants = {
+ hidden: { y: 50, opacity: 0 },
+ visible: {
+ y: 0,
+ opacity: 1,
+ transition: {
+ type: "spring",
+ stiffness: 100,
+ damping: 15
+ }
+ }
+};
+
+// Slide from left animation
+export const slideFromLeft: Variants = {
+ hidden: { x: -100, opacity: 0 },
+ visible: {
+ x: 0,
+ opacity: 1,
+ transition: {
+ type: "spring",
+ stiffness: 100,
+ damping: 15
+ }
+ }
+};
+
+// Slide from right animation
+export const slideFromRight: Variants = {
+ hidden: { x: 100, opacity: 0 },
+ visible: {
+ x: 0,
+ opacity: 1,
+ transition: {
+ type: "spring",
+ stiffness: 100,
+ damping: 15
+ }
+ }
+};
+
+// Scale animation
+export const scaleUp: Variants = {
+ hidden: { scale: 0.8, opacity: 0 },
+ visible: {
+ scale: 1,
+ opacity: 1,
+ transition: {
+ type: "spring",
+ stiffness: 100,
+ damping: 15
+ }
+ }
+};
+
+// Staggered children animation container
+export const staggerContainer: Variants = {
+ hidden: { opacity: 0 },
+ visible: {
+ opacity: 1,
+ transition: {
+ staggerChildren: 0.2
+ }
+ }
+};
+
+// Hover scale animation
+export const hoverScale = {
+ scale: 1.05,
+ transition: { duration: 0.2 }
+};
\ No newline at end of file