Skip to content
Closed
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
17 changes: 17 additions & 0 deletions packages/shared/src/components/Feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import { FeedCardContext } from '../features/posts/FeedCardContext';
import {
briefCardFeedFeature,
briefFeedEntrypointPage,
dailyShowFeedCardFeature,
featureFeedAdTemplate,
} from '../lib/featureManagement';
import type { AwardProps } from '../graphql/njord';
Expand All @@ -73,6 +74,8 @@ import { BriefBannerFeed } from './cards/brief/BriefBanner/BriefBannerFeed';
import { ActionType } from '../graphql/actions';
import { TopHero } from './banners/HeroBottomBanner';
import { useReadingReminderFeedHero } from '../hooks/notifications/useReadingReminderFeedHero';
import { YoutubeLiveFeedCard } from './cards/common/YoutubeLiveFeedCard';
import { syntaxLiveMockPost } from '../lib/syntaxLiveMockPost';

const FeedErrorScreen = dynamic(
() => import(/* webpackChunkName: "feedErrorScreen" */ './FeedErrorScreen'),
Expand Down Expand Up @@ -210,6 +213,17 @@ export default function Feed<T>({
const isSquadFeed = feedName === OtherFeedPage.Squad;
const trackedFeedFinish = useRef(false);
const isMyFeed = feedName === SharedFeedPage.MyFeed;
const isDailyShowFeedCardEligible =
feedName === SharedFeedPage.MyFeed ||
feedName === SharedFeedPage.Popular ||
feedName === SharedFeedPage.Upvoted;
const { value: dailyShowFeedCardEnabled } = useConditionalFeature({
feature: dailyShowFeedCardFeature,
shouldEvaluate: isDailyShowFeedCardEligible,
});
const showDailyShowFeedCard =
isDailyShowFeedCardEligible && dailyShowFeedCardEnabled;
const dailyShowFeedCardIndex = 3;
const showAcquisitionForm =
isMyFeed &&
(routerQuery?.[acquisitionKey] as string)?.toLocaleLowerCase() === 'true' &&
Expand Down Expand Up @@ -655,6 +669,9 @@ export default function Feed<T>({
}}
/>
)}
{showDailyShowFeedCard && index === dailyShowFeedCardIndex && (
<YoutubeLiveFeedCard post={syntaxLiveMockPost} />
)}
{shouldShowInFeedHero && index === adjustedHeroInsertIndex && (
<div
style={{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
.card {
background-image: radial-gradient(
circle at 15% 15%,
color-mix(
in srgb,
var(--theme-accent-ketchup-default) 45%,
transparent
)
0%,
transparent 55%
),
radial-gradient(
circle at 85% 25%,
color-mix(in srgb, var(--theme-accent-bun-default) 40%, transparent) 0%,
transparent 60%
),
radial-gradient(
circle at 50% 100%,
color-mix(
in srgb,
var(--theme-accent-ketchup-bolder) 35%,
transparent
)
0%,
transparent 65%
),
linear-gradient(
135deg,
color-mix(in srgb, var(--theme-accent-ketchup-default) 18%, transparent),
color-mix(in srgb, var(--theme-accent-bun-default) 18%, transparent)
);
background-color: var(--theme-background-subtle);
background-size: 220% 220%, 200% 200%, 240% 240%, 200% 200%;
background-position: 0% 50%, 100% 0%, 50% 100%, 0% 50%;
animation: youtube-live-card-mesh 9s ease-in-out infinite;
}

@keyframes youtube-live-card-mesh {
0% {
background-position: 0% 50%, 100% 0%, 50% 100%, 0% 50%;
}
50% {
background-position: 100% 50%, 0% 100%, 50% 0%, 100% 50%;
}
100% {
background-position: 0% 50%, 100% 0%, 50% 100%, 0% 50%;
}
}

@media (prefers-reduced-motion: reduce) {
.card {
animation: none;
}
}
102 changes: 102 additions & 0 deletions packages/shared/src/components/cards/common/YoutubeLiveFeedCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { CSSProperties, MouseEvent, ReactElement } from 'react';
import React from 'react';
import classNames from 'classnames';
import Link from '../../utilities/Link';
import SourceButton from './SourceButton';
import { ProfileImageSize } from '../../ProfilePicture';
import { PlayIcon } from '../../icons';
import { IconSize } from '../../Icon';
import type { Post } from '../../../graphql/posts';
import { webappUrl } from '../../../lib/constants';
import { useFeedLayout } from '../../../hooks/useFeedLayout';
import styles from './YoutubeLiveFeedCard.module.css';

export type YoutubeLiveFeedCardProps = {
post: Post;
style?: CSSProperties;
className?: string;
onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
};

const previewVideoSrc = `${webappUrl}assets/videos/syntax-live-preview.webm`;
const previewPosterSrc = `${webappUrl}assets/videos/syntax-live-preview-poster.jpg`;

/**
* Card-shaped feed slot for the daily.dev show with a looping muted webm
* preview as background and bottom-aligned squad/title metadata.
*/
export function YoutubeLiveFeedCard({
post,
style,
className,
onClick,
}: YoutubeLiveFeedCardProps): ReactElement {
const href = post.commentsPermalink;
const { shouldUseListFeedLayout } = useFeedLayout();

const handleClick = (event: MouseEvent<HTMLAnchorElement>) => {
if (!onClick) {
return;
}
event.preventDefault();
onClick(event);
};

return (
<Link href={href} passHref>
<a
style={style}
onClick={handleClick}
data-testid="youtube-live-feed-card"
className={classNames(
'group relative flex h-full w-full flex-col items-start justify-end gap-4 overflow-hidden rounded-16 p-4 no-underline transition-shadow hover:shadow-2',
shouldUseListFeedLayout
? 'min-h-[17rem]'
: 'min-h-card max-h-cardLarge',
styles.card,
className,
)}
>
<video
className="pointer-events-none absolute inset-0 h-full w-full object-cover opacity-40"
src={previewVideoSrc}
poster={previewPosterSrc}
autoPlay
loop
muted
playsInline
preload="metadata"
aria-hidden
/>
<div className="absolute inset-0 bg-overlay-tertiary-black opacity-50" />
<div
className="pointer-events-none absolute inset-0 z-1 flex items-center justify-center"
aria-hidden
>
<span className="flex size-24 items-center justify-center rounded-full bg-black/60 text-white transition-transform group-hover:scale-110">
<PlayIcon secondary size={IconSize.XXLarge} />
</span>
</div>
<div className="relative z-1 flex w-full flex-col items-start gap-2">
<span className="block w-full break-words text-left font-bold text-white typo-title3">
{post.title}
</span>
<div className="flex items-center gap-2">
{post.source && (
<SourceButton
source={post.source}
size={ProfileImageSize.Large}
className="shrink-0"
/>
)}
{post.source?.name && (
<span className="font-bold text-raw-salt-50 typo-footnote">
{post.source.name}
</span>
)}
</div>
</div>
</a>
</Link>
);
}
5 changes: 5 additions & 0 deletions packages/shared/src/lib/featureManagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ export const boostSettingsFeature = new Feature('boost_settings', {

export const adImprovementsV3Feature = new Feature('ad_improvements_v3', false);

export const dailyShowFeedCardFeature = new Feature(
'daily_show_feed_card',
isDevelopment,
);

export const featureYearInReview = new Feature('year_in_review_2025', false);

export const featureProfileCompletionIndicator = new Feature(
Expand Down
51 changes: 51 additions & 0 deletions packages/shared/src/lib/syntaxLiveMockPost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { Post } from '../graphql/posts';
import { PostType, UserVote } from '../graphql/posts';
import { SourceType } from '../graphql/sources';
import { SYNTAX_LIVE_VIDEO_ID } from './youtubeLive';

/** The daily.dev Show squad source for livestream experiment mocks. */
export const syntaxSquadSource = {
handle: 'dailydevshow',
name: 'The daily.dev Show',
permalink: 'https://app.daily.dev/squads/dailydevshow',
id: '9836af88-07c6-41e0-a18d-f434aa64895c',
image:
'https://media.daily.dev/image/upload/s--4GpvCvpD--/f_auto/v1777382360/squads/9836af88-07c6-41e0-a18d-f434aa64895c',
type: SourceType.Squad,
active: true,
public: true,
membersCount: 1000,
};

/** YouTube video post pointing at the Syntax.fm test live `videoId` (feed + modal QA). */
export const syntaxLiveMockPost: Post = {
id: 'syntax-live-mock',
title: 'The roundup you actually need. S1E1: Goodbye Tim Apple.',
type: PostType.VideoYouTube,
videoId: SYNTAX_LIVE_VIDEO_ID,
createdAt: new Date().toISOString(),
image:
'https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/youtube-placeholder',
commentsPermalink: 'https://daily.dev/posts/syntax-live-mock',
permalink: `https://www.youtube.com/watch?v=${SYNTAX_LIVE_VIDEO_ID}`,
source: syntaxSquadSource,
author: {
id: 'joao-graca',
name: 'Joao Graca',
username: 'joaograca',
permalink: 'https://app.daily.dev/joaograca',
image: 'https://i.pravatar.cc/64?img=12',
},
tags: ['javascript'],
readTime: 0,
numComments: 0,
numUpvotes: 0,
bookmarked: false,
read: false,
upvoted: false,
commented: false,
userState: {
vote: UserVote.None,
flags: { feedbackDismiss: false },
},
} as Post;
2 changes: 2 additions & 0 deletions packages/shared/src/lib/youtubeLive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/** Canonical daily.dev Show test stream for the live card variation. */
export const SYNTAX_LIVE_VIDEO_ID = 'XKO67n3xfzM';
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading