From bd5f7ad46ea9191deb3f1e8bd55769a29ed1961e Mon Sep 17 00:00:00 2001 From: Ashish Padhy <100484401+Shurtu-gal@users.noreply.github.com> Date: Thu, 2 May 2024 02:29:53 +0530 Subject: [PATCH] feat: add email link log-in and do final touches (#44) --- package.json | 1 + pnpm-lock.yaml | 14 +++ src/app/index.jsx | 13 ++ src/components/FaqSection/faq.jsx | 2 +- src/components/form/FormContainer.jsx | 113 +++++++++++------- src/components/shared/marginals/NavBar.jsx | 18 +-- src/components/shared/partials/FormInputs.jsx | 70 +++++++---- src/config/.gitkeep | 0 src/context/AuthContext.jsx | 20 +++- src/data/.gitkeep | 0 src/{config/content => data}/faqData.js | 0 src/data/formInformation.js | 35 +++--- src/firebase/login.js | 27 ++--- src/firebase/registration.js | 14 ++- src/pages/register.jsx | 12 +- 15 files changed, 222 insertions(+), 117 deletions(-) delete mode 100644 src/config/.gitkeep delete mode 100644 src/data/.gitkeep rename src/{config/content => data}/faqData.js (100%) diff --git a/package.json b/package.json index 2072a63..53630e3 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.21.3", + "react-toastify": "^10.0.5", "smooth-scroll": "^16.1.3", "tailwind-merge": "^2.2.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7654bd..fb2faee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ dependencies: react-router-dom: specifier: ^6.21.3 version: 6.21.3(react-dom@18.2.0)(react@18.2.0) + react-toastify: + specifier: ^10.0.5 + version: 10.0.5(react-dom@18.2.0)(react@18.2.0) smooth-scroll: specifier: ^16.1.3 version: 16.1.3 @@ -3091,6 +3094,17 @@ packages: react: 18.2.0 dev: false + /react-toastify@10.0.5(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==} + peerDependencies: + react: '>=18' + react-dom: '>=18' + dependencies: + clsx: 2.1.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} diff --git a/src/app/index.jsx b/src/app/index.jsx index 8e5e486..ea4ba33 100644 --- a/src/app/index.jsx +++ b/src/app/index.jsx @@ -4,11 +4,24 @@ import { Routes } from 'react-router-dom'; import { Route } from 'react-router-dom'; import Pages from '../pages'; import { AuthProvider } from '../context/AuthContext'; +import { ToastContainer } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; export default function App() { return ( <> + }> } /> diff --git a/src/components/FaqSection/faq.jsx b/src/components/FaqSection/faq.jsx index b783a55..44957a2 100644 --- a/src/components/FaqSection/faq.jsx +++ b/src/components/FaqSection/faq.jsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import faqData from '../../config/content/faqData.js'; +import faqData from '../../data/faqData'; import { PersonalizedText, Heading, Paragraph } from '../shared/index.js'; function FAQ() { diff --git a/src/components/form/FormContainer.jsx b/src/components/form/FormContainer.jsx index 24cbab5..9f2cce2 100644 --- a/src/components/form/FormContainer.jsx +++ b/src/components/form/FormContainer.jsx @@ -8,15 +8,19 @@ import { donation, feeCoverage, initialContent, inputContent, lastPartContent } import { storeFormData } from '../../firebase/registration'; import { toCloudinary } from './uploadingFiles'; import { registrationOptions, branchOptions } from '../../data/formInformation'; +import { toast } from 'react-toastify'; +import Button from '../shared/Button'; const FormContainer = () => { const { userInfo } = useContext(AuthContext); - var [currentUser, userData] = userInfo; + const [currentUser, userData] = userInfo; + const [errorMessage, setErrorMessage] = useState(''); + const [formData, setFormData] = useState({}); + const verifiedEmail = currentUser?.email ? true : false; const [isValid, setValid] = useState({ - recRollNumber: false, - name: false, - email: false, + recRollNumber: true, + name: true, country: true, state: true, city: true, @@ -25,31 +29,31 @@ const FormContainer = () => { regType: true, profileImage: true, }); - const [isEmpty, setEmpty] = useState({ - recRollNumber: true, - name: true, - email: true, - country: false, - state: false, - city: false, - prefix: false, - mobile: false, - regType: false, - profileImage: false, - }); - var checkValidity = Object.values(isValid).every(value => value); - var checkIfEmpty = Object.values(isEmpty).some(value => value); - var notAllowed = !checkValidity || checkIfEmpty; - const [errorMessage, setErrorMessage] = useState(''); + const checkValidity = Object.values(isValid).every(value => value) && verifiedEmail; + const checkIfEmpty = Object.entries(formData).some( + ([key, value]) => + value === '' && + inputContent.find(item => { + if (Array.isArray(item.id)) { + return item.id.includes(key); + } else { + return item.id === key; + } + })?.required + ); - const [formData, setFormData] = useState({}); + const notAllowed = !checkValidity || checkIfEmpty; const setInputValue = async (key, value, file) => { if (!key) return; if (key === 'profileImage') { - var imgURL = await toCloudinary(file); + const imgURL = await toast.promise(toCloudinary(file), { + loading: 'Uploading image...', + success: 'Image uploaded successfully', + error: 'Error uploading image', + }); setFormData(prev => ({ ...prev, profileImage: imgURL, @@ -66,31 +70,33 @@ const FormContainer = () => { if (userData) { setFormData({ uid: currentUser?.uid?.toString() || '', - recRollNumber: userData?.rollNumber || '', - branch: userData?.branch || '', + recRollNumber: userData?.recRollNumber || '', + branch: userData?.branch || branchOptions[0], name: userData?.name || '', - email: userData?.email || '', - county: userData?.county || '', + email: currentUser?.email || '', + country: userData?.country || '', state: userData?.state || '', city: userData?.city || '', prefix: userData?.prefix || '', mobile: userData?.mobile || '', - regType: userData?.regType || '', + regType: userData?.regType || registrationOptions[0], profileImage: userData?.profileImage || '', testimonial: userData?.testimonial || '', - googlName: currentUser?.name || '', - googleMail: currentUser?.email || '', }); } - }, [userData]); + }, [userData, currentUser]); const registerUser = async e => { if (notAllowed) { - return; + toast.error('Please fill all the required fields'); } e.preventDefault(); try { - const documentId = await storeFormData(formData); + const documentId = await toast.promise(storeFormData(formData), { + loading: 'Registering...', + success: 'Registration successful', + error: 'Error registering', + }); return documentId; } catch (error) { console.error('Error:', error); @@ -159,7 +165,12 @@ const FormContainer = () => { }}> Choose your Branch {' '} - setInputValue('branch', e.target.value)} /> + setInputValue('branch', e.target.value)} + value={formData.branch} + /> {inputContent.map(item => ( { className='inline mr-3 w-[31.3%]' onChange={e => setInputValue(id, e.target.value, e.target.validated)} validated={setValid} - checkEmpty={setEmpty} errormsg={setErrorMessage} formData={{ type: item.type[idx], minLength: item.minLength[idx], maxLength: item.maxLength[idx], regex: item.regex[idx], + value: item.type[idx] === 'number' ? parseInt(formData[id]) : formData[id] || '', id: id, - placeholder: formData?.id || item.placeholder[idx], + placeholder: item.placeholder[idx], + disabled: item.id === 'email' && verifiedEmail ? true : false, + verified: verifiedEmail, }} /> )) @@ -199,7 +212,6 @@ const FormContainer = () => { setInputValue(item.id, e.target.value, e.target.files ? e.target.files[0] : null, e.target.validated) } validated={setValid} - checkEmpty={setEmpty} errormsg={setErrorMessage} formData={{ type: item.type, @@ -208,6 +220,9 @@ const FormContainer = () => { regex: item.regex, id: item.id, placeholder: item.placeholder, + value: formData[item.id], + verified: verifiedEmail, + disabled: item.id === 'email' && verifiedEmail ? true : false, }} required={item.required} /> @@ -224,7 +239,12 @@ const FormContainer = () => { }}> Registration Type {' '} - setInputValue('regType', e.target.value)} /> + setInputValue('regType', e.target.value)} + value={formData.regType} + /> { {lastPartContent} - + {notAllowed ? ( {errorMessage} diff --git a/src/components/shared/marginals/NavBar.jsx b/src/components/shared/marginals/NavBar.jsx index d92ffab..e23eec2 100644 --- a/src/components/shared/marginals/NavBar.jsx +++ b/src/components/shared/marginals/NavBar.jsx @@ -1,13 +1,10 @@ -import { useState, useContext } from 'react'; -import { AuthContext } from '../../../context/AuthContext'; +import { useState } from 'react'; import { Link } from 'react-router-dom'; import Text from '../typography/Text'; import Navigation from '../../../data/Navigation'; import image from '../../../assets/images/image.png'; -import Button from '../Button'; import Logo from '../Logo'; import Hamburger from '../Hamburger'; -import { signInWithGoogle, signOutUser } from '../../../firebase/login'; import SmoothScroll from 'smooth-scroll'; // Function Returning new scroll object @@ -29,8 +26,6 @@ const handleScroll = id => { }; function NavBar() { - const { userInfo, setUserData } = useContext(AuthContext); - const { navItems, logo } = Navigation; const [isNavOpen, setIsNavOpen] = useState(false); @@ -49,7 +44,7 @@ function NavBar() {
- + { userInfo[0].uid && Logout} + */}
{isNavOpen && ( -
    - > +
    )} diff --git a/src/components/shared/partials/FormInputs.jsx b/src/components/shared/partials/FormInputs.jsx index 5a60ad4..3364b3b 100644 --- a/src/components/shared/partials/FormInputs.jsx +++ b/src/components/shared/partials/FormInputs.jsx @@ -1,10 +1,14 @@ -export function Inputs({ className, formData, onChange, validated, checkEmpty, errormsg, required }) { - const { type, minLength, maxLength, regex, id, placeholder, value } = formData; +import { toast } from 'react-toastify'; +import { signInWithEmailLink, signOutUser } from '../../../firebase/login'; +import Button from '../Button'; + +export function Inputs({ className, formData, onChange, validated, errormsg, required }) { + const { type, minLength, maxLength, regex, id, placeholder, value, verified, disabled } = formData; const validateInput = event => { const value = event.target.value; if (regex && value) { const isValid = value.match(regex); - const isEmpty = !required || value === ''; + const isEmpty = required && value === ''; if (!isValid) { event.target.style.border = '1px solid #b91c1c'; @@ -17,10 +21,8 @@ export function Inputs({ className, formData, onChange, validated, checkEmpty, e } if (isEmpty) { - updateState(id, checkEmpty, true); errormsg(prevMsg => prevMsg + `\nEmpty input for ${id}`); } else { - updateState(id, checkEmpty, false); errormsg(prevMsg => prevMsg.replace(new RegExp(`\\nEmpty input for ${id}`, 'g'), '')); } } @@ -33,22 +35,50 @@ export function Inputs({ className, formData, onChange, validated, checkEmpty, e })); }; + const handleEmailVerification = async () => { + if (verified) { + toast.promise(signOutUser(), { + loading: 'Logging out...', + success: 'Logged out successfully', + error: 'Error logging out', + }); + } else { + toast.promise(signInWithEmailLink(value), { + loading: 'Sending verification email...', + success: 'Verification email sent', + error: 'Error sending verification email', + }); + } + }; + return ( - + + + {type === 'email' && ( + + )} + ); } diff --git a/src/config/.gitkeep b/src/config/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/context/AuthContext.jsx b/src/context/AuthContext.jsx index d098fce..0849499 100644 --- a/src/context/AuthContext.jsx +++ b/src/context/AuthContext.jsx @@ -1,6 +1,7 @@ import { createContext, useState, useEffect } from 'react'; import { getUserData, auth } from '../firebase/login'; -import { onAuthStateChanged } from 'firebase/auth'; +import { isSignInWithEmailLink, onAuthStateChanged, signInWithEmailLink } from 'firebase/auth'; +import { toast } from 'react-toastify'; export const AuthContext = createContext(); @@ -23,5 +24,22 @@ export const AuthProvider = ({ children }) => { listenForAuthChanges(); }, []); + useEffect(() => { + if (isSignInWithEmailLink(auth, window.location.href) && window.localStorage.getItem('emailForSignIn')) { + const email = window.localStorage.getItem('emailForSignIn'); + signInWithEmailLink(auth, email, window.location.href) + .then(result => { + if (result) { + toast.success('Email link verified. You can register now.'); + window.localStorage.removeItem('emailForSignIn'); + } + }) + .catch(error => { + console.error('Error signing in with email link:', error); + toast.error('Error signing in with email link. Please try again.'); + }); + } + }, []); + return {children}; }; diff --git a/src/data/.gitkeep b/src/data/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/config/content/faqData.js b/src/data/faqData.js similarity index 100% rename from src/config/content/faqData.js rename to src/data/faqData.js diff --git a/src/data/formInformation.js b/src/data/formInformation.js index 2f8369b..cb93293 100644 --- a/src/data/formInformation.js +++ b/src/data/formInformation.js @@ -36,13 +36,24 @@ export const currentUser = { state: '', city: '', prefix: '', - phoneNumber: '', + mobile: '', regType: '', }; export const inputContent = [ { key: 1, + id: 'email', + label: 'Email Address', + type: 'email', + regex: '^[a-zA-Z0-9+_.-]+@[a-zA-Z0-9.-]+$', + maxLength: 50, + minLength: 3, + placeholder: 'Enter your Email here', + required: true, + }, + { + key: 2, id: 'recRollNumber', label: 'REC Roll Number', type: 'text', @@ -53,7 +64,7 @@ export const inputContent = [ required: true, }, { - key: 2, + key: 3, id: 'name', label: 'Name', type: 'text', @@ -63,17 +74,6 @@ export const inputContent = [ placeholder: 'Enter Your Name here', required: true, }, - { - key: 3, - id: 'email', - label: 'Email Address', - type: 'email', - regex: '^[a-zA-Z0-9+_.-]+@[a-zA-Z0-9.-]+$', - maxLength: 50, - minLength: 3, - placeholder: 'Enter your Email here', - required: true, - }, { key: 4, id: ['country', 'state', 'city'], @@ -83,16 +83,18 @@ export const inputContent = [ minLength: [3, 3, 3], maxLength: [50, 50, 50], placeholder: ['Country', 'State', 'City'], + required: true, }, { key: 5, - id: ['prefix', 'phoneNumber'], + id: ['prefix', 'mobile'], label: 'Contact Number', - type: ['number', 'number'], - regex: ['^+?d{1,3}', '^[0-9]{10}$'], + type: ['string', 'number'], + regex: ['^\\+?[0-9]*$', '^[0-9]{10}$'], maxLength: [4, 10], minLength: [1, 10], placeholder: ['Prefix +91', 'Enter your Phone number '], + required: true, }, { key: 6, @@ -103,6 +105,7 @@ export const inputContent = [ maxLength: 50, minLength: 3, placeholder: 'Upload your Image for Profile', + required: false, }, ]; diff --git a/src/firebase/login.js b/src/firebase/login.js index 7bd72eb..d5bd4f0 100644 --- a/src/firebase/login.js +++ b/src/firebase/login.js @@ -1,24 +1,21 @@ -import { getAuth, GoogleAuthProvider, signInWithPopup, signOut } from 'firebase/auth'; +import { getAuth, sendSignInLinkToEmail, signOut } from 'firebase/auth'; import { query, where, getDocs, collection } from 'firebase/firestore'; import { db, app } from './firebaseConfig'; export const auth = getAuth(app); -export const signInWithGoogle = async () => { - const provider = new GoogleAuthProvider(); - return await signInWithPopup(auth, provider) - .then(async result => { - const user = result.user; - const loggedUser = { name: user.displayName, email: user.email, uid: user.uid }; - - const logUserRegData = await getUserData(user.uid); - return [loggedUser, logUserRegData]; - }) - .catch(error => { - const errorMessage = error.message; - console.error(errorMessage); - return [{}, {}]; +export const signInWithEmailLink = async email => { + try { + await sendSignInLinkToEmail(auth, email, { + url: window.location.href, + handleCodeInApp: true, }); + window.localStorage.setItem('emailForSignIn', email); + return true; + } catch (error) { + console.error('Error sending email link:', error); + return false; + } }; export const getUserData = async userId => { diff --git a/src/firebase/registration.js b/src/firebase/registration.js index 4fe26dc..8e08bb7 100644 --- a/src/firebase/registration.js +++ b/src/firebase/registration.js @@ -1,11 +1,17 @@ -import { collection, addDoc } from 'firebase/firestore'; +import { collection, addDoc, getDocs, query, where, updateDoc } from 'firebase/firestore'; import { db } from './firebaseConfig'; export const storeFormData = async formData => { try { - const docRef = await addDoc(collection(db, 'users'), formData); - - return docRef.id; + const docQuery = query(collection(db, 'users'), where('email', '==', formData.email)); + const docSnapshot = await getDocs(docQuery); + if (!docSnapshot.empty) { + await updateDoc(docSnapshot.docs[0].ref, formData); + return docSnapshot.id; + } else { + const docRef = await addDoc(collection(db, 'users'), formData); + return docRef.id; + } } catch (error) { console.error('Error adding document: ', error); throw error; diff --git a/src/pages/register.jsx b/src/pages/register.jsx index cb1c2ad..f7e3b47 100644 --- a/src/pages/register.jsx +++ b/src/pages/register.jsx @@ -1,9 +1,15 @@ import FormContainer from '../components/form/FormContainer'; +import NavBar from '../components/shared/marginals/NavBar'; +import Footer from '../components/shared/marginals/footer'; export default function register() { return ( -
    - -
    + <> + +
    + +
    +