diff --git a/package-lock.json b/package-lock.json index fa0d704..d496d70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,10 @@ "name": "rtf-website", "version": "0.0.0", "dependencies": { + "@hookform/resolvers": "^5.9.1", "@use-gesture/react": "^10.3.1", "autoprefixer": "^10.4.27", + "axios": "^1.20.0", "face-api.js": "^0.22.2", "framer-motion": "^12.40.0", "gsap": "^3.15.0", @@ -20,11 +22,12 @@ "postprocessing": "^6.39.1", "react": "^19.2.0", "react-dom": "^19.2.0", - "react-hook-form": "^7.71.2", + "react-hook-form": "^7.88.0", "react-icons": "^5.6.0", "react-router-dom": "^7.13.1", "tailwindcss": "^3.4.19", - "three": "^0.184.0" + "three": "^0.184.0", + "zod": "^4.6.5" }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -881,6 +884,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -894,6 +914,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/js": { "version": "9.39.4", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", @@ -931,6 +958,116 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hookform/resolvers": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.9.1.tgz", + "integrity": "sha512-7b7vsbraJxKgjVSA1Nur9tLwj539WGJUBLA7QNvXnFoT2pM5Z7G+6rlukk4B2/QrTZy6huRtH6wKeESPKuIr6w==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "@sinclair/typebox": ">=0.25.24", + "@standard-schema/spec": "^1.0.0", + "@typeschema/main": ">=0.13.7", + "@vinejs/vine": "^2.0.0 || ^3.0.0 || ^4.0.0", + "ajv": "^8.12.0", + "ajv-errors": "^3.0.0", + "ajv-formats": "^2.1.1", + "arktype": "^2.0.0", + "ata-validator": "^1.2.0", + "class-transformer": ">=0.4.0", + "class-validator": ">=0.12.0", + "computed-types": "^1.0.0", + "effect": "^3.10.3", + "fluentvalidation-ts": "^3.0.0", + "fp-ts": "^2.7.0", + "io-ts": "^2.0.0", + "joi": "^17.0.0 || ^18.0.0", + "nope-validator": ">=0.12.0", + "react-hook-form": "^7.55.0", + "superstruct": ">=0.12.0", + "typanion": "^3.3.2", + "valibot": ">=0.31.0 || ^1.0.0-beta.4 || ^1.0.0-rc", + "vest": ">=6.0.0", + "yup": "^1.0.0", + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@sinclair/typebox": { + "optional": true + }, + "@standard-schema/spec": { + "optional": true + }, + "@typeschema/main": { + "optional": true + }, + "@vinejs/vine": { + "optional": true + }, + "ajv": { + "optional": true + }, + "ajv-errors": { + "optional": true + }, + "ajv-formats": { + "optional": true + }, + "arktype": { + "optional": true + }, + "ata-validator": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + }, + "computed-types": { + "optional": true + }, + "effect": { + "optional": true + }, + "fluentvalidation-ts": { + "optional": true + }, + "fp-ts": { + "optional": true + }, + "io-ts": { + "optional": true + }, + "joi": { + "optional": true + }, + "nope-validator": { + "optional": true + }, + "superstruct": { + "optional": true + }, + "typanion": { + "optional": true + }, + "valibot": { + "optional": true + }, + "vest": { + "optional": true + }, + "yup": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1421,6 +1558,12 @@ "win32" ] }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@tensorflow/tfjs-core": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-1.7.0.tgz", @@ -1603,21 +1746,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "dev": true, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "debug": "4" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">= 6.0.0" } }, "node_modules/ansi-styles": { @@ -1680,6 +1818,12 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.4.27", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", @@ -1716,6 +1860,18 @@ "postcss": "^8.1.0" } }, + "node_modules/axios": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1803,6 +1959,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1915,6 +2084,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -1989,7 +2170,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2010,6 +2190,15 @@ "dev": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2022,12 +2211,71 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "license": "MIT" }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.307", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", "license": "ISC" }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", @@ -2212,6 +2460,30 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -2296,7 +2568,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -2430,6 +2702,42 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -2503,6 +2811,43 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2528,6 +2873,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gsap": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz", @@ -2544,10 +2901,37 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -2573,6 +2957,19 @@ "hermes-estree": "0.25.1" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2723,13 +3120,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -2865,6 +3255,15 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2899,6 +3298,27 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -2931,7 +3351,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mz": { @@ -3318,6 +3737,15 @@ "node": ">= 0.8.0" } }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3370,9 +3798,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.71.2", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", - "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", + "version": "7.88.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.88.0.tgz", + "integrity": "sha512-QRaLOWhX93YCnMiRfnOFRSwWXZNt8qhm2JTZwypoDvKpSffTJHmpzMXt8U6PV5UThvL3IiiLDUWe2nHMtz6Mmw==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -3988,10 +4416,9 @@ } }, "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "dev": true, + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index feb6dc7..e2a746c 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,10 @@ "preview": "vite preview" }, "dependencies": { + "@hookform/resolvers": "^5.9.1", "@use-gesture/react": "^10.3.1", "autoprefixer": "^10.4.27", + "axios": "^1.20.0", "face-api.js": "^0.22.2", "framer-motion": "^12.40.0", "gsap": "^3.15.0", @@ -22,11 +24,12 @@ "postprocessing": "^6.39.1", "react": "^19.2.0", "react-dom": "^19.2.0", - "react-hook-form": "^7.71.2", + "react-hook-form": "^7.88.0", "react-icons": "^5.6.0", "react-router-dom": "^7.13.1", "tailwindcss": "^3.4.19", - "three": "^0.184.0" + "three": "^0.184.0", + "zod": "^4.6.5" }, "devDependencies": { "@eslint/js": "^9.39.1", diff --git a/src/components/auth/AuthCard.jsx b/src/components/auth/AuthCard.jsx new file mode 100644 index 0000000..ca9ef93 --- /dev/null +++ b/src/components/auth/AuthCard.jsx @@ -0,0 +1,116 @@ +import { useState } from 'react'; +import { X } from 'lucide-react'; +import HoloCard from '../ui/HoloCard'; +import LoginForm from './LoginForm'; +import RegisterForm from './RegisterForm'; + +/** + * AuthCard — single container that toggles between the "Log in" and + * "Create Account" views, using the site's existing HoloCard / NeoButton + * design tokens throughout. + * + * - Login is RTF ID + password only (see LoginForm.jsx). + * - "Create Account" uses the existing member-application RegisterForm.jsx + * (name, collegeEnrollmentNo, collegeEmail, personalEmail, branch, + * yearOfPassing, phone, domain, password) — same component the /register + * page already drives, so both entry points submit through one + * validated path (see utils/validators.js + services/authService.js). + * + * Usage: + * { ... }} + * onForgotPassword={() => { ... }} + * /> + * + * Pass `onClose` if this is rendered inside a modal/dialog — it renders + * the top-right "X". Omit it (e.g. used as a full page like pages/Login.jsx) + * and the close button is hidden. + * + * @param {object} props + * @param {'login'|'signup'} [props.initialView='login'] + * @param {() => void} [props.onClose] + * @param {(credentials: { rtfId: string, password: string }) => void|Promise} [props.onLogin] + * @param {() => void} [props.onForgotPassword] + * @param {string} [props.className] + */ +export default function AuthCard({ + initialView = 'login', + onClose, + onLogin, + onForgotPassword, + className = '', +}) { + const [view, setView] = useState(initialView); + const [successMessage, setSuccessMessage] = useState(null); + + const switchToLogin = () => { + setSuccessMessage(null); + setView('login'); + }; + + return ( + + {onClose && ( + + )} + + {view === 'login' && ( + setView('signup')} + onSubmit={onLogin} + onForgotPassword={onForgotPassword} + /> + )} + + {view === 'signup' && successMessage && ( +
+

{successMessage}

+

+ You'll be able to log in once your domain admin approves your request. +

+ +
+ )} + + {view === 'signup' && !successMessage && ( +
+
+

Create Account

+

+ For existing RTF members only +

+

+ Already have an account?{' '} + +

+
+ +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/components/auth/LoginForm.jsx b/src/components/auth/LoginForm.jsx index e69de29..2f12ee4 100644 --- a/src/components/auth/LoginForm.jsx +++ b/src/components/auth/LoginForm.jsx @@ -0,0 +1,151 @@ +import { useState } from 'react'; +import { Lock, User, Eye, EyeOff } from 'lucide-react'; +import NeoButton from '../ui/NeoButton'; + +/** + * LoginForm — "Log in" view. + * + * Structure (content only — styling pulled entirely from the existing + * RTF design system: HoloCard-compatible spacing, `input` field classes + * from Login.jsx, NeoButton, cyan accents): + * 1. Header — title + "Activate Account" / "Register Account" toggle + * 2. RTF ID field + * 3. Password field w/ show/hide toggle + * 4. "Forgot password" link (left-aligned) + * 5. Full-width "Sign In" submit button + * + * No third-party (Google) sign-in — login is RTF ID + password only. + * + * @param {object} props + * @param {() => void} props.onSwitchToSignup - called when account registration is clicked + * @param {(credentials: { rtfId: string, password: string }) => void|Promise} [props.onSubmit] + * @param {() => void} [props.onForgotPassword] + */ +export default function LoginForm({ onSwitchToSignup, onSubmit, onForgotPassword }) { + const [rtfId, setRtfId] = useState(''); + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [error, setError] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(''); + + if (!rtfId.trim() || !password) { + setError('Please fill in both fields.'); + return; + } + + if (!onSubmit) { + // No handler wired up yet — mirrors the existing placeholder + // behaviour in pages/Login.jsx until auth is connected. + setError('Member portal coming soon. Contact RTF leader for access.'); + return; + } + + try { + setIsSubmitting(true); + await onSubmit({ rtfId: rtfId.trim(), password }); + } catch (err) { + setError(err?.message || 'Unable to sign in. Please try again.'); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+ {/* Header */} +
+

Log in

+

+ Existing RTF member?{' '} + +

+
+ +
+ {/* RTF ID */} +
+ +
+ + setRtfId(e.target.value)} + placeholder="Enter your RTF ID" + autoComplete="username" + className="w-full pl-10 pr-4 py-3 bg-elevated border border-border rounded-button text-sm text-text-primary placeholder:text-text-muted/50 focus:outline-none focus:border-cyan-500/50 focus:ring-1 focus:ring-cyan-500/20 transition-all" + /> +
+
+ + {/* Password */} +
+ +
+ + setPassword(e.target.value)} + placeholder="Enter password" + autoComplete="current-password" + className="w-full pl-10 pr-11 py-3 bg-elevated border border-border rounded-button text-sm text-text-primary placeholder:text-text-muted/50 focus:outline-none focus:border-cyan-500/50 focus:ring-1 focus:ring-cyan-500/20 transition-all" + /> + +
+
+ + {/* Forgot password */} +
+ {onForgotPassword ? ( + + ) : ( + Forgot password + )} +
+ + {/* Error */} + {error && ( +
+

{error}

+
+ )} + + + {isSubmitting ? 'Signing in…' : 'Sign In'} + +
+
+ ); +} \ No newline at end of file diff --git a/src/components/auth/RegisterForm.jsx b/src/components/auth/RegisterForm.jsx index e4817a2..270edf9 100644 --- a/src/components/auth/RegisterForm.jsx +++ b/src/components/auth/RegisterForm.jsx @@ -13,29 +13,129 @@ // this component doesn't own navigation logic // 5. On error → distinguish field-level errors (show under the // specific input) from general errors (show as a banner/toast) +// +// Styling note: fields use the same token set as LoginForm.jsx / +// pages/Login.jsx (bg-elevated, border-border, text-text-primary, +// focus:border-cyan-500/50 …) rather than a bare `.input` class — +// that class doesn't exist anywhere in globals.css, which is why +// typed text used to be invisible (browser default black-on-dark). // ───────────────────────────────────────────────────────────── import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { useState } from 'react'; -import { registerSchema } from '../../utils/validators'; +import { + User, + Mail, + Lock, + Eye, + EyeOff, + Hash, + Calendar, + Phone, + GraduationCap, + Layers, + ChevronDown, +} from 'lucide-react'; +import { + registerSchema, + BRANCH_OPTIONS, + DOMAIN_OPTIONS, + MIN_YEAR_OF_PASSING, + MAX_YEAR_OF_PASSING, +} from '../../utils/validators'; import { registerUser } from '../../services/authService'; -import NeoButton from '../ui/NeoButton'; // reuse existing UI — don't rebuild a button +import NeoButton from '../ui/NeoButton'; + +const inputClasses = + 'w-full pl-10 pr-4 py-3 bg-elevated border border-border rounded-button text-sm text-text-primary placeholder:text-text-muted/50 focus:outline-none focus:border-cyan-500/50 focus:ring-1 focus:ring-cyan-500/20 transition-all'; + +const selectClasses = + 'w-full appearance-none pl-10 pr-9 py-3 bg-elevated border border-border rounded-button text-sm text-text-primary focus:outline-none focus:border-cyan-500/50 focus:ring-1 focus:ring-cyan-500/20 transition-all'; + +/** Text/email/tel/number input with a leading icon, label above, and an error line below. */ +function Field({ id, label, icon: Icon, error, registration, ...inputProps }) { + return ( +
+ +
+ + +
+ {error &&

{error.message}

} +
+ ); +} -const DOMAIN_OPTIONS = [ - { value: 'software', label: 'Software Domain' }, - { value: 'electrical', label: 'Electrical Domain' }, - { value: 'aeromech', label: 'Aeronautics & Mechanical Domain' }, -]; +/** Password input with a leading lock icon and a show/hide toggle. */ +function PasswordField({ id, label, error, registration, placeholder }) { + const [visible, setVisible] = useState(false); + return ( +
+ +
+ + + +
+ {error &&

{error.message}

} +
+ ); +} + +/** Native select with a leading icon, styled to match the text inputs. */ +function SelectField({ id, label, icon: Icon, error, registration, placeholder, options, getValue, getLabel }) { + return ( +
+ +
+ + + +
+ {error &&

{error.message}

} +
+ ); +} /** * @param {{ onSuccess: (message: string) => void }} props * onSuccess is called with a message once registration succeeds — - * the parent page (Register.jsx) decides what to render next. + * the parent (AuthCard / Register.jsx) decides what to render next. */ export default function RegisterForm({ onSuccess }) { - // formState.errors is populated automatically by zodResolver — - // no manual validation code needed here at all. const { register, handleSubmit, @@ -43,129 +143,148 @@ export default function RegisterForm({ onSuccess }) { setError, } = useForm({ resolver: zodResolver(registerSchema) }); - // Separate from field errors — this is for errors that aren't - // tied to one specific input (e.g. "email already registered", - // or "server unreachable"). + // Separate from field errors — for errors not tied to one input + // (e.g. "email already registered", or "server unreachable"). const [formError, setFormError] = useState(null); - // react-hook-form only calls this if validation already passed. const onSubmit = async (data) => { setFormError(null); - // confirmPassword is only for the frontend check — strip it - // before sending to the backend, which doesn't expect it. + // confirmPassword is frontend-only — stripped before sending. const { confirmPassword, ...payload } = data; try { const result = await registerUser(payload); - onSuccess(result.message); // parent shows "pending approval" screen + onSuccess(result.message); } catch (err) { if (err.fieldErrors) { - // Backend rejected specific fields (rare, since frontend - // already validated — but possible, e.g. a race condition - // on the email-already-exists check). Map them onto the - // same fields react-hook-form is tracking. Object.entries(err.fieldErrors).forEach(([field, messages]) => { setError(field, { message: messages[0] }); }); } else { - // General error (duplicate email, server down, etc.) — - // shown as a banner above the form, not tied to one field. setFormError(err.message); } } }; return ( -
+ {formError && ( -
- {formError} +
+

{formError}

)} -
- - {errors.name &&

{errors.name.message}

} -
+ -
- - {errors.collegeEnrollmentNo && ( -

{errors.collegeEnrollmentNo.message}

- )} -
- -
- - {errors.collegeEmail &&

{errors.collegeEmail.message}

} -
- -
- - {errors.personalEmail && ( -

{errors.personalEmail.message}

- )} -
- -
- - {errors.branch &&

{errors.branch.message}

} -
- -
- + + - {errors.yearOfPassing && ( -

{errors.yearOfPassing.message}

- )}
-
- - {errors.phone &&

{errors.phone.message}

} +
+ +
-
- - {errors.domain &&

{errors.domain.message}

} +
+ b} + getLabel={(b) => b} + /> + d.value} + getLabel={(d) => d.label} + />
-
- - {errors.password &&

{errors.password.message}

} -
+ -
- + + - {errors.confirmPassword && ( -

{errors.confirmPassword.message}

- )}
- {/* isSubmitting comes free from react-hook-form — disable the - button and show loading state while the API call is in flight, - so the user can't double-submit. */} - - {isSubmitting ? 'Submitting...' : 'Register'} + + {isSubmitting ? 'Submitting…' : 'Register'} ); diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx index a0df498..bfd2ef5 100644 --- a/src/pages/Login.jsx +++ b/src/pages/Login.jsx @@ -1,21 +1,14 @@ -import { useState } from 'react'; import { motion } from 'framer-motion'; import { pageTransition } from '../lib/animations'; -import HoloCard from '../components/ui/HoloCard'; -import NeoButton from '../components/ui/NeoButton'; -import { Lock, Mail, Eye, EyeOff } from 'lucide-react'; +import AuthCard from '../components/auth/AuthCard'; import rtfLogo from '../assets/images/rtf-logo-img.jpg'; export default function Login() { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [showPassword, setShowPassword] = useState(false); - const [error, setError] = useState(''); - - const handleSubmit = (e) => { - e.preventDefault(); - // Placeholder — wire to Firebase / Supabase / custom backend - setError('Member portal coming soon. Contact RTF leader for access.'); + // TODO: wire this up to the real login endpoint once it's ready. + // AuthCard already handles its own loading/error state — just make + // this throw (or reject) on failure and LoginForm will surface it. + const handleLogin = async ({ rtfId, password }) => { + throw new Error('Member portal coming soon. Contact RTF leader for access.'); }; return ( @@ -27,87 +20,28 @@ export default function Login() { exit="exit" className="min-h-screen flex items-center justify-center px-6 py-28" > -
+
{/* Logo */} -
+
RTF Logo
-

Member Login

+

Member Access

Access the RTF member portal for team resources, schedules, and internal tools.

- -
- {/* Email */} -
- -
- - setEmail(e.target.value)} - placeholder="you@gcoea.ac.in" - className="w-full pl-10 pr-4 py-3 bg-elevated border border-border rounded-button text-sm text-text-primary placeholder:text-text-muted/50 focus:outline-none focus:border-cyan-500/50 focus:ring-1 focus:ring-cyan-500/20 transition-all" - /> -
-
- - {/* Password */} -
- -
- - setPassword(e.target.value)} - placeholder="••••••••" - className="w-full pl-10 pr-11 py-3 bg-elevated border border-border rounded-button text-sm text-text-primary placeholder:text-text-muted/50 focus:outline-none focus:border-cyan-500/50 focus:ring-1 focus:ring-cyan-500/20 transition-all" - /> - -
-
- - {/* Error */} - {error && ( -
-

{error}

-
- )} - - - SIGN IN - -
+ -
-

