From c3fed6e383d94a140fcc11f04da41fe5d3248ae2 Mon Sep 17 00:00:00 2001 From: shivang-16 Date: Sat, 17 May 2025 03:26:23 +0530 Subject: [PATCH 1/2] referrals page added --- package-lock.json | 261 ++++++++++- package.json | 3 + src/actions/cookie_actions.ts | 7 + src/actions/index.ts | 50 +++ src/apiClient/apiClient.ts | 31 ++ .../(auth)/_components/GoogleLoginButton.tsx | 70 +++ src/app/(auth)/layout.tsx | 19 + src/app/api/auth/login/route.ts | 22 + src/app/api/auth/logout/route.ts | 18 + src/app/api/auth/verify/route.ts | 22 + src/app/api/google/auth/route.ts | 22 + src/app/layout.tsx | 11 +- src/app/referrals/page.tsx | 409 ++++++++++++++++++ 13 files changed, 942 insertions(+), 3 deletions(-) create mode 100644 src/actions/cookie_actions.ts create mode 100644 src/actions/index.ts create mode 100644 src/apiClient/apiClient.ts create mode 100644 src/app/(auth)/_components/GoogleLoginButton.tsx create mode 100644 src/app/(auth)/layout.tsx create mode 100644 src/app/api/auth/login/route.ts create mode 100644 src/app/api/auth/logout/route.ts create mode 100644 src/app/api/auth/verify/route.ts create mode 100644 src/app/api/google/auth/route.ts create mode 100644 src/app/referrals/page.tsx diff --git a/package-lock.json b/package-lock.json index 4714149..45f21ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,8 @@ "dependencies": { "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-icons": "^1.3.0", + "@react-oauth/google": "^0.12.2", + "axios": "^1.9.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "core-js": "^3.39.0", @@ -21,6 +23,7 @@ "react": "^18", "react-dom": "^18", "react-pdf": "^9.1.1", + "sonner": "^2.0.3", "tailwind-merge": "^2.4.0", "tailwindcss-animate": "^1.0.7" }, @@ -641,6 +644,16 @@ } } }, + "node_modules/@react-oauth/google": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.2.tgz", + "integrity": "sha512-d1GVm2uD4E44EJft2RbKtp8Z1fp/gK8Lb6KHgs3pHlM0PxCXGLaq8LLYQYENnN4xPWO1gkL4apBtlPKzpLvZwg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -1044,6 +1057,23 @@ "node": ">=10" } }, + "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.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1143,6 +1173,19 @@ "node": ">=10.16.0" } }, + "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/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -1305,6 +1348,18 @@ "color-support": "bin.js" } }, + "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", @@ -1403,6 +1458,15 @@ "node": ">=8" } }, + "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/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -1446,6 +1510,20 @@ "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/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -1488,6 +1566,24 @@ "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-module-lexer": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", @@ -1495,6 +1591,33 @@ "license": "MIT", "peer": true }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "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", @@ -1643,6 +1766,26 @@ "node": ">=8" } }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", @@ -1659,6 +1802,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/framer-motion": { "version": "11.3.0", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.3.0.tgz", @@ -1813,6 +1971,30 @@ "node": ">=8" } }, + "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-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -1821,6 +2003,19 @@ "node": ">=6" } }, + "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": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -1860,6 +2055,18 @@ "license": "BSD-2-Clause", "peer": true }, + "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", @@ -1876,6 +2083,33 @@ "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/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", @@ -2190,6 +2424,15 @@ "url": "https://github.com/wojtekmaj/make-event-props?sponsor=1" } }, + "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/merge-refs": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.3.0.tgz", @@ -2241,7 +2484,6 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -2251,7 +2493,6 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", - "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -2822,6 +3063,12 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3274,6 +3521,16 @@ "simple-concat": "^1.0.0" } }, + "node_modules/sonner": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.3.tgz", + "integrity": "sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", diff --git a/package.json b/package.json index 60ce73a..1d3e82e 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "dependencies": { "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-icons": "^1.3.0", + "@react-oauth/google": "^0.12.2", + "axios": "^1.9.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "core-js": "^3.39.0", @@ -22,6 +24,7 @@ "react": "^18", "react-dom": "^18", "react-pdf": "^9.1.1", + "sonner": "^2.0.3", "tailwind-merge": "^2.4.0", "tailwindcss-animate": "^1.0.7" }, diff --git a/src/actions/cookie_actions.ts b/src/actions/cookie_actions.ts new file mode 100644 index 0000000..59aa782 --- /dev/null +++ b/src/actions/cookie_actions.ts @@ -0,0 +1,7 @@ +"use server"; + +import { cookies } from "next/headers"; + +export const getCookie = async (name: string) => { + return cookies().get(name)?.value ?? ""; +}; diff --git a/src/actions/index.ts b/src/actions/index.ts new file mode 100644 index 0000000..10c6505 --- /dev/null +++ b/src/actions/index.ts @@ -0,0 +1,50 @@ +import { getCookie } from "./cookie_actions"; + +export const createReferralCode = async (data?: any) => { + const token = await getCookie("token"); + try { + const res = await fetch( + `${process.env.NEXT_PUBLIC_STUDENT_API_URL}/api/user/referral/create`, + { + method: "POST", + body: JSON.stringify(data), + headers: { + "Content-Type": "application/json", + // "x-api-key": process.env.NEXT_PUBLIC_STUDENT_API_KEY, + Cookie: `token=${token}`, + }, + credentials: "include", + } + ); + + const responseData = await res.json(); + + return responseData; + } catch (error: unknown) { + return error + } + }; + +export const fetchReferralCode = async () => { + const token = await getCookie("token"); + try { + const res = await fetch( + `${process.env.NEXT_PUBLIC_STUDENT_API_URL}/api/user/referral/get`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + // "x-api-key": process.env.NEXT_STUDENT_API_KEY, + Cookie: `token=${token}`, + }, + credentials: "include", + } + ); + + const responseData = await res.json(); + + return responseData; + } catch (error: unknown) { + return error + } + }; \ No newline at end of file diff --git a/src/apiClient/apiClient.ts b/src/apiClient/apiClient.ts new file mode 100644 index 0000000..8a89d71 --- /dev/null +++ b/src/apiClient/apiClient.ts @@ -0,0 +1,31 @@ +import axios from 'axios'; + +const apiClient = axios.create({ + baseURL: process.env.NEXT_PUBLIC_STUDENT_API_URL, + withCredentials: true, + headers: { + "Content-Type": "application/json", + "x-api-key": process.env.NEXT_PUBLIC_STUDENT_API_KEY, + }, +}); + +apiClient.interceptors.request.use( + (config) => { + // config.headers.Authorization = `Bearer ${yourToken}`; + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +apiClient.interceptors.response.use( + (response) => { + return response; + }, + (error) => { + return Promise.reject(error); + } +); + +export default apiClient; diff --git a/src/app/(auth)/_components/GoogleLoginButton.tsx b/src/app/(auth)/_components/GoogleLoginButton.tsx new file mode 100644 index 0000000..2d7b159 --- /dev/null +++ b/src/app/(auth)/_components/GoogleLoginButton.tsx @@ -0,0 +1,70 @@ +import { useGoogleLogin } from "@react-oauth/google"; +import axios from "axios"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { toast } from "sonner"; + +interface GoogleLoginButtonProps { + onLoginSuccess?: () => void; +} + +const GoogleLoginButton = ({ onLoginSuccess }: GoogleLoginButtonProps) => { + const router = useRouter(); + + const login = useGoogleLogin({ + onSuccess: async (credentialResponse) => { + try { + const res = await axios.post("/api/google/auth", { + access_token: credentialResponse.access_token + },{ + withCredentials: true, + headers: { + 'Content-Type': 'application/json', + }, + }); + + // Instead of using Redux, we can store user data in localStorage or cookies if needed + // Or simply use the response data directly + + toast.success("Login success", { + description: res.data.message, + }); + + // Call the onLoginSuccess callback if provided + if (onLoginSuccess) { + onLoginSuccess(); + } + + } catch (error: any) { + console.error("Axios error:", error); + toast.error("Google login failed!", { + description: error.response?.data?.message || error.message, + }); + } + }, + onError: (error: any) => { + console.error("Google login error:", error); + toast.error("Google login failed!", { + description: error.message, + }); + }, + }); + + + return ( + + ); +}; + +export default GoogleLoginButton; diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx new file mode 100644 index 0000000..de84c7e --- /dev/null +++ b/src/app/(auth)/layout.tsx @@ -0,0 +1,19 @@ +import { Metadata } from "next"; +import React from "react"; +import { Mada as FontSans } from "next/font/google"; +import "../globals.css"; +import { cn } from "@/lib/utils"; + +const fontSans = FontSans({ subsets: ["latin"], variable: "--font-sans" }); + +export const metadata: Metadata = { + title: "Leadlly | Auth", +}; + +export default function AuthLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return
{children}
; +} diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts new file mode 100644 index 0000000..ee8d7c3 --- /dev/null +++ b/src/app/api/auth/login/route.ts @@ -0,0 +1,22 @@ +// app/api/auth/login/route.ts +import { NextRequest, NextResponse } from 'next/server'; + +import apiClient from '@/apiClient/apiClient'; + +export async function POST(req: NextRequest) { + try { + + const body = await req.json(); + + const response = await apiClient.post('/api/auth/login', body); + + const { token, ...userData } = response.data; + + const res = NextResponse.json(userData); + res.cookies.set('token', token, { httpOnly: true, path: '/', sameSite: 'strict' }); + + return res; + } catch (error: any) { + return NextResponse.json({ message: error.message }, { status: error.response?.status || 500 }); + } +} diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts new file mode 100644 index 0000000..26b8535 --- /dev/null +++ b/src/app/api/auth/logout/route.ts @@ -0,0 +1,18 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(req: NextRequest) { + try { + const res = NextResponse.json({ message: "Logged Out" }); + + res.cookies.set("token", "", { + httpOnly: true, + path: "/", + sameSite: "strict", + expires: new Date(0), + }); + + return res; + } catch (error: any) { + return NextResponse.json({ message: error.message }, { status: 500 }); + } +} diff --git a/src/app/api/auth/verify/route.ts b/src/app/api/auth/verify/route.ts new file mode 100644 index 0000000..ba4b2c3 --- /dev/null +++ b/src/app/api/auth/verify/route.ts @@ -0,0 +1,22 @@ +// app/api/auth/login/route.ts +import { NextRequest, NextResponse } from 'next/server'; + +import apiClient from '@/apiClient/apiClient'; + +export async function POST(req: NextRequest) { + try { + + const body = await req.json(); + console.log(body,"this is body") + const response = await apiClient.post('/api/auth/verify', body); + + const { token, ...userData } = response.data; + + const res = NextResponse.json(userData); + res.cookies.set('token', token, { httpOnly: true, path: '/', sameSite: 'strict' }); + + return res; + } catch (error: any) { + return NextResponse.json({ message: error.message }, { status: error.response?.status || 500 }); + } +} diff --git a/src/app/api/google/auth/route.ts b/src/app/api/google/auth/route.ts new file mode 100644 index 0000000..b4c36af --- /dev/null +++ b/src/app/api/google/auth/route.ts @@ -0,0 +1,22 @@ +// app/api/auth/login/route.ts +import { NextRequest, NextResponse } from 'next/server'; + +import apiClient from '@/apiClient/apiClient'; + +export async function POST(req: NextRequest) { + try { + + const body = await req.json(); + + const response = await apiClient.post('/api/google/auth', body); + + const { token, ...userData } = response.data; + + const res = NextResponse.json(userData); + res.cookies.set('token', token, { httpOnly: true, path: '/', sameSite: 'strict' }); + + return res; + } catch (error: any) { + return NextResponse.json({ message: error.message }, { status: error.response?.status || 500 }); + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 4a2cb1a..115d7b2 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,8 @@ import type { Metadata } from "next"; import { Montserrat } from "next/font/google"; import "./globals.css"; +import { GoogleOAuthProvider } from "@react-oauth/google"; + // Import Montserrat font const montserrat = Montserrat({ @@ -22,7 +24,14 @@ export default function RootLayout({ return ( {/* Apply the Montserrat font */} - {children} + + + {children} + + + ); } diff --git a/src/app/referrals/page.tsx b/src/app/referrals/page.tsx new file mode 100644 index 0000000..5f08ca5 --- /dev/null +++ b/src/app/referrals/page.tsx @@ -0,0 +1,409 @@ +"use client"; +import { useState, useEffect } from "react"; +import Image from "next/image"; +import Footer from "@/components/shared/Footer"; +import Reveal from "@/components/shared/Reveal"; +import { useRouter } from "next/navigation"; +import GoogleLoginButton from "@/app/(auth)/_components/GoogleLoginButton"; +import axios from "axios"; +import { toast } from "sonner"; +import { createReferralCode, fetchReferralCode } from "@/actions"; +import { getCookie } from "@/actions/cookie_actions"; + +export default function ReferralsPage() { + const [isLoggedIn, setIsLoggedIn] = useState(false); + const [referralCode, setReferralCode] = useState(""); + const [customCode, setCustomCode] = useState(""); + const [isCustomizing, setIsCustomizing] = useState(false); + const [customCodeError, setCustomCodeError] = useState(""); + const [referralStats, setReferralStats] = useState({ + totalReferrals: 0, + pendingReferrals: 0, + completedReferrals: 0, + rewards: 0, + }); + const [isLoading, setIsLoading] = useState(false); + const router = useRouter(); + + // Check if user is logged in and fetch referral data + useEffect(() => { + const checkLoginStatus = async () => { + try { + // Check if token exists in cookies + + const token = await getCookie("token"); + console.log(document.cookie) + setIsLoggedIn(!!token); + + if (token) { + await createReferralCode(); + await fetchReferralData(); + } + } catch (error) { + console.error("Error checking login status:", error); + } + }; + + checkLoginStatus(); + }, []); + + // Fetch referral data from API + const fetchReferralData = async () => { + setIsLoading(true); + try { + const response = await fetchReferralCode(); + console.log(response, "here is the reponse "); + + if (response.referralCode) { + setReferralCode(response.referralCode.code || ""); + setReferralStats({ + totalReferrals: response.totalReferrals || 0, + pendingReferrals: response.pendingReferrals || 0, + completedReferrals: response.completedReferrals || 0, + rewards: response.rewards || 0, + }); + } + } catch (error: any) { + console.error("Error fetching referral data:", error); + toast.error("Failed to fetch referral data", { + description: error.response?.data?.message || error.message, + }); + } finally { + setIsLoading(false); + } + }; + + // Handle successful login + const handleLoginSuccess = async () => { + setIsLoggedIn(true); + await fetchReferralData(); + }; + + const handleLogout = async () => { + try { + await axios.post('/api/auth/logout'); + setIsLoggedIn(false); + setReferralCode(""); + setReferralStats({ + totalReferrals: 0, + pendingReferrals: 0, + completedReferrals: 0, + rewards: 0, + }); + toast.success("Logged out successfully"); + } catch (error: any) { + console.error("Logout error:", error); + toast.error("Logout failed", { + description: error.response?.data?.message || error.message, + }); + } + }; + + const copyReferralCode = () => { + navigator.clipboard.writeText(referralCode); + toast.success("Referral code copied to clipboard!"); + }; + + const toggleCustomCodeInput = () => { + setIsCustomizing(!isCustomizing); + setCustomCode(""); + setCustomCodeError(""); + }; + + const validateCustomCode = (code: string) => { + // Basic validation rules + if (code.length < 6) { + return "Code must be at least 6 characters"; + } + if (code.length > 12) { + return "Code must be at most 12 characters"; + } + if (!/^[A-Za-z0-9]+$/.test(code)) { + return "Code can only contain letters and numbers"; + } + return ""; + }; + + const saveCustomCode = async () => { + // Validate the custom code + const error = validateCustomCode(customCode); + if (error) { + setCustomCodeError(error); + return; + } + + setIsLoading(true); + try { + const response = await axios.post('/api/referrals/custom-code', { + customCode: customCode.toUpperCase() + }, { + withCredentials: true, + headers: { + 'Content-Type': 'application/json', + }, + }); + + // Update the referral code + setReferralCode(customCode.toUpperCase()); + setIsCustomizing(false); + setCustomCode(""); + setCustomCodeError(""); + + toast.success("Custom code saved successfully"); + } catch (error: any) { + console.error("Error saving custom code:", error); + toast.error("Failed to save custom code", { + description: error.response?.data?.message || error.message, + }); + setCustomCodeError("Failed to save custom code. Please try again."); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+
+ +

+ Refer & Earn Rewards +

+
+ +

+ Share the gift of personalized learning with your friends and earn exciting rewards! +

+
+
+ +
+
+ + Referral Program + +
+ +
+ {!isLoggedIn ? ( +
+ +

+ Start Referring Friends +

+
+ +

+ Login to get your unique referral code and start earning rewards when your friends join Leadlly. +

+
+ +
+ +
+
+
+ ) : ( +
+ +

+ Your Referral Dashboard +

+
+ +
+ +
+

Your Referral Code:

+ +
+
+ + {isCustomizing ? ( + +
+
+ setCustomCode(e.target.value)} + placeholder="Enter custom code" + className="bg-purple-50 text-purple-800 font-mono text-xl py-2 px-4 rounded-l-lg flex-grow border-2 border-purple-100 focus:outline-none focus:border-purple-300" + maxLength={12} + disabled={isLoading} + /> + +
+ {customCodeError && ( +

{customCodeError}

+ )} +

+ Custom code must be 6-12 characters and can only contain letters and numbers. +

+
+
+ ) : ( + +
+
+ {isLoading ? "Loading..." : referralCode} +
+ +
+
+ )} +
+ +
+ +
+

Total Referrals

+

+ {isLoading ? "..." : referralStats.totalReferrals} +

+
+
+ +
+

Pending

+

+ {isLoading ? "..." : referralStats.pendingReferrals} +

+
+
+ +
+

Completed

+

+ {isLoading ? "..." : referralStats.completedReferrals} +

+
+
+ +
+

Rewards Earned

+

+ ₹{isLoading ? "..." : referralStats.rewards} +

+
+
+
+ + + + +
+ )} +
+
+ +
+ +

