From 9fa5cc38e8a52f604559eec21192790b4e75ba5c Mon Sep 17 00:00:00 2001 From: Emmanuel Rojas Date: Mon, 7 Sep 2026 20:11:59 -0500 Subject: [PATCH] =?UTF-8?q?feat(all):=20conexi=C3=B3n=20de=20servicio=20de?= =?UTF-8?q?=20login.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 361 ++++++++++++++++++++++++- package.json | 4 + src/app/course/[id]/_content.tsx | 295 ++++++++++++++++++++ src/app/course/[id]/page.tsx | 7 + src/app/instructor/layout.tsx | 34 ++- src/app/layout.tsx | 22 +- src/app/login/_content.tsx | 52 +++- src/app/my-courses/_content.tsx | 285 +++++++++++++++++++ src/app/my-courses/page.tsx | 5 + src/components/auth/LoginForm.tsx | 15 +- src/components/auth/RegisterForm.tsx | 320 +++++++++++++++++++--- src/components/auth/SocialButtons.tsx | 115 ++++---- src/components/layout/Navbar.tsx | 142 ++++++++-- src/components/ui/DatePicker.tsx | 289 ++++++++++++++++++++ src/components/ui/ImageUpload.tsx | 89 ++++++ src/components/ui/PasswordStrength.tsx | 47 ++++ src/components/ui/index.ts | 6 + src/hooks/auth/useAuthGuard.ts | 28 ++ src/hooks/auth/useLogin.ts | 28 ++ src/hooks/auth/useOAuth.ts | 33 +++ src/hooks/auth/useRegister.ts | 26 ++ src/hooks/user/useSaveProfile.ts | 33 +++ src/lib/QueryProvider.tsx | 13 + src/lib/apiClient.ts | 27 ++ src/lib/msalConfig.ts | 15 + src/types/auth.ts | 19 ++ tsconfig.tsbuildinfo | 2 +- 27 files changed, 2173 insertions(+), 139 deletions(-) create mode 100644 src/app/course/[id]/_content.tsx create mode 100644 src/app/course/[id]/page.tsx create mode 100644 src/app/my-courses/_content.tsx create mode 100644 src/app/my-courses/page.tsx create mode 100644 src/components/ui/DatePicker.tsx create mode 100644 src/components/ui/ImageUpload.tsx create mode 100644 src/components/ui/PasswordStrength.tsx create mode 100644 src/hooks/auth/useAuthGuard.ts create mode 100644 src/hooks/auth/useLogin.ts create mode 100644 src/hooks/auth/useOAuth.ts create mode 100644 src/hooks/auth/useRegister.ts create mode 100644 src/hooks/user/useSaveProfile.ts create mode 100644 src/lib/QueryProvider.tsx create mode 100644 src/lib/apiClient.ts create mode 100644 src/lib/msalConfig.ts diff --git a/package-lock.json b/package-lock.json index b1d2a21..153c2cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,13 +8,17 @@ "name": "courses-rynext", "version": "0.0.0", "dependencies": { + "@azure/msal-browser": "^5.21.0", + "@react-oauth/google": "^0.13.5", "@tailwindcss/postcss": "^4.3.3", "@tanstack/react-query": "^5.101.4", + "axios": "^1.20.0", "next": "^16.3.3", "postcss": "^8.5.26", "react": "^19.2.8", "react-dom": "^19.2.8", "react-icons": "^5.7.0", + "sonner": "^2.0.8", "tailwindcss": "^4.3.3", "zustand": "^5.0.15" }, @@ -43,6 +47,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@azure/msal-browser": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.21.0.tgz", + "integrity": "sha512-80OcuXDErmcEDAIH9pBtSqBsed2sPT/IWmbG3xHLoPMl5zc8TINd6SlJAbVSmN5huGa3xGAg5qR7VnpaIEK0Zw==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.14.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.0.tgz", + "integrity": "sha512-A4rb55hI86Q9tBl/+jBj7TMz7iX2RFgQs/nExFzcAtoI/BFRVdaH5SL/MivrYD7qvweMpN8AgVvVMHV8UBYxew==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -1168,6 +1193,16 @@ "node": ">= 10" } }, + "node_modules/@react-oauth/google": { + "version": "0.13.5", + "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.13.5.tgz", + "integrity": "sha512-xQWri2s/3nNekZJ4uuov2aAfQYu83bN3864KcFqw2pK1nNbFurQIjPFDXhWaKH3IjYJ2r/9yyIIpsn5lMqrheQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@swc/helpers": { "version": "0.5.23", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", @@ -1779,6 +1814,18 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "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": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -1796,6 +1843,24 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "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/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": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -1866,6 +1931,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/caniuse-lite": { "version": "1.0.30001809", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", @@ -1892,6 +1970,18 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "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/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1925,7 +2015,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" @@ -1946,6 +2035,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/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1955,6 +2053,20 @@ "node": ">=8" } }, + "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.412", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.412.tgz", @@ -1975,6 +2087,51 @@ "node": ">=10.13.0" } }, + "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/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2274,6 +2431,51 @@ "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2284,6 +2486,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", @@ -2310,12 +2549,63 @@ "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/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "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.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" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -2333,6 +2623,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", @@ -2754,6 +3057,36 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "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/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": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", @@ -2774,7 +3107,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/nanoid": { @@ -3021,6 +3353,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", @@ -3165,6 +3506,22 @@ "node": ">=8" } }, + "node_modules/sonner": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.8.tgz", + "integrity": "sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/package.json b/package.json index ccb518c..7fc5453 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,17 @@ "lint": "next lint" }, "dependencies": { + "@azure/msal-browser": "^5.21.0", + "@react-oauth/google": "^0.13.5", "@tailwindcss/postcss": "^4.3.3", "@tanstack/react-query": "^5.101.4", + "axios": "^1.20.0", "next": "^16.3.3", "postcss": "^8.5.26", "react": "^19.2.8", "react-dom": "^19.2.8", "react-icons": "^5.7.0", + "sonner": "^2.0.8", "tailwindcss": "^4.3.3", "zustand": "^5.0.15" }, diff --git a/src/app/course/[id]/_content.tsx b/src/app/course/[id]/_content.tsx new file mode 100644 index 0000000..0563b08 --- /dev/null +++ b/src/app/course/[id]/_content.tsx @@ -0,0 +1,295 @@ +"use client"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { + FiArrowLeft, FiPlay, FiCheckCircle, FiLock, FiChevronDown, FiChevronUp, + FiClock, FiBook, FiAward, FiStar, FiGlobe, FiMenu, FiX, +} from "react-icons/fi"; +import LayoutBase from "@/components/layout/LayoutBase"; +import { useTheme } from "@/context/ThemeContext"; +import { useRequireAuth } from "@/hooks/auth/useAuthGuard"; + +// ── Mock data ───────────────────────────────────────────────────────────────── +const MOCK: Record = { + "1": { + id: "1", title: "Next.js 14 — De cero a producción", subtitle: "Aprende React, Server Components, App Router y despliega en Vercel.", + instructor: "Carlos Méndez", instructorAvatar: "CM", + instructorBio: "Desarrollador Full Stack con 10 años de experiencia. Especialista en ecosistema React y arquitecturas modernas.", + instructorRating: 4.9, instructorStudents: 32400, + thumbnail: "https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=800&q=80", + duration: "18h 30m", language: "Español", rating: 4.8, reviewsCount: 1240, progress: 68, + modules: [ + { id: "m1", title: "Introducción y configuración", lessons: [ + { id: "l1", title: "Bienvenida al curso", duration: "3:20", completed: true, free: true }, + { id: "l2", title: "Instalación y primer proyecto", duration: "8:45", completed: true, free: true }, + { id: "l3", title: "Estructura del proyecto", duration: "6:10", completed: true, free: false }, + ]}, + { id: "m2", title: "App Router y Pages", lessons: [ + { id: "l4", title: "Diferencias con Pages Router", duration: "10:30", completed: true, free: false }, + { id: "l5", title: "Layouts y templates", duration: "12:15", completed: true, free: false }, + { id: "l6", title: "Loading y Error boundaries", duration: "9:00", completed: false, free: false }, + ]}, + { id: "m3", title: "Server Components", lessons: [ + { id: "l7", title: "¿Qué son los Server Components?", duration: "14:20", completed: false, free: false }, + { id: "l8", title: "Fetching de datos en servidor", duration: "18:45", completed: false, free: false }, + { id: "l9", title: "Client vs Server Components", duration: "11:30", completed: false, free: false }, + ]}, + { id: "m4", title: "Deploy en Vercel", lessons: [ + { id: "l10", title: "Preparando el proyecto", duration: "7:15", completed: false, free: false }, + { id: "l11", title: "Variables de entorno", duration: "5:40", completed: false, free: false }, + { id: "l12", title: "Deploy y dominio personalizado", duration: "13:00", completed: false, free: false }, + ]}, + ], + }, + "2": { + id: "2", title: "TypeScript Avanzado para React", subtitle: "Domina los tipos avanzados, genéricos y patrones de React con TypeScript.", + instructor: "Ana García", instructorAvatar: "AG", + instructorBio: "Ingeniera de software y autora técnica. Contribuidora activa de DefinitelyTyped.", + instructorRating: 4.8, instructorStudents: 18700, + thumbnail: "https://images.unsplash.com/photo-1587620962725-abab7fe55159?w=800&q=80", + duration: "12h 15m", language: "Español", rating: 4.9, reviewsCount: 876, progress: 100, + modules: [ + { id: "m1", title: "Tipos avanzados", lessons: [ + { id: "l1", title: "Utility Types", duration: "12:00", completed: true, free: true }, + { id: "l2", title: "Conditional Types", duration: "15:30", completed: true, free: false }, + ]}, + { id: "m2", title: "Genéricos en React", lessons: [ + { id: "l3", title: "Componentes genéricos", duration: "18:00", completed: true, free: false }, + { id: "l4", title: "Hooks tipados", duration: "14:20", completed: true, free: false }, + ]}, + ], + }, + "3": { + id: "3", title: "Diseño UI/UX con Figma", subtitle: "Crea interfaces profesionales y prototipos interactivos desde cero.", + instructor: "Sofía Rojas", instructorAvatar: "SR", + instructorBio: "Diseñadora de producto con experiencia en startups y empresas Fortune 500.", + instructorRating: 4.7, instructorStudents: 25100, + thumbnail: "https://images.unsplash.com/photo-1561070791-2526d30994b5?w=800&q=80", + duration: "15h 45m", language: "Español", rating: 4.7, reviewsCount: 654, progress: 23, + modules: [ + { id: "m1", title: "Fundamentos de Figma", lessons: [ + { id: "l1", title: "Interfaz de Figma", duration: "8:00", completed: true, free: true }, + { id: "l2", title: "Frames y componentes", duration: "11:30", completed: true, free: false }, + { id: "l3", title: "Auto Layout", duration: "14:00", completed: false, free: false }, + ]}, + ], + }, + "4": { + id: "4", title: "Python para Data Science", subtitle: "Pandas, NumPy, visualización de datos y Machine Learning básico.", + instructor: "Luis Torres", instructorAvatar: "LT", + instructorBio: "Data Scientist con PhD en estadística. Consultor para empresas de tecnología en LATAM.", + instructorRating: 4.9, instructorStudents: 41200, + thumbnail: "https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?w=800&q=80", + duration: "22h 00m", language: "Español", rating: 4.9, reviewsCount: 2103, progress: 100, + modules: [ + { id: "m1", title: "Python esencial", lessons: [ + { id: "l1", title: "Tipos y estructuras", duration: "10:00", completed: true, free: true }, + { id: "l2", title: "Funciones y clases", duration: "13:00", completed: true, free: false }, + ]}, + ], + }, +}; + +// ── Componente principal ────────────────────────────────────────────────────── +export default function CoursePlayer({ id }: { id: string }) { + const { isDark } = useTheme(); + const router = useRouter(); + const { isAuthenticated } = useRequireAuth(); + + const course = MOCK[id]; + const [activeLesson, setActiveLesson] = useState(() => { + if (!course) return null; + for (const mod of course.modules) { + const next = mod.lessons.find(l => !l.completed); + if (next) return next.id; + } + return course.modules[0]?.lessons[0]?.id ?? null; + }); + const [expandedModules, setExpandedModules] = useState>({ m1: true }); + const [sidebarOpen, setSidebarOpen] = useState(false); + + if (!isAuthenticated) return null; + if (!course) return ( + +
+

Curso no encontrado

+ +
+
+ ); + + const allLessons = course.modules.flatMap(m => m.lessons); + const currentLesson = allLessons.find(l => l.id === activeLesson) ?? allLessons[0]; + const totalLessons = allLessons.length; + const completedLessons = allLessons.filter(l => l.completed).length; + + const toggleModule = (id: string) => setExpandedModules(p => ({ ...p, [id]: !p[id] })); + + const border = isDark ? "border-slate-800" : "border-slate-200"; + const bg = isDark ? "bg-slate-900" : "bg-white"; + const muted = isDark ? "text-slate-400" : "text-slate-500"; + + // ── Sidebar del temario ──────────────────────────────────────────────────── + const Sidebar = () => ( + + ); + + return ( +
+ + {/* Top bar */} +
+ +
+

