Skip to content

Commit

Permalink
(PC-33639) feat(chronicle): display chronicles data from backend (#7558)
Browse files Browse the repository at this point in the history
* (PC-33639) feat(api): regenerate file

* (PC-33639) test(offer): fix fixtures and tests with chronicles

* (PC-33649) feat(chronicle): use prop for card width

* (PC-33639) feat(chronicle): get chronicles data from backend

* (PC-33639) feat(chronicle): display chronicles data from backend

* (PC-33639) fix(chronicle): go back on web

* (PC-33639) fix(storybook): ChronicleCard story

* (PC-33639) refactor(chronicle): remarks (move some file in shared folder + add adapters folder)
  • Loading branch information
clesausse-pass authored Jan 21, 2025
1 parent 33ac075 commit a3c4293
Show file tree
Hide file tree
Showing 26 changed files with 1,127 additions and 753 deletions.
1,581 changes: 869 additions & 712 deletions src/api/gen/api.ts

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/features/bookOffer/fixtures/offer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export const mockOffer = toMutable({
reactionsCount: {
likes: 0,
},
chronicles: [],
} as const satisfies ReadonlyDeep<OfferResponseV2>)

export const mockDigitalOffer = toMutable({
Expand Down Expand Up @@ -223,4 +224,5 @@ export const mockDigitalOffer = toMutable({
reactionsCount: {
likes: 0,
},
chronicles: [],
} as const satisfies ReadonlyDeep<OfferResponseV2>)
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { offerChroniclesToChronicleCardData } from 'features/chronicle/adapters/offerChroniclesToChronicleCardData/offerChroniclesToChronicleCardData'
import { chroniclesFixture } from 'features/chronicle/fixtures/offerChronicles.fixture'

describe('transformOfferChroniclesToChronicleCardData', () => {
it('should transform offer chronicles to chronicle card data', () => {
const chronicles = [...chroniclesFixture]
const result = offerChroniclesToChronicleCardData(chronicles)

expect(result).toEqual([
{
date: 'Janvier 2025',
description: 'Chronique sur le produit Product 30 mais sans utilisateur.',
id: 31,
subtitle: '',
title: 'Membre du Book Club',
},
{
date: 'Janvier 2025',
description:
'Chronique sur le produit Product 30 écrite par l’utilisateur Jeanne Doux (2).',
id: 1,
subtitle: 'Membre du Book Club',
title: 'Jeanne, 15 ans',
},
])
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { OfferChronicle } from 'api/gen'
import { ChronicleCardData } from 'features/chronicle/type'
import { getChronicleCardTitle } from 'shared/chronicle/getChronicleCardTitle/getChronicleCardTitle'
import { getFormattedLongMonthYear } from 'shared/date/getFormattedLongMonthYear/getFormattedLongMonthYear'

export function offerChroniclesToChronicleCardData(
chronicles: OfferChronicle[]
): ChronicleCardData[] {
return chronicles.map(({ id, author, content, dateCreated }) => ({
id,
title: getChronicleCardTitle(author),
subtitle: author?.firstName ? 'Membre du Book Club' : '',
description: content,
date: getFormattedLongMonthYear(dateCreated),
}))
}
22 changes: 22 additions & 0 deletions src/features/chronicle/api/useChronicles/useChronicles.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useChronicles } from 'features/chronicle/api/useChronicles/useChronicles'
import { offerChroniclesFixture } from 'features/chronicle/fixtures/offerChronicles.fixture'
import { offerResponseSnap } from 'features/offer/fixtures/offerResponse'
import { mockServer } from 'tests/mswServer'
import { reactQueryProviderHOC } from 'tests/reactQueryProviderHOC'
import { act, renderHook } from 'tests/utils'

describe('useChronicles', () => {
beforeEach(() =>
mockServer.getApi(`/v1/offer/${offerResponseSnap.id}/chronicles`, offerChroniclesFixture)
)

it('should call API otherwise', async () => {
const { result } = renderHook(() => useChronicles({ offerId: offerResponseSnap.id }), {
wrapper: ({ children }) => reactQueryProviderHOC(children),
})

await act(async () => {})

expect(JSON.stringify(result.current.data)).toEqual(JSON.stringify(offerChroniclesFixture))
})
})
11 changes: 11 additions & 0 deletions src/features/chronicle/api/useChronicles/useChronicles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useQuery } from 'react-query'

import { api } from 'api/api'
import { OfferChronicles } from 'api/gen'
import { QueryKeys } from 'libs/queryKeys'

export const useChronicles = ({ offerId }: { offerId: number }) => {
return useQuery<OfferChronicles | undefined>([QueryKeys.OFFER_CHRONICLES, offerId], () =>
offerId ? api.getNativeV1OfferofferIdChronicles(offerId) : undefined
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const meta: ComponentMeta<typeof ChronicleCard> = {
export default meta

const baseProps = {
id: 1,
title: 'Olivier, 15 ans',
subtitle: 'Membre du book club',
description:
Expand Down
15 changes: 10 additions & 5 deletions src/features/chronicle/components/ChronicleCard/ChronicleCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { FunctionComponent } from 'react'
import styled from 'styled-components/native'

import { CHRONICLE_ITEM_WIDTH } from 'features/chronicle/constant'
import { ChronicleCardData } from 'features/chronicle/type'
import { InfoHeader } from 'ui/components/InfoHeader/InfoHeader'
import { Separator } from 'ui/components/Separator'
Expand All @@ -11,15 +10,20 @@ import { TypoDS, getShadow, getSpacing } from 'ui/theme'

const CHRONICLE_THUMBNAIL_SIZE = getSpacing(14)

export const ChronicleCard: FunctionComponent<ChronicleCardData> = ({
type Props = ChronicleCardData & {
cardWidth?: number
}

export const ChronicleCard: FunctionComponent<Props> = ({
id,
title,
subtitle,
description,
date,
cardWidth,
}) => {
return (
<Container gap={3} testID={`chronicle-${id.toString()}`}>
<Container gap={3} testID={`chronicle-${id.toString()}`} maxWidth={cardWidth}>
<InfoHeader
title={title}
subtitle={subtitle}
Expand All @@ -33,12 +37,13 @@ export const ChronicleCard: FunctionComponent<ChronicleCardData> = ({
)
}

const Container = styled(ViewGap)(({ theme }) => ({
const Container = styled(ViewGap)<{ maxWidth?: number }>(({ theme, maxWidth }) => ({
padding: getSpacing(6),
borderRadius: getSpacing(2),
border: 1,
borderColor: theme.colors.greyMedium,
maxWidth: CHRONICLE_ITEM_WIDTH,
maxWidth,

backgroundColor: theme.colors.white,
...getShadow({
shadowOffset: { width: 0, height: getSpacing(1) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import { PlaylistArrowButton } from 'ui/Playlist/PlaylistArrowButton'
type ChronicleCardListProps = {
data: ChronicleCardData[]
horizontal?: boolean
cardWidth?: number
}

export const ChronicleCardList: FunctionComponent<ChronicleCardListProps> = ({
data,
horizontal = true,
cardWidth,
}) => {
const [indexItem, setIndexItem] = useState(0)

Expand Down Expand Up @@ -40,7 +42,12 @@ export const ChronicleCardList: FunctionComponent<ChronicleCardListProps> = ({
) : null}
</React.Fragment>
) : null}
<ChronicleCardListBase data={data} indexItem={indexItem} horizontal={horizontal} />
<ChronicleCardListBase
data={data}
indexItem={indexItem}
horizontal={horizontal}
cardWidth={cardWidth}
/>
</FlatListContainer>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,18 @@ type ChronicleCardListProps = {
data: ChronicleCardData[]
indexItem?: number
horizontal?: boolean
cardWidth?: number
}

const renderItem = ({ item }: { item: ChronicleCardData }) => {
const renderItem = ({ item, cardWidth }: { item: ChronicleCardData; cardWidth?: number }) => {
return (
<ChronicleCard
id={item.id}
title={item.title}
subtitle={item.subtitle}
description={item.description}
date={item.date}
cardWidth={cardWidth}
/>
)
}
Expand All @@ -31,6 +33,7 @@ export const ChronicleCardListBase: FunctionComponent<ChronicleCardListProps> =
data,
indexItem = 0,
horizontal = true,
cardWidth,
}) => {
const listRef = useRef<FlatList>(null)

Expand All @@ -46,7 +49,7 @@ export const ChronicleCardListBase: FunctionComponent<ChronicleCardListProps> =
<FlatList
ref={listRef}
data={data}
renderItem={renderItem}
renderItem={({ item }) => renderItem({ item, cardWidth })}
keyExtractor={keyExtractor}
ItemSeparatorComponent={horizontal ? RowSeparator : ColumnSeparator}
contentContainerStyle={contentContainerStyle}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,23 @@ import React from 'react'
import { Animated } from 'react-native'

import { ChroniclesHeader } from 'features/chronicle/components/ChroniclesHeader/ChroniclesHeader'
import * as useGoBack from 'features/navigation/useGoBack'
import { act, render, screen, userEvent } from 'tests/utils'

jest.unmock('react-native/Libraries/Animated/createAnimatedComponent')

const mockGoBack = jest.fn()
jest.spyOn(useGoBack, 'useGoBack').mockReturnValue({
goBack: mockGoBack,
canGoBack: jest.fn(() => true),
})
const mockHandleGoBack = jest.fn()

const user = userEvent.setup()

jest.useFakeTimers()

describe('<ChroniclesHeader />', () => {
it('should goBack when we press on the back button', async () => {
it('should handle goBack when pressing back button', async () => {
renderChroniclesHeader()

await user.press(screen.getByTestId('animated-icon-back'))

expect(mockGoBack).toHaveBeenCalledTimes(1)
expect(mockHandleGoBack).toHaveBeenCalledTimes(1)
})

it('should fully display the title at the end of the animation', () => {
Expand All @@ -48,6 +43,7 @@ function renderChroniclesHeader() {
<ChroniclesHeader
title='Tous les avis de "Mon oeuvre incroyable"'
headerTransition={animatedValue}
handleGoBack={mockHandleGoBack}
/>
)
return { animatedValue }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import React, { FunctionComponent } from 'react'
import { Animated } from 'react-native'

import { getSearchStackConfig } from 'features/navigation/SearchStackNavigator/helpers'
import { useGoBack } from 'features/navigation/useGoBack'
import { ContentHeader } from 'ui/components/headers/ContentHeader'

type Props = {
headerTransition: Animated.AnimatedInterpolation<string | number>
title: string
handleGoBack: VoidFunction
}

export const ChroniclesHeader: FunctionComponent<Props> = ({ headerTransition, title }) => {
const { goBack } = useGoBack(...getSearchStackConfig('SearchLanding'))

export const ChroniclesHeader: FunctionComponent<Props> = ({
headerTransition,
title,
handleGoBack,
}) => {
return (
<ContentHeader
headerTitle={title}
headerTransition={headerTransition}
titleTestID="chroniclesHeaderName"
onBackPress={goBack}
onBackPress={handleGoBack}
/>
)
}
30 changes: 30 additions & 0 deletions src/features/chronicle/fixtures/offerChronicles.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { ReadonlyDeep } from 'type-fest'

import { OfferChronicle, OfferChronicles } from 'api/gen'

export const chroniclesFixture = [
{
id: 31,
dateCreated: '2025-01-20T23:32:14.456451Z',
author: {
firstName: null,
age: 15,
city: 'Paris',
},
content: 'Chronique sur le produit Product 30 mais sans utilisateur.',
},
{
id: 1,
dateCreated: '2025-01-20T23:32:13.978038Z',
author: {
firstName: 'Jeanne',
age: 15,
city: 'Paris',
},
content: 'Chronique sur le produit Product 30 \u00e9crite par l’utilisateur Jeanne Doux (2).',
},
] as const satisfies readonly OfferChronicle[]

export const offerChroniclesFixture = {
chronicles: chroniclesFixture,
} as const satisfies ReadonlyDeep<OfferChronicles>
22 changes: 19 additions & 3 deletions src/features/chronicle/pages/Chronicles/Chronicles.native.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
import React from 'react'

import { useRoute } from '__mocks__/@react-navigation/native'
import { offerChroniclesFixture } from 'features/chronicle/fixtures/offerChronicles.fixture'
import { Chronicles } from 'features/chronicle/pages/Chronicles/Chronicles'
import { offerResponseSnap } from 'features/offer/fixtures/offerResponse'
import { mockServer } from 'tests/mswServer'
import { reactQueryProviderHOC } from 'tests/reactQueryProviderHOC'
import { render, screen } from 'tests/utils'

useRoute.mockReturnValue({
params: {
offerId: offerResponseSnap.id,
},
})

describe('Chronicles', () => {
it('should render correctly', () => {
render(<Chronicles />)
beforeEach(() => {
mockServer.getApi(`/v2/offer/${offerResponseSnap.id}`, offerResponseSnap)
mockServer.getApi(`/v1/offer/${offerResponseSnap.id}/chronicles`, offerChroniclesFixture)
})

it('should render correctly', async () => {
render(reactQueryProviderHOC(<Chronicles />))

expect(screen.getByText('Tous les avis')).toBeOnTheScreen()
expect(await screen.findByText('Tous les avis')).toBeOnTheScreen()
})
})
Loading

0 comments on commit a3c4293

Please sign in to comment.