diff --git a/db.json-schema.json b/db.json-schema.json index 169e83c..e11816a 100644 --- a/db.json-schema.json +++ b/db.json-schema.json @@ -214,6 +214,10 @@ "generalLocationName": { "type": "string", "description": "A name for the location, generally 'City, State'." + }, + "hasProfile": { + "type": "boolean", + "description": "True if this user has set up a profile" } }, "required": ["generalLocation", "generalLocationName"] diff --git a/firestore.rules b/firestore.rules index 915e8ed..2825aa4 100644 --- a/firestore.rules +++ b/firestore.rules @@ -43,7 +43,7 @@ service cloud.firestore { // Public user profiles match /users_public/{userId} { allow read; - allow write: if false; // only written to by indexUser cloud function + allow write: if isThisUser(userId); } // Private request data diff --git a/src/components/ClickableMap/ClickableMap.js b/src/components/ClickableMap/ClickableMap.js index c545b93..89631c5 100644 --- a/src/components/ClickableMap/ClickableMap.js +++ b/src/components/ClickableMap/ClickableMap.js @@ -281,7 +281,7 @@ function ClickableMap({ onLocationChange, locationInfo }) { data-test="address-entry" {...getInputProps({ ...params, - placeholder: 'Enter Address', + placeholder: 'Address Search', })} InputProps={{ ...params.InputProps, diff --git a/src/constants/paths.js b/src/constants/paths.js index 1043cb0..088a788 100644 --- a/src/constants/paths.js +++ b/src/constants/paths.js @@ -3,6 +3,8 @@ export const ACCOUNT_PATH = '/account'; export const LOGIN_PATH = '/login'; export const LOGOUT_PATH = '/logout'; export const SEARCH_PATH = '/search'; +export const USER_PROFILE_PATH = '/user-profile'; +export const PASSWORD_RESET_PATH = '/password-reset'; export const PRIVACY_POLICY_PATH = '/privacy-policy'; export const TERMS_OF_SERVICE_PATH = '/terms-of-service'; diff --git a/src/containers/Navbar/AccountMenu.js b/src/containers/Navbar/AccountMenu.js index 1f5d44a..a20f8a9 100644 --- a/src/containers/Navbar/AccountMenu.js +++ b/src/containers/Navbar/AccountMenu.js @@ -6,7 +6,7 @@ import MenuItem from '@material-ui/core/MenuItem'; import IconButton from '@material-ui/core/IconButton'; import AccountCircle from '@material-ui/icons/AccountCircle'; import { makeStyles } from '@material-ui/core/styles'; -import { ACCOUNT_PATH, MY_REQUESTS_PATH } from 'constants/paths'; +import { USER_PROFILE_PATH, MY_REQUESTS_PATH } from 'constants/paths'; const useStyles = makeStyles(() => ({ buttonRoot: { @@ -54,8 +54,11 @@ function AccountMenu() { onClick={closeAccountMenu}> My Requests - - Account + + Profile Sign Out diff --git a/src/routes/Login/components/LoginForm/LoginForm.js b/src/routes/Login/components/LoginForm/LoginForm.js index 05b2da4..8b2f96b 100644 --- a/src/routes/Login/components/LoginForm/LoginForm.js +++ b/src/routes/Login/components/LoginForm/LoginForm.js @@ -1,10 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useForm } from 'react-hook-form'; -import TextField from '@material-ui/core/TextField'; -import { makeStyles } from '@material-ui/core/styles'; -import Button from '@material-ui/core/Button'; +import { TextField, makeStyles, Button } from '@material-ui/core'; +import { Link } from 'react-router-dom'; import { validateEmail } from 'utils/form'; +import { USER_PROFILE_PATH, PASSWORD_RESET_PATH } from 'constants/paths'; import styles from './LoginForm.styles'; const useStyles = makeStyles(styles); @@ -50,6 +50,7 @@ function LoginForm({ onSubmit }) { error={!!errors.password} helperText={errors.password && 'Password is required'} /> + Forgot Password{' '}
+ I'm new, sign me up!{' '} ); } diff --git a/src/routes/Login/components/LoginPage/LoginPage.js b/src/routes/Login/components/LoginPage/LoginPage.js index 562a900..b610dc6 100644 --- a/src/routes/Login/components/LoginPage/LoginPage.js +++ b/src/routes/Login/components/LoginPage/LoginPage.js @@ -5,8 +5,8 @@ import { useAuth, useFirestore } from 'reactfire'; import Paper from '@material-ui/core/Paper'; import { makeStyles } from '@material-ui/core/styles'; import GoogleButton from 'react-google-button'; -import { NEW_USER_PATH, SEARCH_PATH } from 'constants/paths'; -import { USERS_COLLECTION } from 'constants/collections'; +import { USER_PROFILE_PATH, SEARCH_PATH } from 'constants/paths'; +import { USERS_PUBLIC_COLLECTION } from 'constants/collections'; import useNotifications from 'modules/notification/useNotifications'; import LoadingSpinner from 'components/LoadingSpinner'; import LoginForm from '../LoginForm'; @@ -20,33 +20,18 @@ function LoginPage() { const history = useHistory(); const firestore = useFirestore(); const [isLoading, setLoadingState] = useState(false); - const { showError } = useNotifications(); + const { showError, showMessage } = useNotifications(); - async function updateUserAndRedirect(authState) { - try { - // Write user profile if it doesn't exist, otherwise redirect to search page - const userSnap = await firestore - .doc(`${USERS_COLLECTION}/${authState.user.uid}`) - .get(); - // Redirect to search page if user exists - if (userSnap.get('preciseLocation')) { - history.replace(SEARCH_PATH); - } else { - // Write user object then redirect to new user page - const { email, displayName, photoURL, providerData } = authState.user; - const newProfile = { email, displayName, photoURL }; - if (providerData && providerData.length) { - newProfile.providerData = [{ ...providerData[0] }]; - } - await userSnap.ref.set(newProfile, { merge: true }); - window.setTimeout(() => { - history.replace(NEW_USER_PATH); - }, 1000); - } - } catch (err) { - setLoadingState(false); - showError(err.message); + async function hasProfile(user) { + const userSnap = await firestore + .doc(`${USERS_PUBLIC_COLLECTION}/${user.uid}`) + .get(); + const data = await userSnap.data(); + console.log('Checking account', data); // eslint-disable-line no-console + if (!!data && !!data.d && !!data.d.hasProfile) { + return true; } + return false; } async function googleLogin() { @@ -58,8 +43,12 @@ function LoginPage() { const signInMethod = isMobile ? 'signInWithRedirect' : 'signInWithPopup'; try { const authState = await auth[signInMethod](provider); - // Write user profile if it doesn't exist, otherwise redirect to search page - await updateUserAndRedirect(authState); + if (await hasProfile(authState.user)) { + history.replace(SEARCH_PATH); + return; + } + showMessage('You need to finish filling out your profile.'); + history.replace(USER_PROFILE_PATH); } catch (err) { setLoadingState(false); showError(err.message); @@ -72,26 +61,14 @@ function LoginPage() { creds.email, creds.password, ); - // Write user profile if it doesn't exist, otherwise redirect to search page - await updateUserAndRedirect(authState); - } catch (err) { - try { - // Create user if they do not exist - if (err.code === 'auth/user-not-found') { - const authState = await auth.createUserWithEmailAndPassword( - creds.email, - creds.password, - ); - // Write user profile if it doesn't exist, otherwise redirect to search page - await updateUserAndRedirect(authState); - } - } catch (err2) { - if (err2.message === 'auth/user-exists') { - showError(err.message); - } else { - showError(err2.message); - } + if (await hasProfile(authState.user)) { + history.replace(SEARCH_PATH); + return; } + showMessage('You need to finish filling out your profile.'); + history.replace(USER_PROFILE_PATH); + } catch (err) { + showError(err.message); } } diff --git a/src/routes/PasswordReset/components/PasswordResetPage/PasswordResetPage.js b/src/routes/PasswordReset/components/PasswordResetPage/PasswordResetPage.js new file mode 100644 index 0000000..c9e6144 --- /dev/null +++ b/src/routes/PasswordReset/components/PasswordResetPage/PasswordResetPage.js @@ -0,0 +1,102 @@ +import React from 'react'; +import { validateEmail } from 'utils/form'; +import { useAuth } from 'reactfire'; +import { useForm } from 'react-hook-form'; +import { + Paper, + Typography, + Grid, + TextField, + makeStyles, + Button, + Container, +} from '@material-ui/core'; +import { Helmet } from 'react-helmet'; +import useNotifications from 'modules/notification/useNotifications'; +import styles from './PasswordResetPage.styles'; + +const useStyles = makeStyles(styles); + +function PasswordReset() { + const classes = useStyles(); + const auth = useAuth(); + const { showError, showMessage } = useNotifications(); + + const { + register, + handleSubmit, + errors, + getValues, + formState: { isSubmitting, isValid }, + } = useForm({ + mode: 'onChange', + nativeValidation: false, + }); + + async function handleLinkSend() { + try { + await auth.sendPasswordResetEmail(getValues('email')); + } catch (err) { + showError(err.message); + return; + } + showMessage('Email sent.'); + } + + return ( + + + Password Reset + + + Password Reset + + +
+ +
+ + + Enter your email address, and if you have an account you will be + emailed a link to reset your password. + + + + + + + +
+ +
+
+
+
+
+
+ ); +} + +export default PasswordReset; diff --git a/src/routes/PasswordReset/components/PasswordResetPage/PasswordResetPage.styles.js b/src/routes/PasswordReset/components/PasswordResetPage/PasswordResetPage.styles.js new file mode 100644 index 0000000..60a031b --- /dev/null +++ b/src/routes/PasswordReset/components/PasswordResetPage/PasswordResetPage.styles.js @@ -0,0 +1,23 @@ +export default (theme) => ({ + root: { + ...theme.flexColumnCenter, + justifyContent: 'flex-start', + flexGrow: 1, + height: '100%', + width: '100%', + margin: '.2rem', + }, + submit: { + ...theme.flexColumnCenter, + justifyContent: 'center', + flexGrow: 1, + textAlign: 'center', + padding: '1.25rem', + minWidth: '192px', + marginTop: '1.5rem', + }, + paper: { + paddingTop: theme.spacing(3), + paddingBottom: theme.spacing(1), + }, +}); diff --git a/src/routes/PasswordReset/components/PasswordResetPage/index.js b/src/routes/PasswordReset/components/PasswordResetPage/index.js new file mode 100644 index 0000000..1b7fd2a --- /dev/null +++ b/src/routes/PasswordReset/components/PasswordResetPage/index.js @@ -0,0 +1,3 @@ +import PasswordResetPage from './PasswordResetPage'; + +export default PasswordResetPage; diff --git a/src/routes/PasswordReset/index.js b/src/routes/PasswordReset/index.js new file mode 100644 index 0000000..c425003 --- /dev/null +++ b/src/routes/PasswordReset/index.js @@ -0,0 +1,11 @@ +import { loadable } from 'utils/router'; +import { PASSWORD_RESET_PATH as path } from 'constants/paths'; + +export default { + path, + component: loadable(() => + import( + /* webpackChunkName: 'Password Reset' */ './components/PasswordResetPage' + ), + ), +}; diff --git a/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js new file mode 100644 index 0000000..e7b1760 --- /dev/null +++ b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.js @@ -0,0 +1,670 @@ +import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import { + Typography, + TextField, + Paper, + Card, + Button, + Container, + Grid, + makeStyles, +} from '@material-ui/core'; +import { useHistory } from 'react-router-dom'; +import { Helmet } from 'react-helmet'; +import Stepper from '@material-ui/core/Stepper'; +import Step from '@material-ui/core/Step'; +import StepLabel from '@material-ui/core/StepLabel'; +import * as Yup from 'yup'; +import { useAuth, useFirestore, useUser } from 'reactfire'; +import useNotifications from 'modules/notification/useNotifications'; +import { + USERS_COLLECTION, + USERS_PRIVILEGED_COLLECTION, + USERS_PUBLIC_COLLECTION, +} from 'constants/collections'; +import ClickableMap from 'components/ClickableMap'; +import { geocode } from 'utils/geo'; +import LoadingSpinner from 'components/LoadingSpinner'; +import { useForm, FormContext, useFormContext } from 'react-hook-form'; +import { SEARCH_PATH } from 'constants/paths'; +import styles from './UserProfilePage.styles'; + +const useStyles = makeStyles(styles); + +function isGoogleLoggedIn(user) { + return !!user && user.providerData[0].providerId === 'google.com'; +} + +function hasProfile(userData) { + return !!userData && !!userData.email; +} + +function isLoggedIn(user) { + return !!user && !!user.uid; +} + +function UserProfile() { + const classes = useStyles(); + const firestore = useFirestore(); + const user = useUser(); + const auth = useAuth(); + const history = useHistory(); + const { showError, showMessage } = useNotifications(); + const [isLoading, setLoadingState] = useState(true); + const [userLocationInfo, setUserLocationInfo] = useState(null); + const [retries, setRetries] = useState(0); + const [userRef, setUserRef] = useState(null); + const [userData, setUserData] = useState(null); + const [activeStep, setActiveStep] = useState(isGoogleLoggedIn(user) ? 2 : 0); + + const userProfileSchema = useMemo( + () => + Yup.object().shape({ + firstName: Yup.string().min(2, 'Too Short').required('Required'), + lastName: Yup.string().min(2, 'Too Short').required('Required'), + email: Yup.string(), + phone: Yup.string().required('Required'), + password: Yup.string(), + confirmPassword: Yup.string().oneOf( + [Yup.ref('password'), null], + 'Passwords must match', + ), + address1: Yup.string(), + address2: Yup.string(), + city: Yup.string(), + state: Yup.string(), + zipcode: Yup.string(), + }), + [], + ); + + const steps = ['Email', 'Password', 'Contact Info', 'Location']; + + const userFieldsStepMap = { + email: 0, + password: 1, + confirmPassword: 1, + firstName: 2, + lastName: 2, + phone: 2, + address1: 3, + address2: 3, + city: 3, + state: 3, + zipcode: 3, + }; + + const formContext = useFormContext(); + + const useYupValidationResolver = (validationSchema) => + useCallback( + async (data) => { + try { + const values = await validationSchema.validate(data, { + abortEarly: false, + }); + + return { + values, + errors: {}, + }; + } catch (errors) { + const errs = errors.inner.reduce( + (allErrors, currentError) => ({ + ...allErrors, + [currentError.path]: { + type: currentError.type ?? 'validation', + message: currentError.message, + }, + }), + {}, + ); + console.log('Errors: ', errs, 'Form State: ', formContext); // eslint-disable-line no-console + let step = 3; + Object.keys(errs).forEach((field) => { + const errStep = userFieldsStepMap[field]; + step = errStep < step ? errStep : step; + }); + setActiveStep(step); + return { + values: {}, + errors: errs, + }; + } + }, + [validationSchema], + ); + + const validationResolver = useYupValidationResolver(userProfileSchema); + + // const { + // handleSubmit, + // errors, + // register, + // formState: { isValid, dirty }, + // getValues, + // setValue, + // } = useForm({ + const methods = useForm({ + validationResolver, + }); + + // Because of timing issues, this component will likely get run before the server has applied + // the requested document access resulting in almost a guranteed permission-denied error. So, + // we use this effect to monitor for permission-denied until the change has propagated, at which + // point, we do the actual doc subscription (next useEffect); + useEffect(() => { + console.log(userData, user); // eslint-disable-line no-console + async function getData() { + if (isLoggedIn(user) && userData == null) { + // Already authenticated and haven't loaded data previously + setLoadingState(true); + const ref = firestore.doc(`${USERS_PRIVILEGED_COLLECTION}/${user.uid}`); + let data = {}; + try { + console.log('ref', ref); // eslint-disable-line no-console + // Call it once because this will throw the permission exception. + const snap = await ref.get(); + data = snap.data(); + setUserData(data); + setUserRef(ref); + console.log('Got data', data, ref); // eslint-disable-line no-console + if (!!data && Object.keys(data).length) { + Object.keys(userFieldsStepMap).forEach(function getDefaults(key) { + methods.setValue(key, data[key]); + }); + console.log('Loaded data into defaults'); // eslint-disable-line no-console + } + } catch (err) { + // We only try reloading if insufficient permissions. + if (err.code !== 'permission-denied') { + console.log('permission denied'); // eslint-disable-line no-console + throw err; + } + window.setTimeout(() => { + setRetries(retries + 1); + }, 1000); + } + } + setLoadingState(false); + } + getData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [retries]); + + const handleNext = () => { + console.log('Data from this form (Next)', { ...methods.getValues() }); // eslint-disable-line no-console + setActiveStep(activeStep + 1); + }; + + const handleBack = () => { + console.log('Data from this form (Back)', { ...methods.getValues() }); // eslint-disable-line no-console + setActiveStep(activeStep - 1); + }; + + /** + * Sets the value for the given field if it isn't already populated. + * @param {string} fieldName name of the field which will be set + * @param {string} allFieldValues current value of all fields + * @param {object} googleResultComponent address component returned by the Google Maps API + * @param {object} addressType type of address component to lookup + * @param {string} addressTypeFormat short_name or long_name + */ + function setAddress( + fieldName, + googleResultComponent, + addressType, + addressTypeFormat = 'long_name', + ) { + if (googleResultComponent.types.includes(addressType)) { + methods.setValue(fieldName, googleResultComponent[addressTypeFormat]); + } + } + + function handleSetUserLocationInfo(locationInfo) { + setUserLocationInfo(locationInfo); + const result = locationInfo.lookedUpAddress; + if (result) { + let streetNumber = ''; + let streetAddress = ''; + result.address_components.forEach((component) => { + setAddress('city', component, 'locality'); + setAddress( + 'state', + + component, + 'administrative_area_level_1', + 'short_name', + ); + setAddress('zipcode', component, 'postal_code'); + if (component.types.includes('street_number')) { + streetNumber = component.short_name; + } + if (component.types.includes('route')) { + streetAddress = component.short_name; + } + }); + if (streetNumber && streetAddress) { + methods.setValue('address1', `${streetNumber} ${streetAddress}`); + } + } + } + + // const handleLocationChange = (location) => { + // setUserLocation(location); + // }; + + async function handleFormSubmit(values) { + const newValues = values; + let uid = null; + let newUser = false; + console.log('In submit', values, userData, userRef); // eslint-disable-line no-console + + // New user created with email/password + if (!isLoggedIn(user)) { + try { + // Try creating new user + const authState = await auth.createUserWithEmailAndPassword( + methods.getValues('email'), + methods.getValues('password'), + ); + newUser = true; + uid = authState.user.uid; + } catch (err) { + showError(err.message); + if (err.code === 'auth/weak-password') { + setActiveStep(1); + } else { + setActiveStep(0); + } + return; + } + } else { + uid = user.uid; + } + // Get the user's precise location + try { + const address = `${values.address1} ${values.address2} ${values.city}, ${values.state} ${values.zipcode}`; + const locationResult = await geocode(address); + console.log(address, 'Geocode result', locationResult); // eslint-disable-line no-console + const latitude = locationResult.results[0].geometry.location.lat(); + const longitude = locationResult.results[0].geometry.location.lng(); + newValues.preciseLocation = { latitude, longitude }; + newValues.generalLocation = { latitude, longitude }; + let city = null; + let state = null; + for ( + let i = 0; + i < locationResult.results[0].address_components.length && + (!city || !state); + i += 1 + ) { + for ( + let b = 0; + b < locationResult.results[0].address_components[i].types.length && + (!city || !state); + b += 1 + ) { + if ( + locationResult.results[0].address_components[i].types[b] === + 'administrative_area_level_1' + ) { + state = locationResult.results[0].address_components[i].short_name; + } + if ( + locationResult.results[0].address_components[i].types[b] === + 'locality' + ) { + city = locationResult.results[0].address_components[i].long_name; + } + } + } + + newValues.generalLocationName = `${city}, ${state}`; + console.log('After address lookup', newValues); // eslint-disable-line no-console + } catch (err) { + if (err === 'ZERO_RESULTS') { + showError('Could not find your address. Please try again'); + } else { + console.log('Address Error', err); // eslint-disable-line no-console + showError('Address Lookup Error', err); + } + setActiveStep(3); + return; + } + try { + // Get email if we came in through Google. Wouldn't be in the form + if (isGoogleLoggedIn(user)) { + console.log(user, user.email); // eslint-disable-line no-console + newValues.email = user.email; + } else if (isLoggedIn(user)) { + // Check for email/password changes + if (newValues.email !== user.email) { + await user.updateEmail(newValues.email); + } + if (newValues.password) { + await user.updatePassword(newValues.password); + } + } + // Write USERS profile + const userSnap = await firestore.doc(`${USERS_COLLECTION}/${uid}`).get(); + newValues.displayName = `${values.firstName} ${values.lastName}`; + let newProfile = { + firstName: newValues.firstName, + lastName: newValues.lastName, + email: newValues.email, + displayName: newValues.displayName, + preciseLocation: newValues.preciseLocation, + generalLocation: newValues.generalLocation, + generalLocationName: newValues.generalLocationName, + }; + await userSnap.ref.set(newProfile, { merge: true }); + // Write USERS_PRIVILEGED profile + newProfile = { ...newValues }; + const userPrivSnap = await firestore + .doc(`${USERS_PRIVILEGED_COLLECTION}/${uid}`) + .get(); + console.log('Updating with', newProfile); // eslint-disable-line no-console + await userPrivSnap.ref.set(newProfile, { merge: true }); + // Write USERS_PUBLIC profile + newProfile = { d: { hasProfile: true } }; + const userPubSnap = await firestore + .doc(`${USERS_PUBLIC_COLLECTION}/${uid}`) + .get(); + console.log('Updating with', newProfile); // eslint-disable-line no-console + await userPubSnap.ref.set(newProfile, { merge: true }); + console.log('Updated'); // eslint-disable-line no-console + showMessage(newUser ? 'Account created' : 'Profile updated'); + history.replace(SEARCH_PATH); + } catch (err) { + showError(err.message); + setActiveStep(0); + // TODO: If a new user, remove their Firebase account if profile didn't save? + } + } + + function renderFields(step) { + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + Your Location + + + A rough location is needed to allow us to efficiently and quickly + find a match. Enter address in the address search, click the + "Detect Location" button, or click on the map. You can + also enter your full address in the fields at the bottom.{' '} + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); + } + + return ( + +
+ +
+
+ + + {hasProfile(userData) ? 'My Profile' : ' I Want To Help'} + + + + {hasProfile(userData) ? 'My Profile' : ' I Want To Help'} + + + +
+ + + {steps.map((label) => { + const stepProps = {}; + const labelProps = {}; + return ( + + {label} + + ); + })} + + + + {renderFields(activeStep)} +
+ + + +
+ + + Note: This website and all related work products are provided + "AS IS". The provider of this service makes no other + warranties, express or implied, and hereby disclaims all + implied warranties, including any warranty of merchantability + and warranty of fitness for a particular purpose. + +
+
+
+
+
+
+ ); +} + +export default UserProfile; diff --git a/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.styles.js b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.styles.js new file mode 100644 index 0000000..5a8f1ff --- /dev/null +++ b/src/routes/UserProfile/components/UserProfilePage/UserProfilePage.styles.js @@ -0,0 +1,41 @@ +export default (theme) => ({ + root: { + ...theme.flexColumnCenter, + justifyContent: 'flex-start', + flexGrow: 1, + height: '100%', + width: '100%', + margin: '.2rem', + }, + centerDiv: { + justifyContent: 'center', + display: 'flex', + alignItems: 'center', + }, + buttons: { + display: 'flex', + justifyContent: 'flex-end', + paddingTop: theme.spacing(2), + paddingBottom: theme.spacing(3), + }, + optionalDivider: { + marginTop: theme.spacing(4), + marginBottom: theme.spacing(4), + }, + + errorText: { + color: 'red', + fontWeight: 'bold', + display: 'flex', + justifyContent: 'flex-end', + paddingTop: theme.spacing(2), + paddingBottom: theme.spacing(1), + }, + paper: { + paddingTop: theme.spacing(3), + paddingBottom: theme.spacing(1), + }, + warrantyInfo: { + marginTop: theme.spacing(3), + }, +}); diff --git a/src/routes/UserProfile/components/UserProfilePage/index.js b/src/routes/UserProfile/components/UserProfilePage/index.js new file mode 100644 index 0000000..e4bfdd3 --- /dev/null +++ b/src/routes/UserProfile/components/UserProfilePage/index.js @@ -0,0 +1,3 @@ +import UserProfilePage from './UserProfilePage'; + +export default UserProfilePage; diff --git a/src/routes/UserProfile/index.js b/src/routes/UserProfile/index.js new file mode 100644 index 0000000..62cb913 --- /dev/null +++ b/src/routes/UserProfile/index.js @@ -0,0 +1,11 @@ +import { loadable } from 'utils/router'; +import { USER_PROFILE_PATH as path } from 'constants/paths'; + +export default { + path, + component: loadable(() => + import( + /* webpackChunkName: 'User Profile' */ './components/UserProfilePage' + ), + ), +}; diff --git a/src/routes/index.js b/src/routes/index.js index 0b93218..b4f6c2b 100755 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -13,6 +13,8 @@ import AboutRoute from './About'; import DonateRoute from './Donate'; import AccountRoute from './Account'; import NewUserRoute from './NewUser'; +import UserProfileRoute from './UserProfile'; +import PasswordResetRoute from './PasswordReset'; import SearchRoute from './Search'; import NotFoundRoute from './NotFound'; import PrivacyPolicyRoute from './PrivacyPolicy'; @@ -39,6 +41,8 @@ export default function createRoutes() { ContactRoute, SearchRoute, NewUserRoute, + UserProfileRoute, + PasswordResetRoute, AboutRoute, LoginRoute, TermsOfServiceRoute, diff --git a/src/utils/geo.js b/src/utils/geo.js index 1181777..bf6c493 100644 --- a/src/utils/geo.js +++ b/src/utils/geo.js @@ -107,3 +107,23 @@ export async function reverseGeocode(latitude, longitude) { ); }); } + +export async function geocode(address) { + if (!window.google?.maps?.Geocoder) { + // eslint-disable-next-line no-console + console.error('Google Maps API not found.'); + Sentry.captureMessage('Google Maps API not found', Sentry.Severity.Error); + return null; + } + + return new Promise((resolve, reject) => { + const geocoder = new window.google.maps.Geocoder(); + geocoder.geocode({ address }, (results, status) => { + if (status !== window.google.maps.GeocoderStatus.OK) { + reject(status); + } else { + resolve({ status, results }); + } + }); + }); +}