Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@W-15621253 Initial Store Locator Implementation #1827

Merged
merged 26 commits into from
Jun 21, 2024

Conversation

jeremy-jung1
Copy link
Collaborator

@jeremy-jung1 jeremy-jung1 commented Jun 12, 2024

Description

W-15621253

This is the initial Store Locator implementation, which is without pagination and linking of geolocation web API. The initial implementation includes:

  1. the handling of manual geolocation input
  2. default country and postal code and user specified store locator countries
  3. the listing of corresponding stores sorted by distance away
  4. mobile view
  5. a store locator page, on top of the modal, that can be navigated to via URL for SEO

The PR will merge into a store locator base branch, to which subsequent PRs will be merged until the store locator is ready for shipping. Subsequent PRs will link the geolocation web API to the store locator and enable pagination for the store locator.

Types of Changes

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Documentation update
  • Breaking change (could cause existing functionality to not work as expected)
  • Other changes (non-breaking changes that does not fit any of the above)

Breaking changes include:

  • Removing a public function or component or prop
  • Adding a required argument to a function
  • Changing the data type of a function parameter or return value
  • Adding a new peer dependency to package.json

Changes

  • (change1) Implemented initial store locator page and modal
  • (change2) Linked the existing store locator link in the footer to the page
  • (change3) Added store locator icon at the header

How to Test-Drive This PR

  • (step1) Ensure all tests pass with npm run test
  • (step 2) Go to https://runtime-admin-n16.mobify-storefront.com/ and click on the store icon on the top right of the page to open store locator modal
  • (step 3) See that the list of stores are sorted by distance away
  • (step 4) See that clicking View More opens an accordion to view store hours, if specified
  • (step 4) Change location to Country: United States, Postal Code: 94086, and press "Find" to see that new stores are now listed
  • (step 5) Close the modal, and open it again to see that the last submitted user input is saved.
  • (step 7) Go to https://runtime-admin-n16.mobify-storefront.com/store-locator for the page version of the store locator

Checklists

General

  • Changes are covered by test cases
  • CHANGELOG.md updated with a short description of changes (not required for documentation updates)

Accessibility Compliance

You must check off all items in one of the follow two lists:

  • There are no changes to UI

or...

Localization

  • Changes include a UI text update in the Retail React App (which requires translation)

