Skip to content

learning resource drawer v2 run comparison table #1782

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

Merged
merged 11 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ResourceTypeEnum } from "api"
import { DeliveryEnum, DeliveryEnumDescriptions, ResourceTypeEnum } from "api"
import { factories } from "api/test-utils"

const _makeResource = factories.learningResources.resource
Expand Down Expand Up @@ -41,6 +41,12 @@ const resources = {
}),
}

const sameDataRun = factories.learningResources.run({
resource_prices: [
{ amount: "0", currency: "USD" },
{ amount: "100", currency: "USD" },
],
})
const courses = {
free: {
noCertificate: makeResource({
Expand Down Expand Up @@ -152,6 +158,82 @@ const courses = {
availability: "dated",
}),
},
multipleRuns: {
sameData: makeResource({
resource_type: ResourceTypeEnum.Course,
runs: [
factories.learningResources.run({
delivery: sameDataRun.delivery,
resource_prices: sameDataRun.resource_prices,
location: sameDataRun.location,
}),
factories.learningResources.run({
delivery: sameDataRun.delivery,
resource_prices: sameDataRun.resource_prices,
location: sameDataRun.location,
}),
factories.learningResources.run({
delivery: sameDataRun.delivery,
resource_prices: sameDataRun.resource_prices,
location: sameDataRun.location,
}),
factories.learningResources.run({
delivery: sameDataRun.delivery,
resource_prices: sameDataRun.resource_prices,
location: sameDataRun.location,
}),
],
}),
differentData: makeResource({
resource_type: ResourceTypeEnum.Course,
runs: [
factories.learningResources.run({
delivery: [
{
code: DeliveryEnum.Online,
name: DeliveryEnumDescriptions.online,
},
],
resource_prices: [
{ amount: "0", currency: "USD" },
{ amount: "100", currency: "USD" },
],
}),
factories.learningResources.run({
delivery: [
{
code: DeliveryEnum.Online,
name: DeliveryEnumDescriptions.online,
},
],
resource_prices: [
{ amount: "0", currency: "USD" },
{ amount: "100", currency: "USD" },
],
}),
factories.learningResources.run({
delivery: [
{
code: DeliveryEnum.InPerson,
name: DeliveryEnumDescriptions.in_person,
},
],
resource_prices: [{ amount: "150", currency: "USD" }],
location: "Earth",
}),
factories.learningResources.run({
delivery: [
{
code: DeliveryEnum.InPerson,
name: DeliveryEnumDescriptions.in_person,
},
],
resource_prices: [{ amount: "150", currency: "USD" }],
location: "Earth",
}),
],
}),
},
}

const resourceArgType = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from "react"
import { render, screen, within } from "@testing-library/react"
import { courses } from "../LearningResourceCard/testUtils"
import InfoSectionV2 from "./InfoSectionV2"
import { ThemeProvider } from "../ThemeProvider/ThemeProvider"
import { DeliveryEnumDescriptions } from "api"

describe("Differing runs comparison table", () => {
test("Does not appear if data is the same", () => {
const course = courses.multipleRuns.sameData
render(<InfoSectionV2 resource={course} />, {
wrapper: ThemeProvider,
})
expect(screen.queryByTestId("differing-runs-table")).toBeNull()
})

test("Appears if data is different", () => {
const course = courses.multipleRuns.differentData
render(<InfoSectionV2 resource={course} />, {
wrapper: ThemeProvider,
})
const differingRunsTable = screen.getByTestId("differing-runs-table")
expect(differingRunsTable).toBeInTheDocument()
const onlineLabels = within(differingRunsTable).getAllByText(
DeliveryEnumDescriptions.online,
)
const inPersonLabels = within(differingRunsTable).getAllByText(
DeliveryEnumDescriptions.in_person,
)
const onlinePriceLabels = within(differingRunsTable).getAllByText("$100")
const inPersonPriceLabels = within(differingRunsTable).getAllByText("$150")
const earthLocationLabels = within(differingRunsTable).getAllByText("Earth")
expect(onlineLabels).toHaveLength(2)
expect(inPersonLabels).toHaveLength(2)
expect(onlinePriceLabels).toHaveLength(2)
expect(inPersonPriceLabels).toHaveLength(2)
expect(earthLocationLabels).toHaveLength(2)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import React from "react"
import styled from "@emotion/styled"
import { theme } from "../ThemeProvider/ThemeProvider"
import { LearningResource, LearningResourcePrice } from "api"
import {
formatRunDate,
getDisplayPrice,
getRunPrices,
showStartAnytime,
} from "ol-utilities"

const DifferingRuns = styled.div({
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
alignSelf: "stretch",
borderRadius: "4px",
border: `1px solid ${theme.custom.colors.lightGray2}`,
borderBottom: "none",
})

const DifferingRun = styled.div({
display: "flex",
flexWrap: "wrap",
alignItems: "center",
gap: "16px",
padding: "12px",
alignSelf: "stretch",
borderBottom: `1px solid ${theme.custom.colors.lightGray2}`,
})

const DifferingRunHeader = styled.div({
display: "flex",
alignSelf: "stretch",
alignItems: "center",
flex: "1 0 0",
gap: "16px",
padding: "12px",
color: theme.custom.colors.darkGray2,
backgroundColor: theme.custom.colors.lightGray1,
...theme.typography.subtitle3,
})

const DifferingRunData = styled.div({
display: "flex",
flexShrink: 0,
flex: "1 0 0",
color: theme.custom.colors.darkGray2,
...theme.typography.body3,
})

const DifferingRunLabel = styled.strong({
display: "flex",
flex: "1 0 0",
})

const DifferingRunLocation = styled(DifferingRunData)({
flex: "1 0 100%",
flexDirection: "column",
alignSelf: "stretch",
})

const DifferingRunsTable: React.FC<{ resource: LearningResource }> = ({
resource,
}) => {
if (!resource.runs) {
return null
}
if (resource.runs.length === 1) {
return null
}
const asTaughtIn = resource ? showStartAnytime(resource) : false
const prices: LearningResourcePrice[] = []
const deliveryMethods = []
const locations = []
for (const run of resource.runs) {
if (run.resource_prices) {
run.resource_prices.forEach((price) => {
if (price.amount !== "0") {
prices.push(price)
}
})
}
if (run.delivery) {
deliveryMethods.push(run.delivery)
}
if (run.location) {
locations.push(run.location)
}
}
const distinctPrices = [...new Set(prices.map((p) => p.amount).flat())]
const distinctDeliveryMethods = [
...new Set(deliveryMethods.flat().map((dm) => dm?.code)),
]
const distinctLocations = [...new Set(locations.flat().map((l) => l))]
if (
distinctPrices.length > 1 ||
distinctDeliveryMethods.length > 1 ||
distinctLocations.length > 1
) {
return (
<DifferingRuns data-testid="differing-runs-table">
<DifferingRunHeader>
<DifferingRunLabel>Date</DifferingRunLabel>
<DifferingRunLabel>Price</DifferingRunLabel>
<DifferingRunLabel>Format</DifferingRunLabel>
</DifferingRunHeader>
{resource.runs.map((run, index) => (
<DifferingRun key={index}>
<DifferingRunData>
{formatRunDate(run, asTaughtIn)}
</DifferingRunData>
{run.resource_prices && (
<DifferingRunData>
<span>{getDisplayPrice(getRunPrices(run)["course"])}</span>
</DifferingRunData>
)}
{run.delivery && (
<DifferingRunData>
<span>{run.delivery?.map((dm) => dm?.name).join(", ")}</span>
</DifferingRunData>
)}
{run.delivery.filter((d) => d.code === "in_person").length > 0 &&
Copy link
Contributor

Choose a reason for hiding this comment

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

Is run.delivery always an array here as there are conditionals on it above?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yea, according to the API spec it should always be an array. I've also seen it return an array with a single element:

delivery: Array<CourseResourceDeliveryInner>

run.location && (
<DifferingRunLocation>
<strong>Location</strong>
<span>{run.location}</span>
</DifferingRunLocation>
)}
</DifferingRun>
))}
</DifferingRuns>
)
}
return null
}

export default DifferingRunsTable
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
showStartAnytime,
} from "ol-utilities"
import { theme } from "../ThemeProvider/ThemeProvider"
import DifferingRunsTable from "./DifferingRunsTable"

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

return (
<InfoItems data-testid="drawer-info-items">
{infoItems.map((props, index) => (
<InfoItem key={index} {...props} />
))}
</InfoItems>
<>
<DifferingRunsTable resource={resource} />
<InfoItems data-testid="drawer-info-items">
{infoItems.map((props, index) => (
<InfoItem key={index} {...props} />
))}
</InfoItems>
</>
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ const Image = styled(NextImage)({
borderRadius: "8px",
width: "100%",
objectFit: "cover",
zIndex: -1,
Copy link
Contributor

Choose a reason for hiding this comment

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

Yay

Copy link
Contributor Author

Choose a reason for hiding this comment

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

🙃

})

const SkeletonImage = styled(Skeleton)<{ aspect: number }>((aspect) => ({
Expand Down
47 changes: 33 additions & 14 deletions frontends/ol-utilities/src/learning-resources/pricing.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { LearningResource, LearningResourcePrice, ResourceTypeEnum } from "api"
import {
LearningResource,
LearningResourcePrice,
LearningResourceRun,
ResourceTypeEnum,
} from "api"
import { findBestRun } from "ol-utilities"
import getSymbolFromCurrency from "currency-symbol-map"

Expand Down Expand Up @@ -30,20 +35,23 @@ type Prices = {
certificate: null | LearningResourcePrice[]
}

const getPrices = (resource: LearningResource): Prices => {
const sortedNonzero = resource.resource_prices
? resource.resource_prices
.sort(
(a: LearningResourcePrice, b: LearningResourcePrice) =>
Number(a.amount) - Number(b.amount),
)
.filter((price: LearningResourcePrice) => Number(price.amount) > 0)
: []

const getPrices = (prices: LearningResourcePrice[]) => {
const sortedNonzero = prices
.sort(
(a: LearningResourcePrice, b: LearningResourcePrice) =>
Number(a.amount) - Number(b.amount),
)
.filter((price: LearningResourcePrice) => Number(price.amount) > 0)
const priceRange = sortedNonzero.filter(
(price, index, arr) => index === 0 || index === arr.length - 1,
)
const prices = priceRange.length > 0 ? priceRange : null
return priceRange.length > 0 ? priceRange : null
}

const getResourcePrices = (resource: LearningResource): Prices => {
const prices = resource.resource_prices
? getPrices(resource.resource_prices)
: []

if (resource.free) {
return resource.certification
Expand All @@ -56,14 +64,25 @@ const getPrices = (resource: LearningResource): Prices => {
}
}

export const getRunPrices = (run: LearningResourceRun): Prices => {
const prices = run.resource_prices ? getPrices(run.resource_prices) : []

return {
course: prices ?? PAID,
certificate: null,
}
}

const getDisplayPrecision = (price: number) => {
if (Number.isInteger(price)) {
return price.toFixed(0)
}
return price.toFixed(2)
}

const getDisplayPrice = (price: Prices["course"] | Prices["certificate"]) => {
export const getDisplayPrice = (
price: Prices["course"] | Prices["certificate"],
) => {
if (price === null) {
return null
}
Expand All @@ -82,7 +101,7 @@ const getDisplayPrice = (price: Prices["course"] | Prices["certificate"]) => {
}

export const getLearningResourcePrices = (resource: LearningResource) => {
const prices = getPrices(resource)
const prices = getResourcePrices(resource)
return {
course: {
value: prices.course,
Expand Down
Loading