Skip to content
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
8 changes: 8 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
Release Notes
=============

Version 0.64.5
--------------

- fix: remove the breadcrum sepator style (#3249)
- feat: implement the video series page design (#3235)
- cache "query embeddings" via litellm (#3240)
- bump sentry (#3243)

Version 0.64.4 (Released April 27, 2026)
--------------

Expand Down
2 changes: 1 addition & 1 deletion frontends/main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"@radix-ui/react-popover": "^1.1.15",
"@react-pdf/renderer": "^4.3.0",
"@remixicon/react": "^4.2.0",
"@sentry/nextjs": "^10.0.0",
"@sentry/nextjs": "^10.50.0",
"@tanstack/react-query": "^5.66.0",
"@tiptap/core": "^3.13.0",
"@tiptap/extension-document": "^3.13.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import React from "react"
import Image from "next/image"
import Link from "next/link"
import { styled } from "ol-components"
import { ButtonLink } from "@mitodl/smoot-design"
import VideoContainer from "./VideoContainer"
import { RiPlayFill } from "@remixicon/react"
import { formatDurationClockTime } from "ol-utilities"
import type { VideoResource } from "api/v1"

Expand All @@ -25,7 +27,9 @@ const FeaturedGrid = styled.div(({ theme }) => ({
},
}))

const ImageWrapper = styled(Link)({
const ImageWrapper = styled(Link, {
shouldForwardProp: (prop) => prop !== "$isSeries",
})<{ $isSeries?: boolean }>(({ theme, $isSeries }) => ({
position: "relative",
width: "100%",
aspectRatio: "16/9",
Expand All @@ -34,6 +38,42 @@ const ImageWrapper = styled(Link)({
cursor: "pointer",
display: "block",
textDecoration: "none",
"& .play-overlay": {
opacity: 1,
transform: "scale(1)",
transition: "opacity 0.3s ease, transform 0.3s ease",
},
"&:hover .play-overlay": {
opacity: 0.75,
transform: "scale(1.12)",
},
...($isSeries && {
[theme.breakpoints.down("sm")]: {
height: "270px",
aspectRatio: "auto",
marginBottom: "24px",
},
}),
}))

const PlayOverlay = styled.div({
position: "absolute",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 1,
pointerEvents: "none",
})

const PlayCircle = styled.div({
width: "92px",
height: "92px",
borderRadius: "50%",
backgroundColor: "rgba(255, 255, 255, 0.50)",
display: "flex",
alignItems: "center",
justifyContent: "center",
})

const DurationBadge = styled.span(({ theme }) => ({
Expand All @@ -55,6 +95,13 @@ const TextSide = styled.div(({ theme }) => ({
},
}))

const ButtonSide = styled.div({
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
})

const FeaturedTitle = styled.h2(({ theme }) => ({
...theme.typography.h1,
fontWeight: theme.typography.fontWeightBold,
Expand Down Expand Up @@ -106,12 +153,37 @@ const FeaturedDescription = styled.p(({ theme }) => ({
},
}))

const DurationText = styled.span(({ theme }) => ({
...theme.typography.body3,
color: theme.custom.colors.darkGray1,
marginTop: "16px",
}))

const StyledButton = styled(ButtonLink)(({ theme }) => ({
width: "220px",
...theme.typography.body1,
padding: "12px 24px 12px 16px",
lineHeight: "16px",
height: "48px",
boxShadow:
"0 2px 4px 0 rgba(37, 38, 43, 0.10), 0 3px 8px 0 rgba(37, 38, 43, 0.12)",
}))

type FeaturedVideoProps = {
video: VideoResource
href: string
isSeries?: boolean
totalVideos?: number
totalTime?: string
}

const FeaturedVideo: React.FC<FeaturedVideoProps> = ({ video, href }) => {
const FeaturedVideo: React.FC<FeaturedVideoProps> = ({
video,
href,
isSeries = false,
totalVideos,
totalTime,
}) => {
const imageUrl = video.image?.url ?? null
const duration = video.video?.duration
? formatDurationClockTime(video.video.duration)
Expand All @@ -122,31 +194,64 @@ const FeaturedVideo: React.FC<FeaturedVideoProps> = ({ video, href }) => {
<Section>
<VideoContainer>
<FeaturedGrid>
<ImageWrapper href={href} aria-label={`Play ${video.title}`}>
<ImageWrapper
href={href}
aria-label={`Play ${video.title}`}
$isSeries={isSeries}
>
<Image
src={imageUrl ?? PLACEHOLDER_IMG}
alt={video.title}
fill
sizes="(max-width: 600px) 100vw, 50vw"
style={{ objectFit: "cover" }}
style={{ objectFit: isSeries ? "none" : "cover" }}
/>
{duration && <DurationBadge>{duration}</DurationBadge>}
<PlayOverlay className="play-overlay">
<PlayCircle>
<RiPlayFill size={44} color="#fff" />
</PlayCircle>
</PlayOverlay>
{!isSeries && duration && <DurationBadge>{duration}</DurationBadge>}
{isSeries && totalVideos && totalTime && (
<DurationBadge>
{totalVideos} videos • {totalTime}
</DurationBadge>
)}
</ImageWrapper>

<TextSide>
<FeaturedTitle>
<Link
href={href}
style={{ color: "inherit", textDecoration: "none" }}
>
<span className="desktop-title">{video.title}</span>
<span className="mobile-title">{video.title}</span>
</Link>
</FeaturedTitle>
{description && (
<FeaturedDescription>{description}</FeaturedDescription>
)}
</TextSide>
{!isSeries ? (
<TextSide>
<FeaturedTitle>
<Link
href={href}
style={{ color: "inherit", textDecoration: "none" }}
>
<span className="desktop-title">{video.title}</span>
<span className="mobile-title">{video.title}</span>
</Link>
</FeaturedTitle>
{description && (
<FeaturedDescription>{description}</FeaturedDescription>
)}
</TextSide>
) : (
<>
<ButtonSide>
<StyledButton
href={href}
variant="primary"
startIcon={<RiPlayFill />}
>
Start watching
</StyledButton>
{totalTime && totalVideos && (
<DurationText>
{totalTime} • {totalVideos} videos
</DurationText>
)}
</ButtonSide>
</>
)}
</FeaturedGrid>
</VideoContainer>
</Section>
Expand Down
Loading
Loading