From 3f9d7798c5e972536a96ad780dbbf5d33b85b6b7 Mon Sep 17 00:00:00 2001 From: roshanraj9136 Date: Sat, 13 Jun 2026 00:49:47 +0530 Subject: [PATCH 1/4] build: exclude nested protrade-stocks from root tsconfig The root tsconfig include glob (**/*.ts, **/*.tsx) was pulling the nested protrade-stocks Vite app into root type-checks, surfacing errors that belong to that sub-project's own build. Add it to the exclude list along with .next. --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"] } From 0e255a5a521934b4b99066ec33c952ee292ab91c Mon Sep 17 00:00:00 2001 From: roshanraj9136 Date: Sat, 13 Jun 2026 00:49:58 +0530 Subject: [PATCH 2/4] deps: bump next to 13.5.11 and patch transitive vulnerabilities Resolves the following advisories surfaced by npm audit: - next: middleware/proxy bypass on Pages Router with i18n, WebSocket-upgrade SSRF - uuid <11.1.1: missing buffer bounds check in v3/v5/v6 - ws 8.0.0-8.20.0: uninitialized memory disclosure - yaml: stack overflow on deeply nested collections - picomatch: ReDoS via extglob quantifiers, method injection in POSIX character classes next is bumped patch-level (13.5.1 -> 13.5.11) within the same minor; the rest move to clean ranges via npm audit fix. Build and type-check are unchanged. --- package-lock.json | 246 ++++++++++++++++++++++++---------------------- package.json | 2 +- 2 files changed, 128 insertions(+), 120 deletions(-) diff --git a/package-lock.json b/package-lock.json index 055222c..7a71910 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,7 +59,7 @@ "eslint-config-next": "13.5.1", "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", @@ -1364,9 +1364,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 +1379,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 +1395,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 +1411,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 +1427,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 +1443,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 +1459,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 +1480,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 +1496,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 +1512,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" ], @@ -3621,9 +3621,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 +3946,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 +4317,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", @@ -4940,9 +4940,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 +5856,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": { @@ -6818,9 +6818,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 +6976,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 +7053,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" @@ -7090,9 +7100,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 +7139,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 +7159,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 +7204,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 +7215,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 +7231,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 +7559,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 +7598,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 +7617,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -7693,15 +7697,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 +8830,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 +8860,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 +9024,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 +9435,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 +9728,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 +9749,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..fbc7d9e 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "eslint-config-next": "13.5.1", "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", From 3f9566fb6fde9cbc42b4e2b6aa387491131a21b8 Mon Sep 17 00:00:00 2001 From: roshanraj9136 Date: Sat, 13 Jun 2026 00:50:38 +0530 Subject: [PATCH 3/4] security(auth): perform PBKDF2 anonymization server-side Move the email-to-anonymous-id derivation off the browser and onto a real route handler. Three problems with the previous setup: 1. The handler at src/pages/api/auth/anonymize/route.ts was dead code. It used App Router syntax (export async function POST, NextResponse) under the Pages Router tree, where Next.js expects export default function handler(req, res). It was never invoked. 2. The actual anonymization ran in handleAuthCallback in the browser, which forced two compromises documented in the source: PBKDF2 iterations were lowered from 100000 to 10000 and from 50000 to 5000 'to prevent browser freeze'. That's a 10x weaker brute-force resistance, on the wrong side of the trust boundary, running synchronously and blocking the main thread. 3. The Node 'crypto' module was being shipped to the client bundle via a polyfill. Unnecessary weight and a footgun if anyone reuses the helpers elsewhere. This change: - Adds src/app/api/auth/anonymize/route.ts as the real handler. Reads the session via @/utils/supabase/server, runs PBKDF2 with the original iteration counts (100000 + 50000), uses the async promisified pbkdf2 so the event loop is not blocked, and is idempotent: if a users row already exists for this auth_id it returns the existing anonymous_id instead of inserting a duplicate (which the previous upsert-without-onConflict would have done). - Deletes the broken pages-router file. - Rewrites handleAuthCallback to POST to the new route. The public signature ({ user, anonymousId, error }) is preserved so callers in src/app/auth/callback/page.tsx, src/components/auth/ EmailVerification.tsx, and src/contexts/AuthContext.tsx work unchanged. - Drops the now-unused generateAnonymousIdentity, verifyAnonymousIdentity, AnonymousUser, and AnonymizationResult exports from anonymization.ts. The crypto and uuid imports go with them, so client-bundled code no longer references node:crypto. sanitizeContent, enhancedSanitizeContent, and createFuzzyTimestamp remain since they are still used by review-rendering components. Verified: tsc --noEmit clean, next build succeeds, /api/auth/ anonymize appears as a server route in the build output. --- src/app/api/auth/anonymize/route.ts | 66 ++++++++++ src/lib/anonymization.ts | 172 ++++++-------------------- src/lib/supabase-auth.ts | 117 ++++++------------ src/pages/api/auth/anonymize/route.ts | 85 ------------- 4 files changed, 143 insertions(+), 297 deletions(-) create mode 100644 src/app/api/auth/anonymize/route.ts delete mode 100644 src/pages/api/auth/anonymize/route.ts 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/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 From ea8b93e163e728ff340e3fd0ea516024577f5c96 Mon Sep 17 00:00:00 2001 From: roshanraj9136 Date: Sat, 13 Jun 2026 02:59:14 +0530 Subject: [PATCH 4/4] feat(ui): complete UI overhaul with mobile-first design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 โ€” Mobile UX: - Hamburger nav with animated slide-down menu (framer-motion) - Touch-friendly star ratings (32px targets on mobile) - Shimmer loading skeletons replacing text placeholders - Auto scroll-to-top on route navigation Phase 2 โ€” Premium feel: - Live search bar on homepage (queries courses + professors) - Page transitions with fade/slide animation - SearchBar with instant dropdown results Phase 3 โ€” Legacy features: - /compare page: side-by-side course comparison with animated bars - Professor radar chart (recharts) on professor detail pages - Trending 'Most Reviewed' section on homepage - PWA: manifest.json + service worker for offline + installable - Confetti celebration on first rating submission Stack: Node 20, React 18.2, Next 13.5.11 New deps: framer-motion@11.18.0, canvas-confetti@1.9.3 Verified: tsc clean, next build passes (15 routes), all routes 200 --- package-lock.json | 62 +++++++ package.json | 3 + public/manifest.json | 22 +++ public/sw.js | 31 ++++ src/app/compare/page.tsx | 154 ++++++++++++++++++ src/app/layout.tsx | 11 +- src/app/page.tsx | 37 +---- src/components/common/SearchBar.tsx | 148 +++++++++++++++++ src/components/common/TrendingSection.tsx | 62 +++++++ .../courses/course_page/CoursePageReviews.tsx | 9 +- .../courses/course_page/RateThisCourse.tsx | 8 +- src/components/layout/Header.tsx | 3 + src/components/layout/MobileNav.tsx | 56 +++++++ src/components/layout/PageTransition.tsx | 15 ++ src/components/layout/ScrollToTop.tsx | 14 ++ .../layout/ServiceWorkerRegister.tsx | 12 ++ src/components/professors/ProfessorRadar.tsx | 40 +++++ .../professor_page/ProfessorPageReviews.tsx | 9 +- .../professor_page/ProfessorPageStats.tsx | 9 + src/components/ui/skeleton.tsx | 55 ++++++- 20 files changed, 714 insertions(+), 46 deletions(-) create mode 100644 public/manifest.json create mode 100644 public/sw.js create mode 100644 src/app/compare/page.tsx create mode 100644 src/components/common/SearchBar.tsx create mode 100644 src/components/common/TrendingSection.tsx create mode 100644 src/components/layout/MobileNav.tsx create mode 100644 src/components/layout/PageTransition.tsx create mode 100644 src/components/layout/ScrollToTop.tsx create mode 100644 src/components/layout/ServiceWorkerRegister.tsx create mode 100644 src/components/professors/ProfessorRadar.tsx diff --git a/package-lock.json b/package-lock.json index 7a71910..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,6 +58,7 @@ "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.11", @@ -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", @@ -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", @@ -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", @@ -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", @@ -7082,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", diff --git a/package.json b/package.json index fbc7d9e..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,6 +60,7 @@ "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.11", @@ -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/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 };