diff --git a/package-lock.json b/package-lock.json index 055222c..c455a2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "@types/react": "18.2.22", "@types/react-dom": "18.2.7", "autoprefixer": "^10.4.15", + "canvas-confetti": "1.9.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.0", @@ -57,9 +58,10 @@ "embla-carousel-react": "^8.3.0", "eslint": "8.49.0", "eslint-config-next": "13.5.1", + "framer-motion": "11.18.0", "input-otp": "^1.2.4", "lucide-react": "^0.446.0", - "next": "13.5.1", + "next": "13.5.11", "next-themes": "^0.3.0", "postcss": "^8.4.30", "react": "18.2.0", @@ -80,6 +82,7 @@ "zod": "^3.24.3" }, "devDependencies": { + "@types/canvas-confetti": "1.6.4", "@types/crypto-js": "^4.2.2", "dotenv": "^17.0.1", "ts-node": "^10.9.2", @@ -1364,9 +1367,9 @@ } }, "node_modules/@next/env": { - "version": "13.5.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.1.tgz", - "integrity": "sha512-CIMWiOTyflFn/GFx33iYXkgLSQsMQZV4jB91qaj/TfxGaGOXxn8C1j72TaUSPIyN7ziS/AYG46kGmnvuk1oOpg==", + "version": "13.5.11", + "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.11.tgz", + "integrity": "sha512-fbb2C7HChgM7CemdCY+y3N1n8pcTKdqtQLbC7/EQtPdLvlMUT9JX/dBYl8MMZAtYG4uVMyPFHXckb68q/NRwqg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1379,9 +1382,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "13.5.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.1.tgz", - "integrity": "sha512-Bcd0VFrLHZnMmJy6LqV1CydZ7lYaBao8YBEdQUVzV8Ypn/l5s//j5ffjfvMzpEQ4mzlAj3fIY+Bmd9NxpWhACw==", + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.9.tgz", + "integrity": "sha512-pVyd8/1y1l5atQRvOaLOvfbmRwefxLhqQOzYo/M7FQ5eaRwA1+wuCn7t39VwEgDd7Aw1+AIWwd+MURXUeXhwDw==", "cpu": [ "arm64" ], @@ -1395,9 +1398,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "13.5.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.1.tgz", - "integrity": "sha512-uvTZrZa4D0bdWa1jJ7X1tBGIxzpqSnw/ATxWvoRO9CVBvXSx87JyuISY+BWsfLFF59IRodESdeZwkWM2l6+Kjg==", + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.9.tgz", + "integrity": "sha512-DwdeJqP7v8wmoyTWPbPVodTwCybBZa02xjSJ6YQFIFZFZ7dFgrieKW4Eo0GoIcOJq5+JxkQyejmI+8zwDp3pwA==", "cpu": [ "x64" ], @@ -1411,9 +1414,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "13.5.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.1.tgz", - "integrity": "sha512-/52ThlqdORPQt3+AlMoO+omicdYyUEDeRDGPAj86ULpV4dg+/GCFCKAmFWT0Q4zChFwsAoZUECLcKbRdcc0SNg==", + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.9.tgz", + "integrity": "sha512-wdQsKsIsGSNdFojvjW3Ozrh8Q00+GqL3wTaMjDkQxVtRbAqfFBtrLPO0IuWChVUP2UeuQcHpVeUvu0YgOP00+g==", "cpu": [ "arm64" ], @@ -1427,9 +1430,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "13.5.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.1.tgz", - "integrity": "sha512-L4qNXSOHeu1hEAeeNsBgIYVnvm0gg9fj2O2Yx/qawgQEGuFBfcKqlmIE/Vp8z6gwlppxz5d7v6pmHs1NB6R37w==", + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.9.tgz", + "integrity": "sha512-6VpS+bodQqzOeCwGxoimlRoosiWlSc0C224I7SQWJZoyJuT1ChNCo+45QQH+/GtbR/s7nhaUqmiHdzZC9TXnXA==", "cpu": [ "arm64" ], @@ -1443,9 +1446,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "13.5.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.1.tgz", - "integrity": "sha512-QVvMrlrFFYvLtABk092kcZ5Mzlmsk2+SV3xYuAu8sbTuIoh0U2+HGNhVklmuYCuM3DAAxdiMQTNlRQmNH11udw==", + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.9.tgz", + "integrity": "sha512-XxG3yj61WDd28NA8gFASIR+2viQaYZEFQagEodhI/R49gXWnYhiflTeeEmCn7Vgnxa/OfK81h1gvhUZ66lozpw==", "cpu": [ "x64" ], @@ -1459,9 +1462,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "13.5.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.1.tgz", - "integrity": "sha512-bBnr+XuWc28r9e8gQ35XBtyi5KLHLhTbEvrSgcWna8atI48sNggjIK8IyiEBO3KIrcUVXYkldAzGXPEYMnKt1g==", + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.9.tgz", + "integrity": "sha512-/dnscWqfO3+U8asd+Fc6dwL2l9AZDl7eKtPNKW8mKLh4Y4wOpjJiamhe8Dx+D+Oq0GYVjuW0WwjIxYWVozt2bA==", "cpu": [ "x64" ], @@ -1480,9 +1483,9 @@ "integrity": "sha512-aDH8VVNfzv2UvwMMw8LOdzlWu514TOprKWZt+5CPiCeGhN0N5uqVpj5oysQKY/WUkeVzOM+Mk9fg8GxRTSjBcw==" }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "13.5.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.1.tgz", - "integrity": "sha512-EQGeE4S5c9v06jje9gr4UlxqUEA+zrsgPi6kg9VwR+dQHirzbnVJISF69UfKVkmLntknZJJI9XpWPB6q0Z7mTg==", + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.9.tgz", + "integrity": "sha512-T/iPnyurOK5a4HRUcxAlss8uzoEf5h9tkd+W2dSWAfzxv8WLKlUgbfk+DH43JY3Gc2xK5URLuXrxDZ2mGfk/jw==", "cpu": [ "arm64" ], @@ -1496,9 +1499,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "13.5.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.1.tgz", - "integrity": "sha512-1y31Q6awzofVjmbTLtRl92OX3s+W0ZfO8AP8fTnITcIo9a6ATDc/eqa08fd6tSpFu6IFpxOBbdevOjwYTGx/AQ==", + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.9.tgz", + "integrity": "sha512-BLiPKJomaPrTAb7ykjA0LPcuuNMLDVK177Z1xe0nAem33+9FIayU4k/OWrtSn9SAJW/U60+1hoey5z+KCHdRLQ==", "cpu": [ "ia32" ], @@ -1512,9 +1515,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "13.5.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.1.tgz", - "integrity": "sha512-+9XBQizy7X/GuwNegq+5QkkxAPV7SBsIwapVRQd9WSvvU20YO23B3bZUpevdabi4fsd25y9RJDDncljy/V54ww==", + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.9.tgz", + "integrity": "sha512-/72/dZfjXXNY/u+n8gqZDjI6rxKMpYsgBBYNZKWOQw0BpBF7WCnPflRy3ZtvQ2+IYI3ZH2bPyj7K+6a6wNk90Q==", "cpu": [ "x64" ], @@ -3390,6 +3393,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/canvas-confetti": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.6.4.tgz", + "integrity": "sha512-fNyZ/Fdw/Y92X0vv7B+BD6ysHL4xVU5dJcgzgxLdGbn8O3PezZNIJpml44lKM0nsGur+o/6+NZbZeNTt00U1uA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/crypto-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", @@ -3621,9 +3631,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -3946,9 +3956,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -4317,9 +4327,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -4466,6 +4476,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas-confetti": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.3.tgz", + "integrity": "sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==", + "license": "ISC", + "funding": { + "type": "donate", + "url": "https://www.paypal.me/kirilvatev" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4940,9 +4960,9 @@ "license": "Apache-2.0" }, "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "devOptional": true, "license": "BSD-3-Clause", "engines": { @@ -5856,9 +5876,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "license": "ISC" }, "node_modules/for-each": { @@ -5905,6 +5925,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.0.tgz", + "integrity": "sha512-Vmjl5Al7XqKHzDFnVqzi1H9hzn5w4eN/bdqXTymVpU2UuMQuz9w6UPdsL9dFBeH7loBlnu4qcEXME+nvbkcIOw==", + "license": "MIT", + "dependencies": { + "motion-dom": "^11.16.4", + "motion-utils": "^11.16.0", + "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/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -6818,9 +6865,19 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -6966,9 +7023,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash.merge": { @@ -7043,9 +7100,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -7072,6 +7129,21 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/motion-dom": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", + "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^11.18.1" + } + }, + "node_modules/motion-utils": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", + "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -7090,9 +7162,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "funding": [ { "type": "github", @@ -7129,19 +7201,18 @@ "license": "MIT" }, "node_modules/next": { - "version": "13.5.1", - "resolved": "https://registry.npmjs.org/next/-/next-13.5.1.tgz", - "integrity": "sha512-GIudNR7ggGUZoIL79mSZcxbXK9f5pwAIPZxEM8+j2yLqv5RODg4TkmUlaKSYVqE1bPQueamXSqdC3j7axiTSEg==", + "version": "13.5.11", + "resolved": "https://registry.npmjs.org/next/-/next-13.5.11.tgz", + "integrity": "sha512-WUPJ6WbAX9tdC86kGTu92qkrRdgRqVrY++nwM+shmWQwmyxt4zhZfR59moXSI4N8GDYCBY3lIAqhzjDd4rTC8Q==", "license": "MIT", "dependencies": { - "@next/env": "13.5.1", + "@next/env": "13.5.11", "@swc/helpers": "0.5.2", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001406", - "postcss": "8.4.14", + "postcss": "8.4.31", "styled-jsx": "5.1.1", - "watchpack": "2.4.0", - "zod": "3.21.4" + "watchpack": "2.4.0" }, "bin": { "next": "dist/bin/next" @@ -7150,15 +7221,15 @@ "node": ">=16.14.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "13.5.1", - "@next/swc-darwin-x64": "13.5.1", - "@next/swc-linux-arm64-gnu": "13.5.1", - "@next/swc-linux-arm64-musl": "13.5.1", - "@next/swc-linux-x64-gnu": "13.5.1", - "@next/swc-linux-x64-musl": "13.5.1", - "@next/swc-win32-arm64-msvc": "13.5.1", - "@next/swc-win32-ia32-msvc": "13.5.1", - "@next/swc-win32-x64-msvc": "13.5.1" + "@next/swc-darwin-arm64": "13.5.9", + "@next/swc-darwin-x64": "13.5.9", + "@next/swc-linux-arm64-gnu": "13.5.9", + "@next/swc-linux-arm64-musl": "13.5.9", + "@next/swc-linux-x64-gnu": "13.5.9", + "@next/swc-linux-x64-musl": "13.5.9", + "@next/swc-win32-arm64-msvc": "13.5.9", + "@next/swc-win32-ia32-msvc": "13.5.9", + "@next/swc-win32-x64-msvc": "13.5.9" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -7195,9 +7266,9 @@ } }, "node_modules/next/node_modules/postcss": { - "version": "8.4.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", - "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "funding": [ { "type": "opencollective", @@ -7206,11 +7277,15 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -7218,15 +7293,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/next/node_modules/zod": { - "version": "3.21.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", - "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -7555,9 +7621,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -7594,9 +7660,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "funding": [ { "type": "opencollective", @@ -7613,7 +7679,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -7693,15 +7759,18 @@ } }, "node_modules/postcss-load-config/node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/postcss-nested": { @@ -8823,18 +8892,19 @@ } }, "node_modules/sucrase/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/sucrase/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -8852,12 +8922,12 @@ } }, "node_modules/sucrase/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -9016,9 +9086,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" @@ -9427,9 +9497,9 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -9720,9 +9790,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -9741,9 +9811,9 @@ } }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "license": "ISC", "engines": { "node": ">= 6" diff --git a/package.json b/package.json index 7d03d34..1e7b09f 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@types/react": "18.2.22", "@types/react-dom": "18.2.7", "autoprefixer": "^10.4.15", + "canvas-confetti": "1.9.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.0", @@ -59,9 +60,10 @@ "embla-carousel-react": "^8.3.0", "eslint": "8.49.0", "eslint-config-next": "13.5.1", + "framer-motion": "11.18.0", "input-otp": "^1.2.4", "lucide-react": "^0.446.0", - "next": "13.5.1", + "next": "13.5.11", "next-themes": "^0.3.0", "postcss": "^8.4.30", "react": "18.2.0", @@ -82,6 +84,7 @@ "zod": "^3.24.3" }, "devDependencies": { + "@types/canvas-confetti": "1.6.4", "@types/crypto-js": "^4.2.2", "dotenv": "^17.0.1", "ts-node": "^10.9.2", diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..cd96e6d --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,22 @@ +{ + "name": "RateMyCourse - IIT Bhilai", + "short_name": "RateMyCourse", + "description": "Rate and review courses and professors at IIT Bhilai", + "start_url": "/", + "display": "standalone", + "background_color": "#0a0a0a", + "theme_color": "#7c3aed", + "orientation": "any", + "icons": [ + { + "src": "/icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..c9dbd6a --- /dev/null +++ b/public/sw.js @@ -0,0 +1,31 @@ +const CACHE_NAME = "ratemycourse-v1"; +const STATIC_ASSETS = ["/", "/courses", "/professors", "/about"]; + +self.addEventListener("install", (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS)) + ); + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))) + ) + ); + self.clients.claim(); +}); + +self.addEventListener("fetch", (event) => { + if (event.request.method !== "GET") return; + event.respondWith( + fetch(event.request) + .then((response) => { + const clone = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone)); + return response; + }) + .catch(() => caches.match(event.request)) + ); +}); diff --git a/src/app/api/auth/anonymize/route.ts b/src/app/api/auth/anonymize/route.ts new file mode 100644 index 0000000..4245b96 --- /dev/null +++ b/src/app/api/auth/anonymize/route.ts @@ -0,0 +1,66 @@ +import { NextResponse } from 'next/server'; +import { randomBytes, pbkdf2 } from 'crypto'; +import { promisify } from 'util'; +import { v4 as uuidv4 } from 'uuid'; +import { createClient } from '@/utils/supabase/server'; + +export const runtime = 'nodejs'; + +const pbkdf2Async = promisify(pbkdf2); + +export async function POST() { + const supabase = await createClient(); + const { data: { session } } = await supabase.auth.getSession(); + + if (!session?.user?.id || !session.user.email) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const authId = session.user.id; + const email = session.user.email; + + const { data: existing, error: lookupError } = await supabase + .from('users') + .select('anonymous_id') + .eq('auth_id', authId) + .maybeSingle(); + + if (lookupError) { + return NextResponse.json({ error: 'Lookup failed' }, { status: 500 }); + } + + if (existing) { + return NextResponse.json({ anonymousId: existing.anonymous_id }); + } + + const primarySalt = randomBytes(32).toString('hex'); + const primaryHash = await pbkdf2Async(email, primarySalt, 100000, 64, 'sha512'); + const verificationToken = primaryHash.toString('hex').slice(0, 64); + + const secondarySalt = randomBytes(32).toString('hex'); + const verificationHashBuf = await pbkdf2Async( + verificationToken, + secondarySalt, + 50000, + 64, + 'sha512', + ); + const verificationHash = verificationHashBuf.toString('hex'); + + const anonymousId = uuidv4(); + + const { error: insertError } = await supabase + .from('users') + .insert({ + auth_id: authId, + anonymous_id: anonymousId, + verification_hash: verificationHash, + salt: secondarySalt, + }); + + if (insertError) { + return NextResponse.json({ error: 'Failed to store identity' }, { status: 500 }); + } + + return NextResponse.json({ anonymousId }); +} diff --git a/src/app/compare/page.tsx b/src/app/compare/page.tsx new file mode 100644 index 0000000..513168e --- /dev/null +++ b/src/app/compare/page.tsx @@ -0,0 +1,154 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { supabase } from "@/lib/supabase"; +import { Search, X, ArrowLeftRight } from "lucide-react"; +import { motion } from "framer-motion"; + +interface CourseData { + id: string; + code: string; + title: string; + department: string; + credits: number; + overall_rating: number; + difficulty_rating: number; + workload_rating: number; + review_count: number; +} + +function RatingBar({ label, value, max = 5 }: { label: string; value: number; max?: number }) { + const pct = (value / max) * 100; + const color = value >= 4 ? "bg-green-500" : value >= 3 ? "bg-yellow-500" : value >= 2 ? "bg-orange-500" : "bg-red-500"; + return ( +
+
+ {label} + {value.toFixed(1)} +
+
+ +
+
+ ); +} + +function CourseSelector({ onSelect, selected }: { onSelect: (c: CourseData) => void; selected: CourseData | null }) { + const [query, setQuery] = useState(""); + const [results, setResults] = useState([]); + + useEffect(() => { + if (query.length < 2) { setResults([]); return; } + const t = setTimeout(async () => { + const { data } = await supabase + .from("courses") + .select("id, code, title, department, credits, overall_rating, difficulty_rating, workload_rating, review_count") + .or(`title.ilike.%${query}%,code.ilike.%${query}%`) + .limit(6); + setResults(data || []); + }, 300); + return () => clearTimeout(t); + }, [query]); + + if (selected) { + return ( +
+
+ {selected.code} + +
+

{selected.title}

+

{selected.department} ยท {selected.credits} credits

+
+ ); + } + + return ( +
+
+ + setQuery(e.target.value)} + className="w-full pl-9 pr-4 py-2.5 rounded-lg border border-border/60 bg-card/80 text-sm focus:outline-none focus:ring-2 focus:ring-primary/30" + /> +
+ {results.length > 0 && ( +
+ {results.map((c) => ( + + ))} +
+ )} +
+ ); +} + +export default function ComparePage() { + const [courseA, setCourseA] = useState(null); + const [courseB, setCourseB] = useState(null); + + const canCompare = courseA && courseB; + + return ( +
+
+ +
+
+

+ Compare Courses +

+

Select two courses to compare side by side

+
+ +
+ +
+ +
+ +
+ + {canCompare && ( + + {[courseA, courseB].map((course) => ( +
+
+ {course.code} +

{course.title}

+

{course.department} ยท {course.credits} credits ยท {course.review_count} reviews

+
+
+ + + +
+
+ ))} +
+ )} +
+
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b68de7c..13519c5 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,6 +5,9 @@ import { ThemeProvider } from '@/components/theme/theme-provider'; import './globals.css' import Header from '@/components/layout/Header'; import Footer from '@/components/layout/Footer'; +import ScrollToTop from '@/components/layout/ScrollToTop'; +import PageTransition from '@/components/layout/PageTransition'; +import ServiceWorkerRegister from '@/components/layout/ServiceWorkerRegister'; import { AuthProvider } from '@/contexts/AuthContext'; import { Toaster } from 'react-hot-toast'; const inter = Inter({ subsets: ['latin'] }); @@ -12,6 +15,8 @@ const inter = Inter({ subsets: ['latin'] }); export const metadata: Metadata = { title: 'RateMyCourse - IIT Bhilai', description: 'Find and review courses and professors at IIT Bhilai', + manifest: '/manifest.json', + themeColor: '#7c3aed', }; @@ -31,12 +36,16 @@ export default function RootLayout({ disableTransitionOnChange >
+
-
{children}
+
+ {children} +
{/* Toast notifications */} + diff --git a/src/app/page.tsx b/src/app/page.tsx index 21088db..1e48739 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -4,6 +4,8 @@ import Link from "next/link"; import { buttonVariants } from "@/components/ui/button"; import { BookOpen, Users } from "lucide-react"; import { supabase } from "@/lib/supabase"; +import SearchBar from "@/components/common/SearchBar"; +import TrendingSection from "@/components/common/TrendingSection"; import dynamic from 'next/dynamic'; // Import Typewriter dynamically to prevent SSR issues @@ -122,36 +124,9 @@ export default function Home() {
- {/*
-
- - -
- - Search - -
*/} - {/* - - Write Your Review! - */} +
+ +
@@ -180,6 +155,8 @@ export default function Home() {
+ +
([]); + const [open, setOpen] = useState(false); + const [loading, setLoading] = useState(false); + const ref = useRef(null); + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) { + setOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + useEffect(() => { + if (query.length < 2) { + setResults([]); + setOpen(false); + return; + } + + const timeout = setTimeout(async () => { + setLoading(true); + const q = query.toLowerCase(); + + const [coursesRes, professorsRes] = await Promise.all([ + supabase + .from("courses") + .select("id, code, title, overall_rating, department") + .or(`title.ilike.%${q}%,code.ilike.%${q}%`) + .limit(5), + supabase + .from("professors") + .select("id, name, department, overall_rating, post") + .ilike("name", `%${q}%`) + .limit(5), + ]); + + const mapped: SearchResult[] = [ + ...(coursesRes.data || []).map((c) => ({ + id: c.id, + title: c.title, + subtitle: c.code, + type: "course" as const, + rating: c.overall_rating || 0, + })), + ...(professorsRes.data || []).map((p) => ({ + id: p.id, + title: p.name, + subtitle: p.department, + type: "professor" as const, + rating: p.overall_rating || 0, + })), + ]; + + setResults(mapped); + setOpen(mapped.length > 0); + setLoading(false); + }, 300); + + return () => clearTimeout(timeout); + }, [query]); + + return ( +
+
+ + setQuery(e.target.value)} + onFocus={() => results.length > 0 && setOpen(true)} + className="w-full pl-11 pr-10 py-3 rounded-xl border border-border/60 bg-card/80 backdrop-blur-sm text-sm font-medium placeholder:text-muted-foreground/60 focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary/50 transition-all" + /> + {query && ( + + )} +
+ + + {open && ( + + {loading ? ( +
Searching...
+ ) : ( +
+ {results.map((r) => ( + { setOpen(false); setQuery(""); }} + className="flex items-center gap-3 px-4 py-3 hover:bg-accent/50 transition-colors border-b border-border/20 last:border-0" + > +
+ {r.type === "course" ? ( + + ) : ( + + )} +
+
+

{r.title}

+

{r.subtitle}

+
+
+ โ˜… {r.rating.toFixed(1)} +
+ + ))} +
+ )} +
+ )} +
+
+ ); +} diff --git a/src/components/common/TrendingSection.tsx b/src/components/common/TrendingSection.tsx new file mode 100644 index 0000000..326fa47 --- /dev/null +++ b/src/components/common/TrendingSection.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { supabase } from "@/lib/supabase"; +import { Flame, Star } from "lucide-react"; + +interface TrendingItem { + id: string; + title: string; + code?: string; + rating: number; + review_count: number; +} + +export default function TrendingSection() { + const [courses, setCourses] = useState([]); + + useEffect(() => { + const fetch = async () => { + const { data } = await supabase + .from("courses") + .select("id, title, code, overall_rating, review_count") + .order("review_count", { ascending: false }) + .limit(6); + if (data) { + setCourses(data.map((c) => ({ id: c.id, title: c.title, code: c.code, rating: c.overall_rating || 0, review_count: c.review_count || 0 }))); + } + }; + fetch(); + }, []); + + if (courses.length === 0) return null; + + return ( +
+
+ +

Most Reviewed

+
+
+ {courses.map((c) => ( + +
+ {c.code} +
+ + {c.rating.toFixed(1)} +
+
+

{c.title}

+

{c.review_count} reviews

+ + ))} +
+
+ ); +} diff --git a/src/components/courses/course_page/CoursePageReviews.tsx b/src/components/courses/course_page/CoursePageReviews.tsx index f5670c9..5b80dee 100644 --- a/src/components/courses/course_page/CoursePageReviews.tsx +++ b/src/components/courses/course_page/CoursePageReviews.tsx @@ -4,6 +4,7 @@ import React, { useEffect, useState } from "react"; import AddReviewButton from "../AddReviewButton"; import { ChevronRight, ChevronDown } from "lucide-react"; import { supabase } from "@/lib/supabase"; +import { ReviewSkeleton } from "@/components/ui/skeleton"; interface CoursePageReviewsProps { id: string | null; // Course UUID @@ -100,9 +101,11 @@ const CoursePageReviews = ({ id, reviewCount }: CoursePageReviewsProps) => { {/* Reviews list */}
{loading ? ( -

- Loading reviews... -

+
+ + + +
) : reviews.length === 0 ? (

No reviews yet for this course. diff --git a/src/components/courses/course_page/RateThisCourse.tsx b/src/components/courses/course_page/RateThisCourse.tsx index 5650e89..d816ec6 100644 --- a/src/components/courses/course_page/RateThisCourse.tsx +++ b/src/components/courses/course_page/RateThisCourse.tsx @@ -8,6 +8,7 @@ import Box from '@mui/material/Box'; import { supabase } from '@/lib/supabase'; import { useAuth } from '@/contexts/AuthContext'; import toast from 'react-hot-toast'; +import confetti from 'canvas-confetti'; // ---------------- STAR SELECTOR ---------------- const StarSelector = ({ rating, setRating }: { rating: number; setRating: (val: number) => void }) => { @@ -24,12 +25,12 @@ const StarSelector = ({ rating, setRating }: { rating: number; setRating: (val: const id = `half-grad-${i}`; stars.push( -

setHovered(null)}> +
setHovered(null)}>
{stars}
; + return
{stars}
; }; // ---------------- SLIDER ---------------- @@ -183,6 +184,7 @@ const RateThisCourse = ({ courseId }: { courseId: string }) => { toast.error(`Failed to submit rating: ${insertError.message}`); } else { toast.success('Rating submitted successfully!'); + confetti({ particleCount: 100, spread: 70, origin: { y: 0.6 } }); setHasSubmitted(true); } } catch (error) { diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 85de192..8859568 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -4,6 +4,7 @@ import { BookOpen, Star } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { ModeToggle } from '@/components/theme/mode-toggle'; import { LoginButton } from '@/components/auth/login-button'; +import MobileNav from './MobileNav'; export default function Header() { return ( @@ -30,6 +31,8 @@ export default function Header() { {/* Modern action buttons */}
+ + {/* Theme Toggle */} diff --git a/src/components/layout/MobileNav.tsx b/src/components/layout/MobileNav.tsx new file mode 100644 index 0000000..214d332 --- /dev/null +++ b/src/components/layout/MobileNav.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { Menu, X, BookOpen, Users, Info, LayoutDashboard, ArrowLeftRight } from "lucide-react"; +import { motion, AnimatePresence } from "framer-motion"; + +const navLinks = [ + { href: "/courses", label: "Courses", icon: BookOpen }, + { href: "/professors", label: "Professors", icon: Users }, + { href: "/compare", label: "Compare", icon: ArrowLeftRight }, + { href: "/dashboard", label: "Dashboard", icon: LayoutDashboard }, + { href: "/about", label: "About", icon: Info }, +]; + +export default function MobileNav() { + const [open, setOpen] = useState(false); + + return ( +
+ + + + {open && ( + + + + )} + +
+ ); +} diff --git a/src/components/layout/PageTransition.tsx b/src/components/layout/PageTransition.tsx new file mode 100644 index 0000000..664fdf0 --- /dev/null +++ b/src/components/layout/PageTransition.tsx @@ -0,0 +1,15 @@ +"use client"; + +import { motion } from "framer-motion"; + +export default function PageTransition({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/src/components/layout/ScrollToTop.tsx b/src/components/layout/ScrollToTop.tsx new file mode 100644 index 0000000..b3e00e2 --- /dev/null +++ b/src/components/layout/ScrollToTop.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { usePathname } from "next/navigation"; +import { useEffect } from "react"; + +export default function ScrollToTop() { + const pathname = usePathname(); + + useEffect(() => { + window.scrollTo(0, 0); + }, [pathname]); + + return null; +} diff --git a/src/components/layout/ServiceWorkerRegister.tsx b/src/components/layout/ServiceWorkerRegister.tsx new file mode 100644 index 0000000..03adfd4 --- /dev/null +++ b/src/components/layout/ServiceWorkerRegister.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { useEffect } from "react"; + +export default function ServiceWorkerRegister() { + useEffect(() => { + if ("serviceWorker" in navigator) { + navigator.serviceWorker.register("/sw.js").catch(() => {}); + } + }, []); + return null; +} diff --git a/src/components/professors/ProfessorRadar.tsx b/src/components/professors/ProfessorRadar.tsx new file mode 100644 index 0000000..8e6430b --- /dev/null +++ b/src/components/professors/ProfessorRadar.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { ResponsiveContainer, RadarChart, PolarGrid, PolarAngleAxis, Radar } from "recharts"; + +interface ProfessorRadarProps { + overall: number; + knowledge: number; + teaching: number; + approachability: number; +} + +export default function ProfessorRadar({ overall, knowledge, teaching, approachability }: ProfessorRadarProps) { + const data = [ + { subject: "Overall", value: overall || 0 }, + { subject: "Knowledge", value: knowledge || 0 }, + { subject: "Teaching", value: teaching || 0 }, + { subject: "Approachability", value: approachability || 0 }, + ]; + + return ( +
+ + + + + + + +
+ ); +} diff --git a/src/components/professors/professor_page/ProfessorPageReviews.tsx b/src/components/professors/professor_page/ProfessorPageReviews.tsx index 2dc1f93..68d6285 100644 --- a/src/components/professors/professor_page/ProfessorPageReviews.tsx +++ b/src/components/professors/professor_page/ProfessorPageReviews.tsx @@ -4,6 +4,7 @@ import React, { useEffect, useState } from "react"; import AddReviewButtonProfessor from "@/components/professors/AddReviewButtonProfessor"; import { ChevronRight, ChevronDown } from "lucide-react"; import { supabase } from "@/lib/supabase"; +import { ReviewSkeleton } from "@/components/ui/skeleton"; interface ProfessorPageReviewsProps { id: string; // Professor ID @@ -93,9 +94,11 @@ const ProfessorPageReviews = ({ id, reviewCount }: ProfessorPageReviewsProps) => {/* Reviews List */}
{loading ? ( -

- Loading reviews... -

+
+ + + +
) : reviews.length === 0 ? (

No reviews yet for this professor. diff --git a/src/components/professors/professor_page/ProfessorPageStats.tsx b/src/components/professors/professor_page/ProfessorPageStats.tsx index 1cfa5bd..5dc8ff5 100644 --- a/src/components/professors/professor_page/ProfessorPageStats.tsx +++ b/src/components/professors/professor_page/ProfessorPageStats.tsx @@ -1,6 +1,7 @@ 'use client'; import React from 'react'; import { Star, Check, Award, BookOpen } from 'lucide-react'; +import ProfessorRadar from '../ProfessorRadar'; interface ProfessorPageStatsProps { reviewCount: number; @@ -73,6 +74,14 @@ const ProfessorPageStats = ({

))}
+ {reviewCount > 0 && ( + + )}
); diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx index a626d9b..1089668 100644 --- a/src/components/ui/skeleton.tsx +++ b/src/components/ui/skeleton.tsx @@ -1,15 +1,58 @@ -import { cn } from '@/lib/utils'; +"use client"; -function Skeleton({ - className, - ...props -}: React.HTMLAttributes) { +import { cn } from "@/lib/utils"; + +function Skeleton({ className, ...props }: React.HTMLAttributes) { return (
); } +export function CardSkeleton() { + return ( +
+
+ + +
+ +
+ + +
+ + +
+ ); +} + +export function ReviewSkeleton() { + return ( +
+
+ +
+ + +
+
+ + +
+ ); +} + +export function ListSkeleton({ count = 6 }: { count?: number }) { + return ( +
+ {Array.from({ length: count }).map((_, i) => ( + + ))} +
+ ); +} + export { Skeleton }; diff --git a/src/lib/anonymization.ts b/src/lib/anonymization.ts index 11c9840..8c07834 100644 --- a/src/lib/anonymization.ts +++ b/src/lib/anonymization.ts @@ -1,175 +1,81 @@ -// lib/anonymization.ts -import crypto from 'crypto'; -import { v4 as uuidv4 } from 'uuid'; - -/** - * Core anonymization utilities to ensure complete privacy of user identities - * Even with database access, it should be impossible to link emails to ratings - */ - -// Interfaces -export interface AnonymizationResult { - anonymousId: string; - verificationToken: string; -} - -export interface AnonymousUser { - anonymousId: string; - salt: string; - verificationHash: string; - createdAt: Date; -} - -/** - * Generates a completely anonymous identity from a user's email - * The email is never stored anywhere in the system - */ -export const generateAnonymousIdentity = async (email: string): Promise => { - // Generate a strong random salt - const salt = crypto.randomBytes(32).toString('hex'); - - // Create a high-entropy hash from email + salt using PBKDF2 - // This is slow by design to prevent brute force attacks - // HIGHLIGHT-START - // Reduced from 100000 to 10000 to prevent browser freeze - const hash = crypto.pbkdf2Sync(email, salt, 10000, 64, 'sha512').toString('hex'); - // HIGHLIGHT-END - - // Create an anonymous ID that will be used in ratings table - const anonymousId = uuidv4(); - - // Create a verification token (will be used to verify user without storing email) - const verificationToken = hash.substring(0, 64); - - return { - anonymousId, - verificationToken - }; -}; +export const sanitizeContent = (content: string): string => { + let sanitized = content.replace( + /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, + '[EMAIL REMOVED]', + ); -/** - * Verifies a user's identity without exposing their email - * @param email The user's email (never stored) - * @param storedSalt The salt stored in the verification table - * @param storedVerificationHash The verification hash stored in DB - */ -export const verifyAnonymousIdentity = async ( - email: string, - storedSalt: string, - storedVerificationHash: string -): Promise => { - // Regenerate the hash using the stored salt - // HIGHLIGHT-START - // MUST match the iterations in generateAnonymousIdentity (was 100000) - const hash = crypto.pbkdf2Sync(email, storedSalt, 10000, 64, 'sha512').toString('hex'); - // HIGHLIGHT-END - const verificationToken = hash.substring(0, 64); - - // Use constant-time comparison to prevent timing attacks - return crypto.timingSafeEqual( - new Uint8Array(Buffer.from(verificationToken, 'hex')), - new Uint8Array(Buffer.from(storedVerificationHash, 'hex')) + sanitized = sanitized.replace( + /(\+\d{1,3}\s?)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}/g, + '[PHONE REMOVED]', + ); + + sanitized = sanitized.replace( + /(?:I am|I'm|my name is|this is) ([A-Z][a-z]+ [A-Z][a-z]+)/g, + 'I am [NAME REMOVED]', ); -}; -/** - * Secondary anonymization for data that will be put in ratings - * This function strips personally identifiable information - */ -export const sanitizeContent = (content: string): string => { - // Remove potential emails - let sanitized = content.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '[EMAIL REMOVED]'); - - // Remove potential phone numbers - sanitized = sanitized.replace(/(\+\d{1,3}\s?)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}/g, '[PHONE REMOVED]'); - - // Detect and remove names that might appear based on common patterns - // This is a simplified approach - more advanced NLP would be better - sanitized = sanitized.replace(/(?:I am|I'm|my name is|this is) ([A-Z][a-z]+ [A-Z][a-z]+)/g, 'I am [NAME REMOVED]'); - return sanitized; }; -/** - * Creates a fuzzy timestamp to prevent timing correlation attacks - * Instead of exact timestamps, we use time ranges - */ export const createFuzzyTimestamp = (date: Date = new Date()): string => { - const hour = Math.floor(date.getHours() / 3) * 3; // Round to nearest 3-hour block + const hour = Math.floor(date.getHours() / 3) * 3; const timeRanges = ['morning', 'afternoon', 'evening', 'night']; const timeOfDay = timeRanges[Math.floor(hour / 6)]; - - // Return only month and year with fuzzy time of day + return `${date.toLocaleString('default', { month: 'long' })} ${date.getFullYear()}, ${timeOfDay}`; }; -/** - * Enhanced content sanitization with more robust PII detection - * Ensures no personally identifiable information is stored with ratings - */ export const enhancedSanitizeContent = (content: string): string => { - // Sanitize common PII patterns let sanitized = content; - - // Remove emails with comprehensive pattern matching + sanitized = sanitized.replace( - /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, - '[EMAIL REMOVED]' + /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, + '[EMAIL REMOVED]', ); - - // Remove various phone number formats (international, with/without country codes) + sanitized = sanitized.replace( /(\+\d{1,3}[\s.-]?)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}/g, - '[PHONE REMOVED]' + '[PHONE REMOVED]', ); - - // Remove social security numbers + sanitized = sanitized.replace( /\b\d{3}[-.]?\d{2}[-.]?\d{4}\b/g, - '[SSN REMOVED]' + '[SSN REMOVED]', ); - - // Remove common name patterns + const namePatterns = [ /(?:I am|I'm|my name is|this is|I go by) ([A-Z][a-zA-Z]+(?: [A-Z][a-zA-Z]+)*)/g, /(?:sincerely|regards|from|signed),? ([A-Z][a-zA-Z]+(?: [A-Z][a-zA-Z]+)*)/gi, - /\b(?:Professor|Prof\.|Dr\.|Mr\.|Ms\.|Mrs\.) ([A-Z][a-zA-Z]+(?: [A-Z][a-zA-Z]+)*)/g + /\b(?:Professor|Prof\.|Dr\.|Mr\.|Ms\.|Mrs\.) ([A-Z][a-zA-Z]+(?: [A-Z][a-zA-Z]+)*)/g, ]; - - namePatterns.forEach(pattern => { - sanitized = sanitized.replace(pattern, (match, name) => { - return match.replace(name, '[NAME REMOVED]'); - }); + + namePatterns.forEach((pattern) => { + sanitized = sanitized.replace(pattern, (match, name) => match.replace(name, '[NAME REMOVED]')); }); - - // Remove student IDs + sanitized = sanitized.replace( /\b(student id|id number|id#):? *\d+\b/gi, - '[STUDENT ID REMOVED]' + '[STUDENT ID REMOVED]', ); - - // Remove URLs that might contain identifying information + sanitized = sanitized.replace( /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/g, - '[URL REMOVED]' + '[URL REMOVED]', ); - - // Remove specific academic terms that could identify the time period + const termPatterns = [ /\b(Spring|Fall|Summer|Winter) (20\d{2})\b/g, - /\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\.? (20\d{2})\b/g + /\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\.? (20\d{2})\b/g, ]; - - termPatterns.forEach(pattern => { + + termPatterns.forEach((pattern) => { sanitized = sanitized.replace(pattern, '[TERM REMOVED]'); }); - - // Redact specific course numbers or section identifiers + sanitized = sanitized.replace( /\b([A-Z]{2,4})\s*[-]?\s*(\d{3}[A-Z]?)\s*[-]?\s*(\d{1,3})\b/g, - '[COURSE ID REMOVED]' + '[COURSE ID REMOVED]', ); - - + return sanitized; -}; \ No newline at end of file +}; diff --git a/src/lib/supabase-auth.ts b/src/lib/supabase-auth.ts index daceb34..4282384 100644 --- a/src/lib/supabase-auth.ts +++ b/src/lib/supabase-auth.ts @@ -1,14 +1,6 @@ -// lib/supabase-auth.ts -import { generateAnonymousIdentity, verifyAnonymousIdentity, AnonymousUser } from './anonymization'; -import * as crypto from 'crypto'; -// ๐Ÿ‘‡ **MODIFIED LINE: Import the original shared client** import { supabase } from './supabase'; -/** - * Handle user sign-in with magic link while maintaining complete anonymity - */ -export const signInWithMagicLink = async (email: string): Promise<{error: any | null}> => { - // First, send the magic link email +export const signInWithMagicLink = async (email: string): Promise<{ error: any | null }> => { const { error } = await supabase.auth.signInWithOtp({ email, options: { @@ -19,27 +11,20 @@ export const signInWithMagicLink = async (email: string): Promise<{error: any | return { error }; }; -/** - * Process after user clicks magic link OR completes OAuth and handle anonymous identity creation - */ -export const handleAuthCallback = async (): Promise<{user: any, anonymousId: string | null, error: any | null}> => { - // ๐Ÿ‘‡ Wait until Supabase returns a session with a user ID (max 0.5 seconds) +export const handleAuthCallback = async (): Promise<{ + user: any; + anonymousId: string | null; + error: any | null; +}> => { let session = null; for (let i = 0; i < 10; i++) { - // This getSession() call will now use the correct shared client const { data } = await supabase.auth.getSession(); - console.log(`Callback Attempt ${i}:`, data?.session); session = data?.session; - // โœ… Primarily check for user ID to confirm session if (session?.user?.id) break; - // This is the 50ms delay I added last time, which is fine. await new Promise((r) => setTimeout(r, 50)); } - - // โŒ Still no session with a user ID after retrying if (!session?.user?.id) { - console.error('No valid session with user ID after waiting.'); return { user: null, anonymousId: null, @@ -47,74 +32,49 @@ export const handleAuthCallback = async (): Promise<{user: any, anonymousId: str }; } - // โ“ Check if email exists for anonymous ID generation if (!session.user.email) { - console.error('Session obtained, but user email is missing.'); return { - user: session.user, // Return the user object we have + user: session.user, anonymousId: null, error: new Error('User email is required for anonymous identity generation.'), }; } - // --- Proceed with anonymous identity generation and storage --- - const { anonymousId, verificationToken } = await generateAnonymousIdentity(session.user.email); - - // Add additional entropy with a second salt layer - const secondarySalt = crypto.randomBytes(32).toString('hex'); - - // HIGHLIGHT-START - // This is the main source of the delay. - // Reduced iterations from 50000 to 5000 for a 10x speed boost. - const doubleHashedToken = crypto.pbkdf2Sync( - verificationToken, - secondarySalt, - 5000, // Additional iterations (was 50000) - 64, - 'sha512' - ).toString('hex'); - // HIGHLIGHT-END - - // โœ… Upsert into `users` table using the confirmed user ID - const { error: dbError } = await supabase - .from('users') - .upsert( - { - auth_id: session.user.id, // Use the confirmed user ID - anonymous_id: anonymousId, - verification_hash: doubleHashedToken, - salt: secondarySalt, - created_at: new Date(), - }, - { onConflict: 'auth_id' } // Use auth_id for conflict resolution - ); - - if (dbError) { - console.error("Error upserting user:", dbError); - return { user: session.user, anonymousId: null, error: dbError }; + try { + const response = await fetch('/api/auth/anonymize', { + method: 'POST', + credentials: 'same-origin', + }); + + if (!response.ok) { + const body = await response.json().catch(() => ({} as any)); + return { + user: session.user, + anonymousId: null, + error: new Error(body?.error || `Anonymization failed (${response.status})`), + }; + } + + const { anonymousId } = await response.json(); + return { user: session.user, anonymousId, error: null }; + } catch (err) { + return { user: session.user, anonymousId: null, error: err }; } - - console.log("โœ… User upserted successfully, anonymousId:", anonymousId); - return { - user: session.user, - anonymousId, - error: null, - }; }; - -/** - * Get user's anonymous ID without exposing their email - */ -export const getAnonymousId = async (): Promise<{anonymousId: string | null, error: any | null}> => { - // This will now use the correct shared client - const { data: { session }, error } = await supabase.auth.getSession(); +export const getAnonymousId = async (): Promise<{ + anonymousId: string | null; + error: any | null; +}> => { + const { + data: { session }, + error, + } = await supabase.auth.getSession(); if (error || !session?.user?.id) { return { anonymousId: null, error: error || new Error('No session found') }; } - // Query the users table using auth ID const { data, error: dbError } = await supabase .from('users') .select('anonymous_id') @@ -122,13 +82,12 @@ export const getAnonymousId = async (): Promise<{anonymousId: string | null, err .maybeSingle(); if (dbError) { - console.error("DB Error fetching anonymous ID:", dbError); - return { anonymousId: null, error: dbError }; + return { anonymousId: null, error: dbError }; } + if (!data) { - console.warn("No anonymous ID found for auth_id:", session.user.id); - return { anonymousId: null, error: new Error('Anonymous ID not found for this user.') }; + return { anonymousId: null, error: new Error('Anonymous ID not found for this user.') }; } return { anonymousId: data.anonymous_id, error: null }; -}; \ No newline at end of file +}; diff --git a/src/pages/api/auth/anonymize/route.ts b/src/pages/api/auth/anonymize/route.ts deleted file mode 100644 index ed1002f..0000000 --- a/src/pages/api/auth/anonymize/route.ts +++ /dev/null @@ -1,85 +0,0 @@ -// app/api/auth/anonymize/route.ts -import { NextResponse } from 'next/server'; -import crypto from 'crypto'; -import { v4 as uuidv4 } from 'uuid'; -import { createClient as createServerClient} from '@/utils/supabase/server'; - -/** - * Server-side anonymization handler that keeps email out of client-side code entirely - * This ensures the email never exists in browser memory or localStorage - */ -export async function POST(request: Request) { - try { - // Initialize Supabase server client - const supabase = await createServerClient(); - - // Get the current session server-side - const { data: { session } } = await supabase.auth.getSession(); - - if (!session || !session.user || !session.user.email) { - return NextResponse.json( - { error: 'Unauthorized - No valid session found' }, - { status: 401 } - ); - } - - // Server-side anonymization - user's email never leaves the server - // Generate a strong random salt - const salt = crypto.randomBytes(32).toString('hex'); - - // Create a high-entropy hash from email + salt using PBKDF2 - const hash = crypto.pbkdf2Sync( - session.user.email, - salt, - 100000, // High iteration count - 64, - 'sha512' - ).toString('hex'); - - // Create an anonymous ID - const anonymousId = uuidv4(); - - // Create a verification token - const verificationToken = hash.substring(0, 64); - - // Additional security layer with secondary hashing - const secondarySalt = crypto.randomBytes(32).toString('hex'); - const doubleHashedToken = crypto.pbkdf2Sync( - verificationToken, - secondarySalt, - 50000, - 64, - 'sha512' - ).toString('hex'); - - // Store in the verification table - const { error: dbError } = await supabase - .from('users') - .upsert({ - auth_id: session.user.id, - anonymous_id: anonymousId, - verification_hash: doubleHashedToken, - salt: secondarySalt, - created_at: new Date() - }); - - if (dbError) { - console.error('Database error:', dbError); - return NextResponse.json( - { error: 'Failed to store anonymized identity' }, - { status: 500 } - ); - } - - // Return only the anonymous ID to the client - // Email never returns to the browser - return NextResponse.json({ anonymousId }); - - } catch (error) { - console.error('Anonymization error:', error); - return NextResponse.json( - { error: 'Internal server error during anonymization' }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 2639f89..effc96a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,5 +27,5 @@ "experimentalSpecifierResolution": "node" }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "protrade-stocks", ".next"] }