Skip to content

Commit

Permalink
GEN-751 | Add Retargeting CMS block (#2763)
Browse files Browse the repository at this point in the history
<!--
PR title: GRW-123 / Feature / Awesome new thing
-->

## Describe your changes


![Screenshot 2023-07-14 at 10.13.43.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/OgXegTwxM9IeuXHZyw5h/b2fc0087-821d-4cca-9819-973c28b6f906/Screenshot%202023-07-14%20at%2010.13.43.png)


- Implement CMS block for listing retargeting offers

- Refactor `/session/resume/:id` page

  - Fetch predefined Story from CMS
  - Use ShopSession from URL params

- Collect all retargeting code inside `retargeting` feature folder

<!--
What changes are made?
If there are many changes, a list might be a good format.
If it makes sense, add screenshots and/or screen recordings here.
-->

## Justify why they are needed

- With a CMS block - editors can customize the rest of the page just like they want it

- I tried to preload all the offers (price intents) and pass to Apollo cache but that made the initial payload so big that Next.js warned me about it hurting performance. So I decided to fetch the offers client side instead and implement a simple loading state.

## Checklist before requesting a review

- [ ] I have performed a self-review of my code
  • Loading branch information
robinandeer authored Jul 17, 2023
1 parent 99f4546 commit af38513
Show file tree
Hide file tree
Showing 13 changed files with 213 additions and 129 deletions.
16 changes: 16 additions & 0 deletions apps/store/src/components/CartItem/CartItemSkeleton.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import styled from '@emotion/styled'
import { keyframes } from 'tss-react'
import { theme } from 'ui'

const pulsingAnimation = keyframes({
'0%': { opacity: 0.5 },
'50%': { opacity: 1 },
'100%': { opacity: 0.5 },
})

export const CartItemSkeleton = styled.div({
backgroundColor: theme.colors.grayTranslucent100,
borderRadius: theme.radius.sm,
height: '13.5rem',
animation: `${pulsingAnimation} 1.5s ease-in-out infinite`,
})
96 changes: 0 additions & 96 deletions apps/store/src/components/RetargetingPage/RetargetingPage.tsx

This file was deleted.

67 changes: 67 additions & 0 deletions apps/store/src/features/retargeting/RetargetingBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import styled from '@emotion/styled'
import { storyblokEditable } from '@storyblok/react'
import { motion, AnimatePresence } from 'framer-motion'
import { mq, theme } from 'ui'
import { CartItemSkeleton } from '@/components/CartItem/CartItemSkeleton'
import { GridLayout } from '@/components/GridLayout/GridLayout'
import { MENU_BAR_HEIGHT_DESKTOP, MENU_BAR_HEIGHT_MOBILE } from '@/components/Header/HeaderStyles'
import { type GridColumnsField, type SbBaseBlockProps } from '@/services/storyblok/storyblok'
import { MultiTierOffer } from './MultiTierOffer'
import { SingleTierOffer } from './SingleTierOffer'
import { useRetargetingOffers } from './useRetargetingOffers'

const RETARGETING_BLOCK_NAME = 'retargeting'

type Props = SbBaseBlockProps<{
layout?: GridColumnsField
}>

export const RetargetingBlock = (props: Props) => {
const offers = useRetargetingOffers()

return (
<GridLayout.Root {...storyblokEditable(props.blok)}>
<GridLayoutContent
width={props.blok.layout?.widths ?? '1'}
align={props.blok.layout?.alignment ?? 'center'}
>
<List>
{offers === null ? (
Array.from({ length: 3 }).map((_, index) => <CartItemSkeleton key={index} />)
) : (
<AnimatePresence mode="popLayout">
{offers.map((item) => (
<motion.li
key={item.key}
layout={true}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.8, opacity: 0 }}
>
{item.type === 'single' && <SingleTierOffer {...item} />}
{item.type === 'multiple' && <MultiTierOffer {...item} />}
</motion.li>
))}
</AnimatePresence>
)}
</List>
</GridLayoutContent>
</GridLayout.Root>
)
}

RetargetingBlock.blockName = RETARGETING_BLOCK_NAME

const GridLayoutContent = styled(GridLayout.Content)({
paddingBlock: theme.space.lg,
minHeight: `calc(100vh - ${MENU_BAR_HEIGHT_MOBILE})`,

[mq.lg]: {
minHeight: `calc(100vh - ${MENU_BAR_HEIGHT_DESKTOP})`,
},
})

const List = styled.ul({
display: 'flex',
flexDirection: 'column',
gap: theme.space.md,
})
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import styled from '@emotion/styled'
import { useTranslation } from 'next-i18next'
import { Button, theme } from 'ui'
import { CartItem } from '@/components/CartItem/CartItem'
import { useHandleSubmitAddToCart } from '@/components/ProductPage/PurchaseForm/useHandleSubmitAddToCart'
import { type ProductOfferFragment } from '@/services/apollo/generated'
import { useHandleSubmitAddToCart } from '../ProductPage/PurchaseForm/useHandleSubmitAddToCart'
import { ProductPageLink } from './ProductPageLink'

type Props = {
Expand Down
3 changes: 3 additions & 0 deletions apps/store/src/features/retargeting/retargetingBlocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { RetargetingBlock } from './RetargetingBlock'

export const retargetingBlocks = [RetargetingBlock] as const
52 changes: 52 additions & 0 deletions apps/store/src/features/retargeting/useRetargetingOffers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useMemo, type ComponentProps } from 'react'
import { usePriceIntentsQuery } from '@/services/apollo/generated'
import { useShopSession } from '@/services/shopSession/ShopSessionContext'
import { MultiTierOffer } from './MultiTierOffer'
import { SingleTierOffer } from './SingleTierOffer'