{course.title}

+
+ + {course.progress === 100 ? <> Completado : <>{course.progress}% completado} + + +
+
+ + {/* Body */} +
+ + {/* Main content */} +
+ {/* Video area */} +
+ {course.title} +
+ +

{currentLesson?.title}

+
+
+ + {/* Info */} +
+ {/* Lesson title */} +
+

Lección actual

+

{currentLesson?.title}

+

{currentLesson?.duration}

+
+ + {/* Stats row */} +
+ {totalLessons} lecciones + {course.duration} + {course.language} + {course.rating} ({course.reviewsCount.toLocaleString()} reseñas) +
+ + {/* Instructor */} +
+
+ {course.instructorAvatar} +
+
+

{course.instructor}

+

{course.instructorBio}

+
+ {course.instructorRating} + {course.instructorStudents.toLocaleString()} estudiantes +
+
+
+ + {/* Course description */} +
+

Sobre este curso

+

{course.subtitle}

+
+
+
+ + {/* Sidebar desktop */} +
+
+ +
+
+ + {/* Sidebar mobile overlay */} + {sidebarOpen && ( +
+
setSidebarOpen(false)} /> +
+ +
+
+ )} +
+
+ ); +} diff --git a/src/app/course/[id]/page.tsx b/src/app/course/[id]/page.tsx new file mode 100644 index 0000000..112abbc --- /dev/null +++ b/src/app/course/[id]/page.tsx @@ -0,0 +1,7 @@ +import { use } from "react"; +import CoursePlayer from "./_content"; + +export default function Page({ params }: { params: Promise<{ id: string }> }) { + const { id } = use(params); + return ; +} diff --git a/src/app/instructor/layout.tsx b/src/app/instructor/layout.tsx index 40871e0..cc8d102 100644 --- a/src/app/instructor/layout.tsx +++ b/src/app/instructor/layout.tsx @@ -15,6 +15,8 @@ import { FiChevronDown, } from "react-icons/fi"; import { ThemeToggle } from "@/components/ui"; +import { useAuthStore } from "@/stores/authStore"; +import { useRequireAuth } from "@/hooks/auth/useAuthGuard"; const navigation = [ { name: "Dashboard", href: "/instructor", icon: FiHome }, @@ -35,6 +37,8 @@ export default function InstructorLayout({ children: React.ReactNode; }) { const pathname = usePathname(); + const { isAuthenticated } = useRequireAuth("/login"); + const { user, logout } = useAuthStore(); const [sidebarOpen, setSidebarOpen] = useState(false); const [profileMenuOpen, setProfileMenuOpen] = useState(false); const [dropdownPos, setDropdownPos] = useState({ top: 0, right: 0 }); @@ -75,6 +79,18 @@ export default function InstructorLayout({ } }, [profileMenuOpen]); + if (!isAuthenticated) return null; + + const initials = user?.name + ? user.name.split(" ").map((w: string) => w[0]).slice(0, 2).join("").toUpperCase() + : user?.email?.[0]?.toUpperCase() ?? "U"; + + const handleLogout = () => { + logout(); + localStorage.removeItem("refreshToken"); + window.location.href = "/"; + }; + const SidebarContent = () => ( <>
@@ -200,16 +216,12 @@ export default function InstructorLayout({ >

- Instructor Name + {user?.name || "Mi cuenta"}

-

instructor@rynext.com

+

{user?.email}

-
- Avatar +
+ {initials}
{/* User info header */}
-

Instructor Name

-

instructor@rynext.com

+

{user?.name || "Mi cuenta"}

+

{user?.email}

{/* Menu items */} @@ -247,7 +259,7 @@ export default function InstructorLayout({ +
+ ); + } + + return ( +
router.push(`/course/${course.id}`)} + > +
+ {course.title} + {completed && ( +
+
+ Completado +
+
+ )} + {!completed && ( + + )} +
+
+ {course.category} +

+ {course.title} +

+

{course.instructor}

+
+ {course.duration} + {course.totalLessons} lecciones +
+ {progressBar} +
+
+ ); +} + +// ── Stat card ───────────────────────────────────────────────────────────────── +function StatCard({ icon, label, value, color }: { icon: React.ReactNode; label: string; value: string; color: string }) { + const { isDark } = useTheme(); + return ( +
+
{icon}
+
+

{value}

+

{label}

+
+
+ ); +} + +// ── Página principal ────────────────────────────────────────────────────────── +export default function MyCourses() { + const { isDark } = useTheme(); + const { isAuthenticated } = useRequireAuth(); + const [tab, setTab] = useState<"all" | "in-progress" | "completed">("all"); + const [search, setSearch] = useState(""); + const [view, setView] = useState<"grid" | "list">("grid"); + + if (!isAuthenticated) return null; + + const filtered = MOCK_COURSES.filter((c) => { + const matchTab = + tab === "all" || + (tab === "in-progress" && c.progress < 100) || + (tab === "completed" && c.progress === 100); + const matchSearch = c.title.toLowerCase().includes(search.toLowerCase()); + return matchTab && matchSearch; + }); + + const totalHours = Math.round(MOCK_COURSES.reduce((a, c) => a + parseFloat(c.duration), 0)); + const completedCount = MOCK_COURSES.filter(c => c.progress === 100).length; + const avgProgress = Math.round(MOCK_COURSES.reduce((a, c) => a + c.progress, 0) / MOCK_COURSES.length); + + const muted = isDark ? "text-slate-400" : "text-slate-500"; + const tabBase = "px-4 py-2 rounded-xl text-sm font-medium transition-colors cursor-pointer"; + const tabActive = isDark ? "bg-violet-600 text-white" : "bg-violet-600 text-white"; + const tabInactive = isDark ? "text-slate-400 hover:text-white hover:bg-slate-800" : "text-slate-500 hover:text-slate-900 hover:bg-slate-100"; + + return ( + + {/* Header */} +
+

