diff --git a/data_fixtures/migrations/0005_unit_page_copy_updates.py b/data_fixtures/migrations/0005_unit_page_copy_updates.py new file mode 100644 index 0000000000..c792dc4d46 --- /dev/null +++ b/data_fixtures/migrations/0005_unit_page_copy_updates.py @@ -0,0 +1,35 @@ +from django.db import migrations + +fixtures = [ + { + "name": "mitpe", + "channel_configuration": { + "heading": ( + "Offering lifelong learning opportunities that prepare engineering, " + "science, and technology professionals to address complex industry " + "challenges." + ), + }, + }, +] + + +def update_copy(apps, schema_editor): + Channel = apps.get_model("channels", "Channel") + for fixture in fixtures: + channel_configuration_updates = fixture["channel_configuration"] + channel = Channel.objects.get(name=fixture["name"]) + if Channel.objects.filter(name=fixture["name"]).exists(): + for key, val in channel_configuration_updates.items(): + channel.configuration[key] = val + channel.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("data_fixtures", "0004_upsert_initial_topic_data"), + ] + + operations = [ + migrations.RunPython(update_copy, migrations.RunPython.noop), + ] diff --git a/frontends/mit-open/src/pages/UnitsListingPage/UnitCard.tsx b/frontends/mit-open/src/pages/UnitsListingPage/UnitCard.tsx new file mode 100644 index 0000000000..b0f01991af --- /dev/null +++ b/frontends/mit-open/src/pages/UnitsListingPage/UnitCard.tsx @@ -0,0 +1,215 @@ +import React from "react" +import { LearningResourceOfferorDetail, OfferedByEnum } from "api" +import { Card, Skeleton, Typography, styled, theme } from "ol-components" +import { useChannelDetail } from "api/hooks/channels" + +const CardStyled = styled(Card)({ + height: "100%", +}) + +const UnitCardContainer = styled.div({ + display: "flex", + flexDirection: "column", + alignItems: "center", + height: "100%", + backgroundColor: "rgba(243, 244, 248, 0.50)", + [theme.breakpoints.down("md")]: { + backgroundColor: theme.custom.colors.white, + }, +}) + +const UnitCardContent = styled.div({ + display: "flex", + flexDirection: "column", + flexGrow: 1, + width: "100%", +}) + +const LogoContainer = styled.div({ + padding: "40px 32px", + backgroundColor: theme.custom.colors.white, + [theme.breakpoints.down("md")]: { + padding: "34px 0 14px", + ".MuiSkeleton-root": { + margin: "0 auto", + }, + }, +}) + +const UnitLogo = styled.img({ + height: "50px", + display: "block", + [theme.breakpoints.down("md")]: { + height: "40px", + margin: "0 auto", + }, +}) + +const CardBottom = styled.div({ + padding: "24px", + borderTop: `1px solid ${theme.custom.colors.lightGray2}`, + display: "flex", + flexGrow: 1, + flexDirection: "column", + gap: "24px", + [theme.breakpoints.down("md")]: { + padding: "16px", + gap: "10px", + borderTop: "none", + }, +}) + +const ValuePropContainer = styled.div({ + display: "flex", + flexDirection: "column", + alignItems: "flex-start", + justifyContent: "flex-start", + flexGrow: 1, + paddingBottom: "16px", +}) + +const LoadingContent = styled.div({ + padding: "24px", +}) + +const HeadingText = styled(Typography)(({ theme }) => ({ + alignSelf: "stretch", + color: theme.custom.colors.darkGray2, + ...theme.typography.body2, + [theme.breakpoints.down("md")]: { + display: "none", + }, +})) + +const SubHeadingText = styled(HeadingText)(({ theme }) => ({ + alignSelf: "stretch", + color: theme.custom.colors.darkGray2, + ...theme.typography.body2, + display: "none", + [theme.breakpoints.down("md")]: { + display: "block", + }, +})) + +const CountsTextContainer = styled.div({ + display: "flex", + gap: "10px", + [theme.breakpoints.down("md")]: { + justifyContent: "flex-end", + }, +}) + +const CountsText = styled(Typography)(({ theme }) => ({ + color: theme.custom.colors.darkGray2, + ...theme.typography.body2, + [theme.breakpoints.down("md")]: { + ...theme.typography.body3, + color: theme.custom.colors.silverGrayDark, + }, +})) + +const unitLogos = { + [OfferedByEnum.Mitx]: "/static/images/unit_logos/mitx.svg", + [OfferedByEnum.Ocw]: "/static/images/unit_logos/ocw.svg", + [OfferedByEnum.Bootcamps]: "/static/images/unit_logos/bootcamps.svg", + [OfferedByEnum.Xpro]: "/static/images/unit_logos/xpro.svg", + [OfferedByEnum.Mitpe]: "/static/images/unit_logos/mitpe.svg", + [OfferedByEnum.See]: "/static/images/unit_logos/see.svg", +} + +interface UnitCardsProps { + units: LearningResourceOfferorDetail[] | undefined + courseCounts: Record + programCounts: Record +} + +interface UnitCardProps { + unit: LearningResourceOfferorDetail + logo: string + courseCount: number + programCount: number +} + +const UnitCard: React.FC = (props) => { + const { unit, logo, courseCount, programCount } = props + const channelDetailQuery = useChannelDetail("unit", unit.code) + const channelDetail = channelDetailQuery.data + const unitUrl = channelDetail?.channel_url + + return channelDetailQuery.isLoading ? ( + + ) : ( + + + + + + + + + + + {channelDetail?.configuration?.heading} + + + {channelDetail?.configuration?.sub_heading} + + + + + {courseCount > 0 ? `Courses: ${courseCount}` : ""} + + + {programCount > 0 ? `Programs: ${programCount}` : ""} + + + + + + + + ) +} + +export const UnitCardLoading = () => { + return ( + + + + + + + + + + + + + + + ) +} + +export const UnitCards: React.FC = (props) => { + const { units, courseCounts, programCounts } = props + return ( + <> + {units?.map((unit) => { + const courseCount = courseCounts[unit.code] || 0 + const programCount = programCounts[unit.code] || 0 + const logo = + unitLogos[unit.code as OfferedByEnum] || + `/static/images/unit_logos/${unit.code}.svg` + return unit.value_prop ? ( + + ) : null + })} + + ) +} diff --git a/frontends/mit-open/src/pages/UnitsListingPage/UnitsListingPage.tsx b/frontends/mit-open/src/pages/UnitsListingPage/UnitsListingPage.tsx index ff376a8258..c349ab8adf 100644 --- a/frontends/mit-open/src/pages/UnitsListingPage/UnitsListingPage.tsx +++ b/frontends/mit-open/src/pages/UnitsListingPage/UnitsListingPage.tsx @@ -1,26 +1,24 @@ +import React from "react" import { useLearningResourcesSearch, useOfferorsList, } from "api/hooks/learningResources" import { Banner, - Card, Container, - Skeleton, Typography, styled, Breadcrumbs, + theme, } from "ol-components" import { RiBookOpenLine, RiSuitcaseLine } from "@remixicon/react" -import React from "react" import { LearningResourceOfferorDetail, LearningResourcesSearchResponse, - OfferedByEnum, } from "api" import { MetaTags } from "ol-utilities" -import { useChannelDetail } from "api/hooks/channels" import { HOME } from "@/common/urls" +import { UnitCards, UnitCardLoading } from "./UnitCard" const UNITS_BANNER_IMAGE = "/static/images/background_steps.jpeg" const DESKTOP_WIDTH = "1056px" @@ -61,7 +59,7 @@ const PageContent = styled.div(({ theme }) => ({ flexDirection: "column", alignItems: "center", padding: "40px 10px 80px 10px", - gap: "80px", + gap: "48px", [theme.breakpoints.down("md")]: { padding: "40px 0px 30px 0px", gap: "40px", @@ -84,6 +82,15 @@ const PageHeaderContainerInner = styled.div({ display: "flex", flexDirection: "column", maxWidth: "1000px", + border: `1px solid ${theme.custom.colors.lightGray2}`, + backgroundColor: theme.custom.colors.white, + borderRadius: "8px", + padding: "32px", + [theme.breakpoints.down("md")]: { + backgroundColor: "transparent", + border: "none", + padding: "0", + }, }) const PageHeaderText = styled(Typography)(({ theme }) => ({ @@ -91,10 +98,6 @@ const PageHeaderText = styled(Typography)(({ theme }) => ({ ...theme.typography.subtitle1, })) -const CardStyled = styled(Card)({ - height: "100%", -}) - const UnitContainer = styled.div(({ theme }) => ({ display: "flex", flexDirection: "column", @@ -146,77 +149,12 @@ const GridContainer = styled.div(({ theme }) => ({ display: "grid", gap: "25px", gridTemplateColumns: "repeat(2, 1fr)", + width: "100%", [theme.breakpoints.down("md")]: { gridTemplateColumns: "1fr", }, })) -const UnitCardContainer = styled.div({ - display: "flex", - flexDirection: "column", - alignItems: "center", - padding: "16px", - height: "100%", -}) - -const UnitCardContent = styled.div({ - display: "flex", - flexDirection: "column", - flexGrow: 1, -}) - -const LogoContainer = styled.div({ - display: "flex", - justifyContent: "center", - alignItems: "center", - height: "128px", -}) - -const UnitLogo = styled.img({ - display: "flex", - flexDirection: "column", - justifyContent: "center", - alignItems: "center", - height: "50px", - maxWidth: "100%", -}) - -const ValuePropContainer = styled.div({ - display: "flex", - flexDirection: "column", - alignItems: "flex-start", - justifyContent: "flex-start", - flexGrow: 1, - paddingBottom: "16px", -}) - -const ValuePropText = styled(Typography)(({ theme }) => ({ - alignSelf: "stretch", - color: theme.custom.colors.darkGray2, - ...theme.typography.body2, -})) - -const CountsTextContainer = styled.div({ - display: "flex", - justifyContent: "flex-end", - gap: "10px", -}) - -const CountsText = styled(Typography)(({ theme }) => ({ - textAlign: "center", - color: theme.custom.colors.silverGrayDark, - ...theme.typography.body3, -})) - -const unitLogos = { - [OfferedByEnum.Mitx]: "/static/images/unit_logos/mitx.svg", - [OfferedByEnum.Ocw]: "/static/images/unit_logos/ocw.svg", - [OfferedByEnum.Bootcamps]: "/static/images/unit_logos/bootcamps.svg", - [OfferedByEnum.Xpro]: "/static/images/unit_logos/xpro.svg", - [OfferedByEnum.Mitpe]: "/static/images/unit_logos/mitpe.svg", - [OfferedByEnum.See]: "/static/images/unit_logos/see.svg", -} - interface UnitSectionProps { id: string icon: React.ReactNode @@ -267,95 +205,6 @@ const UnitSection: React.FC = (props) => { ) } -interface UnitCardsProps { - units: LearningResourceOfferorDetail[] | undefined - courseCounts: Record - programCounts: Record -} - -const UnitCards: React.FC = (props) => { - const { units, courseCounts, programCounts } = props - return ( - <> - {units?.map((unit) => { - const courseCount = courseCounts[unit.code] || 0 - const programCount = programCounts[unit.code] || 0 - const logo = - unitLogos[unit.code as OfferedByEnum] || - `/static/images/unit_logos/${unit.code}.svg` - return unit.value_prop ? ( - - ) : null - })} - - ) -} - -interface UnitCardProps { - unit: LearningResourceOfferorDetail - logo: string - courseCount: number - programCount: number -} - -const UnitCard: React.FC = (props) => { - const { unit, logo, courseCount, programCount } = props - const channelDetailQuery = useChannelDetail("unit", unit.code) - const channelDetail = channelDetailQuery.data - const unitUrl = channelDetail?.channel_url - return channelDetailQuery.isLoading ? ( - - ) : ( - - - - - - - - - {unit.value_prop} - - - - {courseCount > 0 ? `Courses: ${courseCount}` : ""} - - - {programCount > 0 ? `Programs: ${programCount}` : ""} - - - - - - - ) -} - -const UnitCardLoading = () => { - return ( - - - - - - - - - - - - - - - ) -} - const UnitsListingPage: React.FC = () => { const unitsQuery = useOfferorsList() const units = unitsQuery.data?.results