Skip to content

Commit f27d2ce

Browse files
authored
learning resource drawer v2 run comparison table (#1782)
* add first draft of run comparison table * hide labels on mobile * update differing run table styles * add tests * display table if location is different * move DifferingRunsTable to its own file * use getDisplayPrice * fix CTA image z-index issue * remove errant console.log * fix location display * remove duplicate sort
1 parent 1a38a09 commit f27d2ce

File tree

6 files changed

+303
-20
lines changed

6 files changed

+303
-20
lines changed

frontends/ol-components/src/components/LearningResourceCard/testUtils.ts

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ResourceTypeEnum } from "api"
1+
import { DeliveryEnum, DeliveryEnumDescriptions, ResourceTypeEnum } from "api"
22
import { factories } from "api/test-utils"
33

44
const _makeResource = factories.learningResources.resource
@@ -41,6 +41,12 @@ const resources = {
4141
}),
4242
}
4343

44+
const sameDataRun = factories.learningResources.run({
45+
resource_prices: [
46+
{ amount: "0", currency: "USD" },
47+
{ amount: "100", currency: "USD" },
48+
],
49+
})
4450
const courses = {
4551
free: {
4652
noCertificate: makeResource({
@@ -152,6 +158,82 @@ const courses = {
152158
availability: "dated",
153159
}),
154160
},
161+
multipleRuns: {
162+
sameData: makeResource({
163+
resource_type: ResourceTypeEnum.Course,
164+
runs: [
165+
factories.learningResources.run({
166+
delivery: sameDataRun.delivery,
167+
resource_prices: sameDataRun.resource_prices,
168+
location: sameDataRun.location,
169+
}),
170+
factories.learningResources.run({
171+
delivery: sameDataRun.delivery,
172+
resource_prices: sameDataRun.resource_prices,
173+
location: sameDataRun.location,
174+
}),
175+
factories.learningResources.run({
176+
delivery: sameDataRun.delivery,
177+
resource_prices: sameDataRun.resource_prices,
178+
location: sameDataRun.location,
179+
}),
180+
factories.learningResources.run({
181+
delivery: sameDataRun.delivery,
182+
resource_prices: sameDataRun.resource_prices,
183+
location: sameDataRun.location,
184+
}),
185+
],
186+
}),
187+
differentData: makeResource({
188+
resource_type: ResourceTypeEnum.Course,
189+
runs: [
190+
factories.learningResources.run({
191+
delivery: [
192+
{
193+
code: DeliveryEnum.Online,
194+
name: DeliveryEnumDescriptions.online,
195+
},
196+
],
197+
resource_prices: [
198+
{ amount: "0", currency: "USD" },
199+
{ amount: "100", currency: "USD" },
200+
],
201+
}),
202+
factories.learningResources.run({
203+
delivery: [
204+
{
205+
code: DeliveryEnum.Online,
206+
name: DeliveryEnumDescriptions.online,
207+
},
208+
],
209+
resource_prices: [
210+
{ amount: "0", currency: "USD" },
211+
{ amount: "100", currency: "USD" },
212+
],
213+
}),
214+
factories.learningResources.run({
215+
delivery: [
216+
{
217+
code: DeliveryEnum.InPerson,
218+
name: DeliveryEnumDescriptions.in_person,
219+
},
220+
],
221+
resource_prices: [{ amount: "150", currency: "USD" }],
222+
location: "Earth",
223+
}),
224+
factories.learningResources.run({
225+
delivery: [
226+
{
227+
code: DeliveryEnum.InPerson,
228+
name: DeliveryEnumDescriptions.in_person,
229+
},
230+
],
231+
resource_prices: [{ amount: "150", currency: "USD" }],
232+
location: "Earth",
233+
}),
234+
],
235+
}),
236+
},
155237
}
156238

157239
const resourceArgType = {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import React from "react"
2+
import { render, screen, within } from "@testing-library/react"
3+
import { courses } from "../LearningResourceCard/testUtils"
4+
import InfoSectionV2 from "./InfoSectionV2"
5+
import { ThemeProvider } from "../ThemeProvider/ThemeProvider"
6+
import { DeliveryEnumDescriptions } from "api"
7+
8+
describe("Differing runs comparison table", () => {
9+
test("Does not appear if data is the same", () => {
10+
const course = courses.multipleRuns.sameData
11+
render(<InfoSectionV2 resource={course} />, {
12+
wrapper: ThemeProvider,
13+
})
14+
expect(screen.queryByTestId("differing-runs-table")).toBeNull()
15+
})
16+
17+
test("Appears if data is different", () => {
18+
const course = courses.multipleRuns.differentData
19+
render(<InfoSectionV2 resource={course} />, {
20+
wrapper: ThemeProvider,
21+
})
22+
const differingRunsTable = screen.getByTestId("differing-runs-table")
23+
expect(differingRunsTable).toBeInTheDocument()
24+
const onlineLabels = within(differingRunsTable).getAllByText(
25+
DeliveryEnumDescriptions.online,
26+
)
27+
const inPersonLabels = within(differingRunsTable).getAllByText(
28+
DeliveryEnumDescriptions.in_person,
29+
)
30+
const onlinePriceLabels = within(differingRunsTable).getAllByText("$100")
31+
const inPersonPriceLabels = within(differingRunsTable).getAllByText("$150")
32+
const earthLocationLabels = within(differingRunsTable).getAllByText("Earth")
33+
expect(onlineLabels).toHaveLength(2)
34+
expect(inPersonLabels).toHaveLength(2)
35+
expect(onlinePriceLabels).toHaveLength(2)
36+
expect(inPersonPriceLabels).toHaveLength(2)
37+
expect(earthLocationLabels).toHaveLength(2)
38+
})
39+
})
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import React from "react"
2+
import styled from "@emotion/styled"
3+
import { theme } from "../ThemeProvider/ThemeProvider"
4+
import { LearningResource, LearningResourcePrice } from "api"
5+
import {
6+
formatRunDate,
7+
getDisplayPrice,
8+
getRunPrices,
9+
showStartAnytime,
10+
} from "ol-utilities"
11+
12+
const DifferingRuns = styled.div({
13+
display: "flex",
14+
flexDirection: "column",
15+
alignItems: "flex-start",
16+
alignSelf: "stretch",
17+
borderRadius: "4px",
18+
border: `1px solid ${theme.custom.colors.lightGray2}`,
19+
borderBottom: "none",
20+
})
21+
22+
const DifferingRun = styled.div({
23+
display: "flex",
24+
flexWrap: "wrap",
25+
alignItems: "center",
26+
gap: "16px",
27+
padding: "12px",
28+
alignSelf: "stretch",
29+
borderBottom: `1px solid ${theme.custom.colors.lightGray2}`,
30+
})
31+
32+
const DifferingRunHeader = styled.div({
33+
display: "flex",
34+
alignSelf: "stretch",
35+
alignItems: "center",
36+
flex: "1 0 0",
37+
gap: "16px",
38+
padding: "12px",
39+
color: theme.custom.colors.darkGray2,
40+
backgroundColor: theme.custom.colors.lightGray1,
41+
...theme.typography.subtitle3,
42+
})
43+
44+
const DifferingRunData = styled.div({
45+
display: "flex",
46+
flexShrink: 0,
47+
flex: "1 0 0",
48+
color: theme.custom.colors.darkGray2,
49+
...theme.typography.body3,
50+
})
51+
52+
const DifferingRunLabel = styled.strong({
53+
display: "flex",
54+
flex: "1 0 0",
55+
})
56+
57+
const DifferingRunLocation = styled(DifferingRunData)({
58+
flex: "1 0 100%",
59+
flexDirection: "column",
60+
alignSelf: "stretch",
61+
})
62+
63+
const DifferingRunsTable: React.FC<{ resource: LearningResource }> = ({
64+
resource,
65+
}) => {
66+
if (!resource.runs) {
67+
return null
68+
}
69+
if (resource.runs.length === 1) {
70+
return null
71+
}
72+
const asTaughtIn = resource ? showStartAnytime(resource) : false
73+
const prices: LearningResourcePrice[] = []
74+
const deliveryMethods = []
75+
const locations = []
76+
for (const run of resource.runs) {
77+
if (run.resource_prices) {
78+
run.resource_prices.forEach((price) => {
79+
if (price.amount !== "0") {
80+
prices.push(price)
81+
}
82+
})
83+
}
84+
if (run.delivery) {
85+
deliveryMethods.push(run.delivery)
86+
}
87+
if (run.location) {
88+
locations.push(run.location)
89+
}
90+
}
91+
const distinctPrices = [...new Set(prices.map((p) => p.amount).flat())]
92+
const distinctDeliveryMethods = [
93+
...new Set(deliveryMethods.flat().map((dm) => dm?.code)),
94+
]
95+
const distinctLocations = [...new Set(locations.flat().map((l) => l))]
96+
if (
97+
distinctPrices.length > 1 ||
98+
distinctDeliveryMethods.length > 1 ||
99+
distinctLocations.length > 1
100+
) {
101+
return (
102+
<DifferingRuns data-testid="differing-runs-table">
103+
<DifferingRunHeader>
104+
<DifferingRunLabel>Date</DifferingRunLabel>
105+
<DifferingRunLabel>Price</DifferingRunLabel>
106+
<DifferingRunLabel>Format</DifferingRunLabel>
107+
</DifferingRunHeader>
108+
{resource.runs.map((run, index) => (
109+
<DifferingRun key={index}>
110+
<DifferingRunData>
111+
{formatRunDate(run, asTaughtIn)}
112+
</DifferingRunData>
113+
{run.resource_prices && (
114+
<DifferingRunData>
115+
<span>{getDisplayPrice(getRunPrices(run)["course"])}</span>
116+
</DifferingRunData>
117+
)}
118+
{run.delivery && (
119+
<DifferingRunData>
120+
<span>{run.delivery?.map((dm) => dm?.name).join(", ")}</span>
121+
</DifferingRunData>
122+
)}
123+
{run.delivery.filter((d) => d.code === "in_person").length > 0 &&
124+
run.location && (
125+
<DifferingRunLocation>
126+
<strong>Location</strong>
127+
<span>{run.location}</span>
128+
</DifferingRunLocation>
129+
)}
130+
</DifferingRun>
131+
))}
132+
</DifferingRuns>
133+
)
134+
}
135+
return null
136+
}
137+
138+
export default DifferingRunsTable

frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
showStartAnytime,
2323
} from "ol-utilities"
2424
import { theme } from "../ThemeProvider/ThemeProvider"
25+
import DifferingRunsTable from "./DifferingRunsTable"
2526

2627
const SeparatorContainer = styled.span({
2728
padding: "0 8px",
@@ -392,11 +393,14 @@ const InfoSectionV2 = ({ resource }: { resource?: LearningResource }) => {
392393
}
393394

394395
return (
395-
<InfoItems data-testid="drawer-info-items">
396-
{infoItems.map((props, index) => (
397-
<InfoItem key={index} {...props} />
398-
))}
399-
</InfoItems>
396+
<>
397+
<DifferingRunsTable resource={resource} />
398+
<InfoItems data-testid="drawer-info-items">
399+
{infoItems.map((props, index) => (
400+
<InfoItem key={index} {...props} />
401+
))}
402+
</InfoItems>
403+
</>
400404
)
401405
}
402406

frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV2.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ const Image = styled(NextImage)({
8888
borderRadius: "8px",
8989
width: "100%",
9090
objectFit: "cover",
91+
zIndex: -1,
9192
})
9293

9394
const SkeletonImage = styled(Skeleton)<{ aspect: number }>((aspect) => ({

frontends/ol-utilities/src/learning-resources/pricing.ts

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { LearningResource, LearningResourcePrice, ResourceTypeEnum } from "api"
1+
import {
2+
LearningResource,
3+
LearningResourcePrice,
4+
LearningResourceRun,
5+
ResourceTypeEnum,
6+
} from "api"
27
import { findBestRun } from "ol-utilities"
38
import getSymbolFromCurrency from "currency-symbol-map"
49

@@ -30,20 +35,23 @@ type Prices = {
3035
certificate: null | LearningResourcePrice[]
3136
}
3237

33-
const getPrices = (resource: LearningResource): Prices => {
34-
const sortedNonzero = resource.resource_prices
35-
? resource.resource_prices
36-
.sort(
37-
(a: LearningResourcePrice, b: LearningResourcePrice) =>
38-
Number(a.amount) - Number(b.amount),
39-
)
40-
.filter((price: LearningResourcePrice) => Number(price.amount) > 0)
41-
: []
42-
38+
const getPrices = (prices: LearningResourcePrice[]) => {
39+
const sortedNonzero = prices
40+
.sort(
41+
(a: LearningResourcePrice, b: LearningResourcePrice) =>
42+
Number(a.amount) - Number(b.amount),
43+
)
44+
.filter((price: LearningResourcePrice) => Number(price.amount) > 0)
4345
const priceRange = sortedNonzero.filter(
4446
(price, index, arr) => index === 0 || index === arr.length - 1,
4547
)
46-
const prices = priceRange.length > 0 ? priceRange : null
48+
return priceRange.length > 0 ? priceRange : null
49+
}
50+
51+
const getResourcePrices = (resource: LearningResource): Prices => {
52+
const prices = resource.resource_prices
53+
? getPrices(resource.resource_prices)
54+
: []
4755

4856
if (resource.free) {
4957
return resource.certification
@@ -56,14 +64,25 @@ const getPrices = (resource: LearningResource): Prices => {
5664
}
5765
}
5866

67+
export const getRunPrices = (run: LearningResourceRun): Prices => {
68+
const prices = run.resource_prices ? getPrices(run.resource_prices) : []
69+
70+
return {
71+
course: prices ?? PAID,
72+
certificate: null,
73+
}
74+
}
75+
5976
const getDisplayPrecision = (price: number) => {
6077
if (Number.isInteger(price)) {
6178
return price.toFixed(0)
6279
}
6380
return price.toFixed(2)
6481
}
6582

66-
const getDisplayPrice = (price: Prices["course"] | Prices["certificate"]) => {
83+
export const getDisplayPrice = (
84+
price: Prices["course"] | Prices["certificate"],
85+
) => {
6786
if (price === null) {
6887
return null
6988
}
@@ -82,7 +101,7 @@ const getDisplayPrice = (price: Prices["course"] | Prices["certificate"]) => {
82101
}
83102

84103
export const getLearningResourcePrices = (resource: LearningResource) => {
85-
const prices = getPrices(resource)
104+
const prices = getResourcePrices(resource)
86105
return {
87106
course: {
88107
value: prices.course,

0 commit comments

Comments
 (0)