+ How It Works +

+
+ +
+ +
+
+ 1 +
+

Share Your Code

+

Share your unique referral code with friends and family

+
+
+ + +
+
+ 2 +
+

Friends Join

+

Your friends sign up using your referral code

+
+
+ + +
+
+ 3 +
+

Earn Rewards

+

Get exciting rewards for each successful referral

+
+
+
+
+ +
+ +

+ Referral Rewards +

+
+ +

+ Earn exciting rewards for each friend who joins Leadlly using your referral code! +

+
+ +
+ +
+

For You

+
    +
  • ₹100 for each successful referral
  • +
  • Premium features unlock at 5 referrals
  • +
  • Exclusive mentorship session at 10 referrals
  • +
+
+
+ + +
+

For Your Friends

+
    +
  • ₹50 discount on their first purchase
  • +
  • Free access to premium study materials
  • +
  • 7-day trial of premium features
  • +
+
+
+
+
+
+
+
+ ); +} \ No newline at end of file From f00ef32f9023c2eacbe4b67bcb4bb70a015f43b8 Mon Sep 17 00:00:00 2001 From: shivang-16 Date: Sat, 17 May 2025 03:55:58 +0530 Subject: [PATCH 2/2] social-shares added --- package-lock.json | 10 +++ package.json | 1 + src/app/referrals/page.tsx | 138 ++++++++++++++++++++++++++++++++++++- 3 files changed, 146 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 45f21ce..e36a9c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "next": "14.2.5", "react": "^18", "react-dom": "^18", + "react-icons": "^5.5.0", "react-pdf": "^9.1.1", "sonner": "^2.0.3", "tailwind-merge": "^2.4.0", @@ -3133,6 +3134,15 @@ "react": "^18.3.1" } }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-pdf": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-9.1.1.tgz", diff --git a/package.json b/package.json index 1d3e82e..8d749e4 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "next": "14.2.5", "react": "^18", "react-dom": "^18", + "react-icons": "^5.5.0", "react-pdf": "^9.1.1", "sonner": "^2.0.3", "tailwind-merge": "^2.4.0", diff --git a/src/app/referrals/page.tsx b/src/app/referrals/page.tsx index 5f08ca5..c18fe4a 100644 --- a/src/app/referrals/page.tsx +++ b/src/app/referrals/page.tsx @@ -9,6 +9,8 @@ import axios from "axios"; import { toast } from "sonner"; import { createReferralCode, fetchReferralCode } from "@/actions"; import { getCookie } from "@/actions/cookie_actions"; +import { FaCopy, FaShare, FaWhatsapp, FaInstagram, FaLinkedin } from "react-icons/fa"; +import { IoMdMore } from "react-icons/io"; export default function ReferralsPage() { const [isLoggedIn, setIsLoggedIn] = useState(false); @@ -16,6 +18,7 @@ export default function ReferralsPage() { const [customCode, setCustomCode] = useState(""); const [isCustomizing, setIsCustomizing] = useState(false); const [customCodeError, setCustomCodeError] = useState(""); + const [showShareOptions, setShowShareOptions] = useState(false); const [referralStats, setReferralStats] = useState({ totalReferrals: 0, pendingReferrals: 0, @@ -30,7 +33,6 @@ export default function ReferralsPage() { const checkLoginStatus = async () => { try { // Check if token exists in cookies - const token = await getCookie("token"); console.log(document.cookie) setIsLoggedIn(!!token); @@ -47,6 +49,33 @@ export default function ReferralsPage() { checkLoginStatus(); }, []); + // Close share options when clicking outside + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + // Don't close if clicking on the share button itself + const shareButton = document.getElementById('share-button'); + if (shareButton && shareButton.contains(e.target as Node)) { + return; + } + + // Don't close if clicking inside the dropdown + const dropdown = document.getElementById('share-dropdown'); + if (dropdown && dropdown.contains(e.target as Node)) { + return; + } + + setShowShareOptions(false); + }; + + if (showShareOptions) { + document.addEventListener('click', handleClickOutside); + } + + return () => { + document.removeEventListener('click', handleClickOutside); + }; + }, [showShareOptions]); + // Fetch referral data from API const fetchReferralData = async () => { setIsLoading(true); @@ -161,6 +190,46 @@ export default function ReferralsPage() { } }; + const toggleShareOptions = (e: React.MouseEvent) => { + e.stopPropagation(); + console.log(showShareOptions, "Share button clicked"); + setShowShareOptions(!showShareOptions); + }; + + const shareMessage = `🚀 Start planning your future with Leadlly and get exclusive discounts! Use my referral code: ${referralCode} and unlock premium learning resources. Join me on this learning journey! 🎓✨ #Leadlly #PersonalizedLearning`; + + const shareViaWhatsApp = () => { + const url = `https://wa.me/?text=${encodeURIComponent(shareMessage)}`; + window.open(url, '_blank'); + }; + + const shareViaInstagram = () => { + // Instagram doesn't support direct sharing via URL, so we'll copy to clipboard + navigator.clipboard.writeText(shareMessage); + toast.success("Message copied! Open Instagram to share", { + description: "Instagram doesn't support direct sharing. Paste the copied message in your Instagram." + }); + }; + + const shareViaLinkedIn = () => { + const url = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent("https://leadlly.in")}&summary=${encodeURIComponent(shareMessage)}`; + window.open(url, '_blank'); + }; + + const shareViaOthers = () => { + if (navigator.share) { + navigator.share({ + title: 'Join Leadlly with my referral code', + text: shareMessage, + url: 'https://leadlly.in', + }) + .catch((error) => console.log('Error sharing', error)); + } else { + navigator.clipboard.writeText(shareMessage); + toast.success("Message copied to clipboard!"); + } + }; + return (
@@ -267,14 +336,77 @@ export default function ReferralsPage() {
+
+ + + +
)} + {showShareOptions && ( +
+
+ + + + +
+
+ )}