+ Mis cursos +

+

Continúa donde lo dejaste.

+
+ + {/* Stats */} +
+ } label="Cursos inscritos" value={String(MOCK_COURSES.length)} color="bg-violet-500/10 text-violet-500" /> + } label="Completados" value={String(completedCount)} color="bg-emerald-500/10 text-emerald-500" /> + } label="Horas totales" value={`${totalHours}h`} color="bg-blue-500/10 text-blue-500" /> + } label="Progreso promedio" value={`${avgProgress}%`} color="bg-orange-500/10 text-orange-500" /> +
+ + {/* Toolbar */} +
+ {/* Tabs */} +
+ {(["all", "in-progress", "completed"] as const).map((t) => ( + + ))} +
+ + {/* Search + view toggle */} +
+
+ + setSearch(e.target.value)} + placeholder="Buscar..." + className="bg-transparent outline-none w-36 placeholder:text-slate-400" + /> +
+
+ + +
+
+
+ + {/* Courses */} + {filtered.length === 0 ? ( +
+ +

+ {search ? "No se encontraron cursos" : "Aún no tienes cursos"} +

+

+ {search ? "Prueba con otro término" : "Explora el catálogo y empieza a aprender"} +

+
+ ) : view === "grid" ? ( +
+ {filtered.map(c => )} +
+ ) : ( +
+ {filtered.map(c => )} +
+ )} +
+ ); +} diff --git a/src/app/my-courses/page.tsx b/src/app/my-courses/page.tsx new file mode 100644 index 0000000..9135d75 --- /dev/null +++ b/src/app/my-courses/page.tsx @@ -0,0 +1,5 @@ +import MyCourses from "./_content"; + +export default function Page() { + return ; +} diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx index 2ae7007..987583f 100644 --- a/src/components/auth/LoginForm.tsx +++ b/src/components/auth/LoginForm.tsx @@ -1,26 +1,28 @@ "use client"; import { useState } from "react"; -import { useRouter } from "next/navigation"; import { FiMail, FiLock, FiArrowRight, FiCheck } from "react-icons/fi"; +import { toast } from "sonner"; import { Button, Input, Checkbox } from "@/components/ui"; import { useTheme } from "@/context/ThemeContext"; import { AuthDivider } from "./AuthDivider"; import { SocialButtons } from "./SocialButtons"; +import { useLogin } from "@/hooks/auth/useLogin"; export function LoginForm({ onSwitchToRegister }: { onSwitchToRegister: () => void }) { - const router = useRouter(); const { isDark } = useTheme(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [remember, setRemember] = useState(true); - const [isLoading, setIsLoading] = useState(false); const isValidEmail = email.includes("@") && email.includes("."); + const { mutate: login, isPending } = useLogin(); + const handleLogin = (e: React.FormEvent) => { e.preventDefault(); if (!email || !password) return; - setIsLoading(true); - setTimeout(() => { setIsLoading(false); router.push("/"); }, 800); + login({ email, password }, { + onError: () => toast.error("Credenciales inválidas. Intenta de nuevo."), + }); }; return ( @@ -33,6 +35,7 @@ export function LoginForm({ onSwitchToRegister }: { onSwitchToRegister: () => vo Ingresa tu correo y contraseña para continuar.

+
vo variant="violet" size="lg" fullWidth - isLoading={isLoading} + isLoading={isPending} loadingText="Verificando..." rightIcon={} > diff --git a/src/components/auth/RegisterForm.tsx b/src/components/auth/RegisterForm.tsx index 0debe5f..c0f3a12 100644 --- a/src/components/auth/RegisterForm.tsx +++ b/src/components/auth/RegisterForm.tsx @@ -1,47 +1,58 @@ "use client"; import { useState } from "react"; -import { useRouter } from "next/navigation"; -import { FiMail, FiLock, FiArrowRight, FiUser } from "react-icons/fi"; -import { Button, Input } from "@/components/ui"; +import { FiMail, FiLock, FiArrowRight, FiUser, FiMapPin, FiGlobe, FiCamera } from "react-icons/fi"; +import { toast } from "sonner"; +import { Button, Input, Textarea, PasswordStrength, DatePicker } from "@/components/ui"; import { useTheme } from "@/context/ThemeContext"; import { AuthDivider } from "./AuthDivider"; import { SocialButtons } from "./SocialButtons"; +import { useRegister } from "@/hooks/auth/useRegister"; +import { useSaveProfile } from "@/hooks/user/useSaveProfile"; +import { useRouter } from "next/navigation"; -export function RegisterForm({ onSwitchToLogin }: { onSwitchToLogin: () => void }) { - const router = useRouter(); +// ─── Paso 1: Credenciales ──────────────────────────────────────────────────── +function StepCredentials({ + onNext, + onSwitchToLogin, +}: { + onNext: () => void; + onSwitchToLogin: () => void; +}) { const { isDark } = useTheme(); - const [name, setName] = useState(""); - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [isLoading, setIsLoading] = useState(false); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirm, setConfirm] = useState(""); - const handleRegister = (e: React.FormEvent) => { - e.preventDefault(); - if (!name || !email || !password) return; - setIsLoading(true); - setTimeout(() => { setIsLoading(false); router.push("/"); }, 800); - }; + const passwordsMatch = confirm === "" || password === confirm; + + const { mutate: register, isPending } = useRegister(onNext); + + const muted = isDark ? "text-slate-400" : "text-slate-500"; return ( <> -
+
+
Paso 1 de 2

Crear cuenta

-

- Regístrate gratis y empieza a aprender hoy. +

+ Ingresa tu correo y una contraseña segura.

- - setName(e.target.value)} - placeholder="Tu nombre" - leftIcon={} - /> + + { + e.preventDefault(); + if (!passwordsMatch) return; + register({ email, password }, { + onError: (err) => { + const msg = (err as { response?: { data?: string } }).response?.data ?? ""; + if (msg.includes("registrado")) toast.error("Este correo ya tiene una cuenta."); + else if (msg.toLowerCase().includes("password")) toast.error("La contraseña no cumple los requisitos."); + else toast.error("Ocurrió un error. Intenta de nuevo."); + }, + }); + }} className="space-y-5"> void placeholder="nombre@correo.com" leftIcon={} /> - setPassword(e.target.value)} - placeholder="Mínimo 8 caracteres" - leftIcon={} - /> +
+ setPassword(e.target.value)} + placeholder="Mínimo 8 caracteres" + leftIcon={} + /> + +
+
+ setConfirm(e.target.value)} + placeholder="Repite tu contraseña" + leftIcon={} + /> + {!passwordsMatch && ( +

Las contraseñas no coinciden

+ )} +
+ -

+ +

¿Ya tienes cuenta?{" "}