@jeremy-jung1 jeremy-jung1 requested a review from a team as a code owner June 12, 2024 18:50
@jeremy-jung1 jeremy-jung1 changed the base branch from develop to store-locator-base-branch June 12, 2024 18:50
@jeremy-jung1 jeremy-jung1 changed the title @W-15621253 Store Locator @W-15621253 Initial Store Locator Page Implementation Jun 12, 2024
<AccordionPanel mb={6} mt={4}>
<div
dangerouslySetInnerHTML={{
__html: store?.storeHours
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Business manager allows customers to use HTML to set the store hours


return (
<>
{isDesktopView ? (
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a breakpoint value boolean instead of the <HideFrom...> components because the latter wasn't super compatible for chakra UI modals:

  1. Modal overrides some aspect of display, requiring inline styling anyways
  2. display "none" applied to modal still rendered in the DOM

@jeremy-jung1 jeremy-jung1 changed the title @W-15621253 Initial Store Locator Page Implementation @W-15621253 Initial Store Locator Implementation Jun 12, 2024
"defaultMessage": "Finden"
},
"store_locator.action.use_my_location": {
"defaultMessage": "Verwenden Sie „Mein Standort“."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typically we don't need to provide the translations ourselves. We have a team of translators that can do that.

What we're responsible for is to mark up which strings are to be translated. And then later we hand the en-GB.json file over to the translators. They will then give us de-DE.json and other non-English locales.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah thank you. I'll remove the translations so it doesn't get lost in track for professional translation

<link rel="manifest" href={getAssetUrl('static/manifest.json')} />

{/* Urls for all localized versions of this page (including current page)
<>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: it looks like we don't need this anymore. Let's remove it, so that the resulting code changes is smaller → makes it harder to review and see where the real changes are.

@@ -106,7 +106,7 @@ const Footer = ({...otherProps}) => {
})}
links={[
{
href: '/',
href: '/store-locator',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah this is where the link to the page version 👍

Comment on lines 357 to 360
<StoreLocator
isOpen={storeLocatorIsOpen}
setIsOpen={setStoreLocatorIsOpen}
/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since StoreLocator is actually a modal, I think it's better to rename it to StoreLocatorModal.

Comment on lines 48 to 53
useEffect(() => {
setSearchStoresParams({
countryCode: defaultCountryCode,
postalCode: DEFAULT_STORE_LOCATORY_POSTAL_CODE
})
}, [])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typically useEffect is for something asynchronous. In this case, we can find the stores params immediately. So I'd replace it and call useState with initial value like this:

Suggested change
useEffect(() => {
setSearchStoresParams({
countryCode: defaultCountryCode,
postalCode: DEFAULT_STORE_LOCATORY_POSTAL_CODE
})
}, [])
const [searchStoresParams, setSearchStoresParams] = useState({
countryCode: defaultCountryCode,
postalCode: DEFAULT_STORE_LOCATORY_POSTAL_CODE
})

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vmarta
Copy link
Contributor

vmarta commented Jun 13, 2024

@jeremy-jung1 FYI I've left some comments. But I see that you've just made new code changes. I'll review them later at another time.

jeremy-jung1 and others added 5 commits June 13, 2024 12:33
@jeremy-jung1
Copy link
Collaborator Author

Before deleting the translations in this PR for professional translation, I took a screenshot of German locale store locator to show that the store locator is translatable.
Screenshot 2024-06-13 at 12 44 12 PM

>
<ModalCloseButton
onClick={() => {
setIsOpen(false)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe there is a current pattern of calling this prop "onClose". Please check the _app component for the DrawerMenu usage.

<ModalBody pb={8} bg="white" paddingBottom={6} marginTop={6}>
<StoreLocatorContent
searchStoresData={searchStoresData}
searchStoresParams={searchStoresParams}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are getting close here. The pattern you've implemented with searchStoresParams and setSearchStoresParams seems like a re-implementation of the pattern established by react-hook-form's useForm() hook.

We used this pattern of exposing the "onSumbit" handler for the form, which I feel is a lot like what you did with "setSearchSotresParams".

Take a look at these doc's here --> https://www.react-hook-form.com/api/useform/

See if you can take a look at exposing the onSubmit in your form component, you might want to look at the defaultValues or values to preserver state for your form.

Copy link
Collaborator Author

@jeremy-jung1 jeremy-jung1 Jun 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for using states here is to be able to call the useSearchStores hook when the user submits an input somewhere in the child component. Rules with hooks prevent this from being called within an event listener, so states were used to re-render the parent component to retrieve the list of stores. Unlike other components in the repo that use the onSubmit which can pull a function out of the hook because it does an async mutation within the listener, the useSearchStores is a hook to be used by itself.

I also do use the useForm in here, and I can look to pull it out to the parent component so that the "form" gets passed in to use a form component's submit handler, but I'm not sure if replacing aforementioned use case of the searchStoresParams state entirely would work

Comment on lines 160 to 163
export const DEFAULT_STORE_LOCATORY_COUNTRY = defineMessage({
defaultMessage: 'Germany',
id: 'store_locator.dropdown.germany'
})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the getDefaultSearchStoresParams implementation I'm wondering if having a constant for the country name is making thing mor complex than it has to be. We should probably using the countryCode as the default.

This code here could be problematic as we are comparing name values that aren't as close to unique identifiers as the country codes are.

var defaultCountryCode = formattedStoreLocatorCountries.find(
        (obj) => obj.countryName == intl.formatMessage(DEFAULT_STORE_LOCATORY_COUNTRY)
    ).countryCode

Comment on lines 23 to 24
var storesInfo = (storesData?.data?.data || []).sort((a, b) => a.distance - b.distance)
return storesInfo?.map((store, index) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a big deal, but if you ensure storesInfo is always an array, then you won't need guards here.

You might also consider using const rather than var.

Copy link
Collaborator Author

@jeremy-jung1 jeremy-jung1 Jun 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks I changed it to a const. It seems validation for storesInfo being undefined is necessary while the hook is asynchronously loading information. I also added a message in the UI to show that stores are being loaded when the user makes a form submission.

Comment on lines 48 to 55
<Controller
name="postalCode"
control={control}
defaultValue={searchStoresParams?.postalCode}
render={({field}) => {
return <Input {...field} marginBottom="10px" />
}}
></Controller>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you consider using a placeholder attribute here to make it clear that this value is intended to be Zip code?

If you remove the default text it isn't entirely clear:

Screenshot 2024-06-13 at 3 23 00 PM

import {Controller, useForm} from 'react-hook-form'
import {SUPPORTED_STORE_LOCATOR_COUNTRIES} from '@salesforce/retail-react-app/app/constants'

const StoreLocatorContent = (props) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you consider extracting the Store Locator Form (eg. All of the inputs) into a seperate component so it could be more easily overridden?

setIsOpen: PropTypes.func.isRequired
}

export default StoreLocatorModal
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also rename the folder to "components/store-locator-modal/" to be consistent.

const intl = useIntl()
var defaultSearchStoresParams = getDefaultSearchStoresParams(intl)

const [searchStoresParams, setSearchStoresParams] = useState(defaultSearchStoresParams)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see.. you're no longer using the localStorage because this state will already save the last search params for you 👍

And this state persists throughout the app's lifecycle because StoreLocatorModal stays mounted.

}

const StoreLocatorModal = (props) => {
const {isOpen, setIsOpen} = props
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than implementing this logic ourselves, let's use the one provided by Chakra: useDisclosure

Suggested change
const {isOpen, setIsOpen} = props
const {isOpen, onOpen, onClose} = useDisclosure()

So we can convert the existing:

  • setIsOpen(true) to onOpen()
  • setIsOpen(false) to onClose()

@@ -125,6 +125,7 @@ const App = (props) => {
const {site, locale, buildUrl} = useMultiSite()

const [isOnline, setIsOnline] = useState(true)
const [storeLocatorIsOpen, setStoreLocatorIsOpen] = useState(false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly here, let's use useDisclosure too.

id: 'store_locator.dropdown.germany'
})
}
]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a project wanted to add or change the list of supported stores, what would they need to do? Would they modify this const?

I am wondering if it might be better to have this in a stores.js config file (similar to how we have a sites.js for defining the sites)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, wait. The list of supported stores is probably defined in BM for each site, no? So a project would be setting these there?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes the stores are specified by the customer in BM and are retrieved in our app via a hook of the API endpoint searchStores

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jeremy-jung1. So this const and the ones below it are temporary and will be replaced with a hook to call searchStores in a subsequent PR?

If so, could you leave a comment saying that the constants are temporary?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vcua-mobify Ah no this const on the other hand is the list of countries that the customer can configure the store locator to query. It is meant to be a permanent configuration option. On top of that, the configurations below the list of countries specify default parameters for the initial query to be done server side.

I had a discussion with @vmarta and @bendvc about whether or not these configurations should go in the sites.js, and the constants.js file was deemed most appropriate due to it being the current pattern of putting new configurations and also in anticipation for a future refactor of the template-retail-react-app, in which it would be easier if at many configurations as possible came from the same file.

@vcua-mobify
Copy link
Contributor

Not a part of this ticket, but I would be interested in seeing how we handle saving the shopper's selected store in the future since it might have some implication on hybrid (ie. how do we share the selected store from PWA to SFRA?).

Could you loop me in once we get to that part of the epic?

Comment on lines 22 to 23
return storesInfo !== undefined ? (
storesInfo.map((store, index) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: In React 18 (which we use now), it's ok to return undefined to render nothing. So we can simplify the code like this:

return storesInfo?.map(() => {...})

Comment on lines 52 to 57
const storesInfo =
searchStoresData.data !== undefined
? searchStoresData.data.data !== undefined
? searchStoresData.data.data
: []
: undefined
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can make it clearer why there are 3 possible scenarios. Since react-query gives us a way to find out whether it's still loading/fetching the data, we can do something like this:

    var {data: searchStoresData, isLoading} = useSearchStores({parameters})

    const storesInfo = isLoading ? undefined : searchStoresData.data || []

@@ -125,9 +125,15 @@ const App = (props) => {
const {site, locale, buildUrl} = useMultiSite()

const [isOnline, setIsOnline] = useState(true)
const [storeLocatorIsOpen, setStoreLocatorIsOpen] = useState(false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this line, as it's no longer used.

Comment on lines 44 to 46
var storesInfo = []
if (searchStoresData.data !== undefined && searchStoresData.data.data !== undefined)
storesInfo = searchStoresData.data.data
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can follow what's done in the other file: https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1827/files#r1646765524

submitForm={submitForm}
storesInfo={storesInfo}
searchStoresParams={searchStoresParams}
distanceLocate={STORE_LOCATOR_DISTANCE}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StoreLocatorContent seems to no longer accept distanceLocate prop.

Copy link
Contributor

@vmarta vmarta left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking forward to seeing future PRs for the store-locator-base-branch.

@jeremy-jung1 jeremy-jung1 merged commit ac3adbf into store-locator-base-branch Jun 21, 2024
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants