Skip to content

Commit f8c2776

Browse files
gumaercmbertrand
authored andcommitted
similar resources carousel (#1835)
* reconfigure API endpoints to not be paginated, support similar resources query and add to v2 drawer * style the carousel * retrieve not list * set anonymous read only permission on new endpoints * put the api endpoints back the way they were * fix js tests * make carousels optional * more styling work * flex grow the carousel section * add vector based similar learning resources carousel * use correct type * add tests * adjust carousel section mobile padding * instead of setting z index on other elements within the drawer to get them below the header, raise the header itself * remove stray comment
1 parent 157da12 commit f8c2776

File tree

11 files changed

+256
-71
lines changed

11 files changed

+256
-71
lines changed

frontends/api/src/generated/v1/api.ts

Lines changed: 4 additions & 40 deletions
Large diffs are not rendered by default.

frontends/api/src/hooks/learningResources/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,26 @@ const updateListParents = (
179179
}
180180
}
181181

182+
const useSimilarLearningResources = (
183+
id: number,
184+
opts: Pick<UseQueryOptions, "enabled"> = {},
185+
) => {
186+
return useQuery({
187+
...learningResources.similar(id),
188+
...opts,
189+
})
190+
}
191+
192+
const useVectorSimilarLearningResources = (
193+
id: number,
194+
opts: Pick<UseQueryOptions, "enabled"> = {},
195+
) => {
196+
return useQuery({
197+
...learningResources.vectorSimilar(id),
198+
...opts,
199+
})
200+
}
201+
182202
export {
183203
useLearningResourcesList,
184204
useFeaturedLearningResourcesList,
@@ -192,4 +212,6 @@ export {
192212
usePlatformsList,
193213
useSchoolsList,
194214
learningResources as learningResourcesKeyFactory,
215+
useSimilarLearningResources,
216+
useVectorSimilarLearningResources,
195217
}

frontends/api/src/hooks/learningResources/keyFactory.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,24 @@ const learningResources = createQueryKeys("learningResources", {
107107
queryFn: () => schoolsApi.schoolsList().then((res) => res.data),
108108
}
109109
},
110+
similar: (id: number) => {
111+
return {
112+
queryKey: [`similar_resources-${id}`],
113+
queryFn: () =>
114+
learningResourcesApi
115+
.learningResourcesSimilarList({ id })
116+
.then((res) => res.data),
117+
}
118+
},
119+
vectorSimilar: (id: number) => {
120+
return {
121+
queryKey: [`vector_similar_resources-${id}`],
122+
queryFn: () =>
123+
learningResourcesApi
124+
.learningResourcesVectorSimilarList({ id })
125+
.then((res) => res.data),
126+
}
127+
},
110128
})
111129

112130
export default learningResources

frontends/api/src/test-utils/urls.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ const learningResources = {
7979
`${API_BASE_URL}/api/v1/learning_resources/${params.id}/`,
8080
featured: (params?: Params<FeaturedApi, "featuredList">) =>
8181
`${API_BASE_URL}/api/v1/featured/${query(params)}`,
82+
similar: (params: Params<LRApi, "learningResourcesSimilarList">) =>
83+
`${API_BASE_URL}/api/v1/learning_resources/${params.id}/similar/`,
84+
vectorSimilar: (
85+
params: Params<LRApi, "learningResourcesVectorSimilarList">,
86+
) => `${API_BASE_URL}/api/v1/learning_resources/${params.id}/vector_similar/`,
8287
setLearningPathRelationships: (
8388
params?: Params<LRApi, "learningResourcesLearningPathsPartialUpdate">,
8489
) =>

frontends/main/src/page-components/LearningResourceDrawer/LearningResourceDrawerV2.test.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ describe("LearningResourceDrawerV2", () => {
4848
urls.learningResources.details({ id: resource.id }),
4949
resource,
5050
)
51+
setMockResponse.get(
52+
urls.learningResources.similar({ id: resource.id }),
53+
[],
54+
)
55+
setMockResponse.get(
56+
urls.learningResources.vectorSimilar({ id: resource.id }),
57+
[],
58+
)
5159

5260
renderWithProviders(<LearningResourceDrawerV2 />, {
5361
url: `?dog=woof&${RESOURCE_DRAWER_QUERY_PARAM}=${resource.id}`,
@@ -108,6 +116,14 @@ describe("LearningResourceDrawerV2", () => {
108116
urls.learningResources.details({ id: resource.id }),
109117
resource,
110118
)
119+
setMockResponse.get(
120+
urls.learningResources.similar({ id: resource.id }),
121+
[],
122+
)
123+
setMockResponse.get(
124+
urls.learningResources.vectorSimilar({ id: resource.id }),
125+
[],
126+
)
111127
const user = factories.user.user({
112128
is_learning_path_editor: isLearningPathEditor,
113129
})
@@ -139,4 +155,46 @@ describe("LearningResourceDrawerV2", () => {
139155
).toBe(expectAddToLearningPathButton)
140156
},
141157
)
158+
159+
it("Renders similar resource carousels", async () => {
160+
const resource = factories.learningResources.resource({
161+
resource_type: ResourceTypeEnum.Course,
162+
runs: [
163+
factories.learningResources.run({
164+
languages: ["en-us", "es-es", "fr-fr"],
165+
}),
166+
],
167+
})
168+
const count = 10
169+
const similarResources = factories.learningResources.resources({
170+
count,
171+
}).results
172+
const vectorSimilarResources = factories.learningResources.resources({
173+
count,
174+
}).results
175+
setMockResponse.get(urls.userMe.get(), null, { code: 403 })
176+
setMockResponse.get(
177+
urls.learningResources.details({ id: resource.id }),
178+
resource,
179+
)
180+
setMockResponse.get(
181+
urls.learningResources.similar({ id: resource.id }),
182+
similarResources,
183+
)
184+
setMockResponse.get(
185+
urls.learningResources.vectorSimilar({ id: resource.id }),
186+
vectorSimilarResources,
187+
)
188+
renderWithProviders(<LearningResourceDrawerV2 />, {
189+
url: `?resource=${resource.id}`,
190+
})
191+
await screen.findByText("Similar Learning Resources")
192+
for (const similarResource of similarResources) {
193+
await screen.findByText(similarResource.title)
194+
}
195+
await screen.findByText("Similar Learning Resources (Vector Based)")
196+
for (const vectorSimilarResource of vectorSimilarResources) {
197+
await screen.findByText(vectorSimilarResource.title)
198+
}
199+
})
142200
})

frontends/main/src/page-components/LearningResourceDrawer/LearningResourceDrawerV2.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from "../Dialogs/AddToListDialog"
2020
import { SignupPopover } from "../SignupPopover/SignupPopover"
2121
import { usePostHog } from "posthog-js/react"
22+
import ResourceCarousel from "../ResourceCarousel/ResourceCarousel"
2223

2324
const RESOURCE_DRAWER_PARAMS = [RESOURCE_DRAWER_QUERY_PARAM] as const
2425

@@ -87,12 +88,47 @@ const DrawerContent: React.FC<{
8788
}
8889
}, [user])
8990
useCapturePageView(Number(resourceId))
91+
const similarResourcesCarousel = (
92+
<ResourceCarousel
93+
titleComponent="p"
94+
titleVariant="subtitle1"
95+
title="Similar Learning Resources"
96+
config={[
97+
{
98+
label: "Similar Learning Resources",
99+
cardProps: { size: "small" },
100+
data: {
101+
type: "lr_similar",
102+
params: { id: resourceId },
103+
},
104+
},
105+
]}
106+
/>
107+
)
108+
const vectorSimilarResourcesCarousel = (
109+
<ResourceCarousel
110+
titleComponent="p"
111+
titleVariant="subtitle1"
112+
title="Similar Learning Resources (Vector Based)"
113+
config={[
114+
{
115+
label: "Similar Learning Resources (Vector Based)",
116+
cardProps: { size: "small" },
117+
data: {
118+
type: "lr_vector_similar",
119+
params: { id: resourceId },
120+
},
121+
},
122+
]}
123+
/>
124+
)
90125

91126
return (
92127
<>
93128
<LearningResourceExpandedV2
94129
imgConfig={imgConfigs.large}
95130
resource={resource.data}
131+
carousels={[similarResourcesCarousel, vectorSimilarResourcesCarousel]}
96132
user={user}
97133
onAddToLearningPathClick={handleAddToLearningPathClick}
98134
onAddToUserListClick={handleAddToUserListClick}

frontends/main/src/page-components/ResourceCarousel/ResourceCarousel.tsx

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -101,17 +101,32 @@ type ContentProps = {
101101

102102
type PanelChildrenProps = {
103103
config: TabConfig[]
104-
queries: UseQueryResult<PaginatedLearningResourceList, unknown>[]
104+
queries: UseQueryResult<
105+
PaginatedLearningResourceList | LearningResource[],
106+
unknown
107+
>[]
105108
children: (props: ContentProps) => React.ReactNode
106109
}
107110
const PanelChildren: React.FC<PanelChildrenProps> = ({
108111
config,
109112
queries,
110113
children,
111114
}) => {
115+
const getResults = (
116+
data: PaginatedLearningResourceList | LearningResource[] | undefined,
117+
): LearningResource[] => {
118+
if (!data) {
119+
return []
120+
}
121+
if ("results" in data) {
122+
return data.results
123+
}
124+
return data
125+
}
126+
112127
if (config.length === 1) {
113128
const { data, isLoading } = queries[0]
114-
const resources = data?.results ?? []
129+
const resources = getResults(data)
115130

116131
return children({
117132
resources,
@@ -123,7 +138,7 @@ const PanelChildren: React.FC<PanelChildrenProps> = ({
123138
<>
124139
{config.map((tabConfig, index) => {
125140
const { data, isLoading } = queries[index]
126-
const resources = data?.results ?? []
141+
const resources = getResults(data)
127142

128143
return (
129144
<StyledTabPanel key={index} value={index.toString()}>
@@ -160,7 +175,8 @@ type ResourceCarouselProps = {
160175
/**
161176
* Element type for the carousel title
162177
*/
163-
titleComponent: React.ElementType
178+
titleComponent?: React.ElementType
179+
titleVariant?: TypographyProps["variant"]
164180
}
165181
/**
166182
* A tabbed carousel that fetches resources based on the configuration provided.
@@ -179,7 +195,8 @@ const ResourceCarousel: React.FC<ResourceCarouselProps> = ({
179195
className,
180196
isLoading,
181197
"data-testid": dataTestId,
182-
titleComponent,
198+
titleComponent = "h4",
199+
titleVariant = "h4",
183200
}) => {
184201
const [tab, setTab] = React.useState("0")
185202
const [ref, setRef] = React.useState<HTMLDivElement | null>(null)
@@ -189,7 +206,7 @@ const ResourceCarousel: React.FC<ResourceCarouselProps> = ({
189206
(
190207
tab,
191208
): UseQueryOptions<
192-
PaginatedLearningResourceList,
209+
PaginatedLearningResourceList | LearningResource[],
193210
unknown,
194211
unknown,
195212
// The factory-generated types for queryKeys are very specific (tuples not arrays)
@@ -205,13 +222,29 @@ const ResourceCarousel: React.FC<ResourceCarouselProps> = ({
205222
return learningResourcesKeyFactory.search(tab.data.params)
206223
case "lr_featured":
207224
return learningResourcesKeyFactory.featured(tab.data.params)
225+
case "lr_similar":
226+
return learningResourcesKeyFactory.similar(tab.data.params.id)
227+
case "lr_vector_similar":
228+
return learningResourcesKeyFactory.vectorSimilar(tab.data.params.id)
208229
}
209230
},
210231
),
211232
})
212233

234+
const getCount = (
235+
data: PaginatedLearningResourceList | LearningResource[] | undefined,
236+
) => {
237+
if (!data) {
238+
return 0
239+
}
240+
if ("count" in data) {
241+
return data.count
242+
}
243+
return data.length
244+
}
245+
213246
const allChildrenLoaded = queries.every(({ isLoading }) => !isLoading)
214-
const allChildrenEmpty = queries.every(({ data }) => !data?.count)
247+
const allChildrenEmpty = queries.every(({ data }) => !getCount(data))
215248
if (!isLoading && allChildrenLoaded && allChildrenEmpty) {
216249
return null
217250
}
@@ -223,7 +256,7 @@ const ResourceCarousel: React.FC<ResourceCarouselProps> = ({
223256
<MobileOverflow className={className} data-testid={dataTestId}>
224257
<TabContext value={tab}>
225258
<HeaderRow>
226-
<HeaderText component={titleComponent} variant="h4">
259+
<HeaderText component={titleComponent} variant={titleVariant}>
227260
{title}
228261
</HeaderText>
229262
{config.length === 1 ? buttonsContainerElement : null}
@@ -237,7 +270,7 @@ const ResourceCarousel: React.FC<ResourceCarouselProps> = ({
237270
if (
238271
!isLoading &&
239272
!queries[index].isLoading &&
240-
!queries[index].data?.count
273+
!getCount(queries[index].data)
241274
) {
242275
return null
243276
}
@@ -256,7 +289,11 @@ const ResourceCarousel: React.FC<ResourceCarouselProps> = ({
256289
</HeaderRow>
257290
<PanelChildren
258291
config={config}
259-
queries={queries as UseQueryResult<PaginatedLearningResourceList>[]}
292+
queries={
293+
queries as UseQueryResult<
294+
PaginatedLearningResourceList | LearningResource[]
295+
>[]
296+
}
260297
>
261298
{({ resources, childrenLoading, tabConfig }) => (
262299
<StyledCarousel arrowsContainer={ref}>

frontends/main/src/page-components/ResourceCarousel/types.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type {
22
LearningResourcesApiLearningResourcesListRequest as LRListRequest,
33
LearningResourcesSearchApiLearningResourcesSearchRetrieveRequest as SearchRequest,
44
FeaturedApiFeaturedListRequest as FeaturedListParams,
5+
LearningResourcesApiLearningResourcesSimilarListRequest as SimilarListParams,
6+
LearningResourcesApiLearningResourcesVectorSimilarListRequest as VectorSimilarListParams,
57
} from "api"
68
import type { LearningResourceCardProps } from "ol-components"
79

@@ -20,7 +22,22 @@ interface FeaturedDataSource {
2022
params: FeaturedListParams
2123
}
2224

23-
type DataSource = ResourceDataSource | SearchDataSource | FeaturedDataSource
25+
interface SimilarDataSource {
26+
type: "lr_similar"
27+
params: SimilarListParams
28+
}
29+
30+
interface VectorSimilarDataSource {
31+
type: "lr_vector_similar"
32+
params: VectorSimilarListParams
33+
}
34+
35+
type DataSource =
36+
| ResourceDataSource
37+
| SearchDataSource
38+
| FeaturedDataSource
39+
| SimilarDataSource
40+
| VectorSimilarDataSource
2441

2542
type TabConfig<D extends DataSource = DataSource> = {
2643
label: React.ReactNode

0 commit comments

Comments
 (0)