- Not a member?{' '} - - Contact us to join RTF - -

-
-
+
+

+ Not a member?{' '} + + Contact us to join RTF + +

+
); diff --git a/src/utils/validators.js b/src/utils/validators.js index 30ba085..b1083d6 100644 --- a/src/utils/validators.js +++ b/src/utils/validators.js @@ -12,18 +12,50 @@ import { z } from 'zod'; +/** Single source of truth for the branch dropdown — used by RegisterForm too. */ +export const BRANCH_OPTIONS = [ + 'Computer Science', + 'Information Technology', + 'Mechanical', + 'Electrical', + 'Civil', + 'Instrumentation', +]; + +/** Single source of truth for the domain dropdown — used by RegisterForm too. */ +export const DOMAIN_OPTIONS = [ + { value: 'software', label: 'Software Domain' }, + { value: 'electrical', label: 'Electrical Domain' }, + { value: 'aeromech', label: 'Aeronautics & Mechanical Domain' }, +]; + +// Matches name@gcoea.ac.in as well as any subdomain, e.g. name@cse.gcoea.ac.in +const GCOEA_EMAIL_REGEX = /^[\w.+-]+@(?:[\w-]+\.)*gcoea\.ac\.in$/i; + +/** Earliest selectable "Year of Passing" — also drives the input's min attribute. */ +export const MIN_YEAR_OF_PASSING = 2026; +/** Latest selectable "Year of Passing". */ +export const MAX_YEAR_OF_PASSING = 2035; + export const registerSchema = z .object({ name: z.string().min(2, 'Name must be at least 2 characters'), - collegeEnrollmentNo: z.string().min(3, 'Enter a valid enrollment number'), - collegeEmail: z.string().email('Enter a valid college email'), + collegeEnrollmentNo: z + .string() + .regex(/^\d{8}$/, 'Enrollment number must be exactly 8 digits'), + collegeEmail: z + .string() + .email('Enter a valid college email') + .regex(GCOEA_EMAIL_REGEX, 'College email must be a gcoea.ac.in address'), personalEmail: z.string().email('Enter a valid personal email'), - branch: z.string().min(2, 'Branch is required'), + branch: z.enum(BRANCH_OPTIONS, { + errorMap: () => ({ message: 'Select a valid branch' }), + }), yearOfPassing: z.coerce .number() .int() - .min(2024, 'Year of passing looks invalid') - .max(2035, 'Year of passing looks invalid'), + .min(MIN_YEAR_OF_PASSING, `Year of passing must be ${MIN_YEAR_OF_PASSING} or later`) + .max(MAX_YEAR_OF_PASSING, 'Year of passing looks invalid'), phone: z.string().regex(/^\d{10}$/, 'Phone number must be 10 digits'), domain: z.enum(['software', 'electrical', 'aeromech'], { errorMap: () => ({ message: 'Choose a domain' }), @@ -41,4 +73,15 @@ export const registerSchema = z .refine((data) => data.password === data.confirmPassword, { message: "Passwords don't match", path: ['confirmPassword'], // shows the error under this specific field - }); \ No newline at end of file + }); + +/** + * Login now happens with RTF ID + password only (no email/username). + * No format is enforced on the ID beyond "non-empty" since RTF IDs + * aren't necessarily the same shape as the 8-digit enrollment number — + * tighten this regex once the backend's RTF ID format is confirmed. + */ +export const loginSchema = z.object({ + rtfId: z.string().trim().min(1, 'RTF ID is required'), + password: z.string().min(1, 'Password is required'), +}); \ No newline at end of file