type OfferSingle = ComponentProps<typeof SingleTierOffer> & { type: 'single' }
type OfferMultiple = ComponentProps<typeof MultiTierOffer> & { type: 'multiple' }
type Offer = (OfferSingle | OfferMultiple) & { key: string }

export const useRetargetingOffers = (): Array<Offer> | null => {
const { shopSession } = useShopSession()

const result = usePriceIntentsQuery({
skip: !shopSession,
variables: shopSession ? { shopSessionId: shopSession.id } : undefined,
})

const offers = useMemo(() => {
if (!shopSession || !result.data) return null

const cartOfferIds = new Set(result.data.shopSession.cart.entries.map((item) => item.id))

return result.data.shopSession.priceIntents.reduce<Array<Offer>>((total, item) => {
if (item.offers.some((offer) => cartOfferIds.has(offer.id))) {
return total
}

if (item.offers.length === 1) {
total.push({
key: item.id,
type: 'single',
product: item.offers[0].variant.product,
offer: item.offers[0],
shopSessionId: shopSession.id,
})
} else if (item.offers.length > 1) {
total.push({
key: item.id,
type: 'multiple',
product: item.offers[0].variant.product,
defaultOffer: item.offers[0],
offers: item.offers,
})
}

return total
}, [])
}, [result.data, shopSession])

return offers
}
2 changes: 1 addition & 1 deletion apps/store/src/pages/[[...slug]].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const getStaticProps: GetStaticProps<PageProps, StoryblokQueryParams> = a

const timerName = `Get static props for ${locale}/${slug} ${draftMode ? '(draft)' : ''}`
console.time(timerName)
const version = draftMode ? 'draft' : 'published'
const version = draftMode ? 'draft' : undefined
const [layoutWithMenuProps, breadcrumbs, trustpilot] = await Promise.all([
getLayoutWithMenuProps(context, apolloClient),
fetchBreadcrumbs(slug, { version, locale }),
Expand Down
File renamed without changes.
71 changes: 71 additions & 0 deletions apps/store/src/pages/cart/resume/[shopSessionId].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { StoryblokComponent, useStoryblokState } from '@storyblok/react'
import { type GetServerSideProps, type NextPageWithLayout } from 'next'
import { HeadSeoInfo } from '@/components/HeadSeoInfo/HeadSeoInfo'
import { getLayoutWithMenuProps } from '@/components/LayoutWithMenu/getLayoutWithMenuProps'
import { LayoutWithMenu } from '@/components/LayoutWithMenu/LayoutWithMenu'
import { addApolloState, initializeApolloServerSide } from '@/services/apollo/client'
import { SHOP_SESSION_PROP_NAME } from '@/services/shopSession/ShopSession.constants'
import { setupShopSessionServiceServerSide } from '@/services/shopSession/ShopSession.helpers'
import { PageStory, getStoryBySlug } from '@/services/storyblok/storyblok'
import { isRoutingLocale } from '@/utils/l10n/localeUtils'

type Props = {
story: PageStory
}

type Params = {
shopSessionId: string
}

const NextPage: NextPageWithLayout<Props> = (props) => {
const story = useStoryblokState(props.story)
if (!story) return null

return (
<>
<HeadSeoInfo story={story} />
<StoryblokComponent blok={story.content} />
</>
)
}

export const getServerSideProps: GetServerSideProps<Props, Params> = async (context) => {
if (!context.params) throw new Error('No params in context')
if (!isRoutingLocale(context.locale)) return { notFound: true }
const shopSessionId = context.params.shopSessionId

const apolloClient = await initializeApolloServerSide({
req: context.req,
res: context.res,
locale: context.locale,
})

const shopSessionService = setupShopSessionServiceServerSide({
apolloClient,
req: context.req,
res: context.res,
})

shopSessionService.saveId(shopSessionId)

const slug = 'cart/resume'
const [story, layoutWithMenuProps] = await Promise.all([
getStoryBySlug<PageStory>(slug, {
locale: context.locale,
...(context.draftMode && { version: 'draft' }),
}),
getLayoutWithMenuProps(context, apolloClient),
])

return addApolloState(apolloClient, {
props: {
...layoutWithMenuProps,
story,
[SHOP_SESSION_PROP_NAME]: shopSessionId,
},
})
}

NextPage.getLayout = (children) => <LayoutWithMenu>{children}</LayoutWithMenu>

export default NextPage
31 changes: 0 additions & 31 deletions apps/store/src/pages/session/resume/[shopSessionId].tsx

This file was deleted.

2 changes: 2 additions & 0 deletions apps/store/src/services/storyblok/storyblok.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import { blogBlocks } from '@/features/blog/blogBlocks'
// TODO: get rid of this import, services should avoid feature-imports
import { STORYBLOK_MANYPETS_FOLDER_SLUG } from '@/features/manyPets/manyPets.constants'
import { manyPetsBlocks } from '@/features/manyPets/manyPetsBlocks'
import { retargetingBlocks } from '@/features/retargeting/retargetingBlocks'
import { TrustpilotData } from '@/services/trustpilot/trustpilot.types'
import { isBrowser } from '@/utils/env'
import { Features } from '@/utils/Features'
Expand Down Expand Up @@ -293,6 +294,7 @@ export const initStoryblok = () => {
ComparisonTableBlock,
...blogBlocks,
...manyPetsBlocks,
...retargetingBlocks,
]
const blockAliases = { reusableBlock: PageBlock }
const components = {
Expand Down

1 comment on commit af38513

@vercel
Copy link

@vercel vercel bot commented on af38513 Jul 17, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.