-
Notifications
You must be signed in to change notification settings - Fork 124
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
Conversation
<AccordionPanel mb={6} mt={4}> | ||
<div | ||
dangerouslySetInnerHTML={{ | ||
__html: store?.storeHours |
There was a problem hiding this comment.
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 ? ( |
There was a problem hiding this comment.
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:
- Modal overrides some aspect of display, requiring inline styling anyways
- display "none" applied to modal still rendered in the DOM
"defaultMessage": "Finden" | ||
}, | ||
"store_locator.action.use_my_location": { | ||
"defaultMessage": "Verwenden Sie „Mein Standort“." |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) | ||
<> |
There was a problem hiding this comment.
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', |
There was a problem hiding this comment.
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 👍
<StoreLocator | ||
isOpen={storeLocatorIsOpen} | ||
setIsOpen={setStoreLocatorIsOpen} | ||
/> |
There was a problem hiding this comment.
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
.
useEffect(() => { | ||
setSearchStoresParams({ | ||
countryCode: defaultCountryCode, | ||
postalCode: DEFAULT_STORE_LOCATORY_POSTAL_CODE | ||
}) | ||
}, []) |
There was a problem hiding this comment.
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:
useEffect(() => { | |
setSearchStoresParams({ | |
countryCode: defaultCountryCode, | |
postalCode: DEFAULT_STORE_LOCATORY_POSTAL_CODE | |
}) | |
}, []) | |
const [searchStoresParams, setSearchStoresParams] = useState({ | |
countryCode: defaultCountryCode, | |
postalCode: DEFAULT_STORE_LOCATORY_POSTAL_CODE | |
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
packages/template-retail-react-app/app/components/store-locator/store-locator-content.jsx
Outdated
Show resolved
Hide resolved
packages/template-retail-react-app/app/components/store-locator/stores-list.jsx
Outdated
Show resolved
Hide resolved
@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. |
Signed-off-by: jeremy-jung1 <140001271+jeremy-jung1@users.noreply.github.com>
> | ||
<ModalCloseButton | ||
onClick={() => { | ||
setIsOpen(false) |
There was a problem hiding this comment.
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} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
export const DEFAULT_STORE_LOCATORY_COUNTRY = defineMessage({ | ||
defaultMessage: 'Germany', | ||
id: 'store_locator.dropdown.germany' | ||
}) |
There was a problem hiding this comment.
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
var storesInfo = (storesData?.data?.data || []).sort((a, b) => a.distance - b.distance) | ||
return storesInfo?.map((store, index) => { |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
<Controller | ||
name="postalCode" | ||
control={control} | ||
defaultValue={searchStoresParams?.postalCode} | ||
render={({field}) => { | ||
return <Input {...field} marginBottom="10px" /> | ||
}} | ||
></Controller> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import {Controller, useForm} from 'react-hook-form' | ||
import {SUPPORTED_STORE_LOCATOR_COUNTRIES} from '@salesforce/retail-react-app/app/constants' | ||
|
||
const StoreLocatorContent = (props) => { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
packages/template-retail-react-app/app/pages/store-locator/index.jsx
Outdated
Show resolved
Hide resolved
} | ||
|
||
const StoreLocatorModal = (props) => { | ||
const {isOpen, setIsOpen} = props |
There was a problem hiding this comment.
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
const {isOpen, setIsOpen} = props | |
const {isOpen, onOpen, onClose} = useDisclosure() |
So we can convert the existing:
setIsOpen(true)
toonOpen()
setIsOpen(false)
toonClose()
@@ -125,6 +125,7 @@ const App = (props) => { | |||
const {site, locale, buildUrl} = useMultiSite() | |||
|
|||
const [isOnline, setIsOnline] = useState(true) | |||
const [storeLocatorIsOpen, setStoreLocatorIsOpen] = useState(false) |
There was a problem hiding this comment.
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' | ||
}) | ||
} | ||
] |
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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? |
return storesInfo !== undefined ? ( | ||
storesInfo.map((store, index) => { |
There was a problem hiding this comment.
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(() => {...})
const storesInfo = | ||
searchStoresData.data !== undefined | ||
? searchStoresData.data.data !== undefined | ||
? searchStoresData.data.data | ||
: [] | ||
: undefined |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
var storesInfo = [] | ||
if (searchStoresData.data !== undefined && searchStoresData.data.data !== undefined) | ||
storesInfo = searchStoresData.data.data |
There was a problem hiding this comment.
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} |
There was a problem hiding this comment.
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.
There was a problem hiding this 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
.
Description
W-15621253
This is the initial Store Locator implementation, which is without pagination and linking of geolocation web API. The initial implementation includes:
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
Changes
How to Test-Drive This PR
npm run test
Checklists
General
Accessibility Compliance
You must check off all items in one of the follow two lists:
or...
Localization