diff --git a/packages/shared/src/components/cards/highlight/HighlightCards.spec.tsx b/packages/shared/src/components/cards/highlight/HighlightCards.spec.tsx index 605cadf9ddf..5ed58fae2cd 100644 --- a/packages/shared/src/components/cards/highlight/HighlightCards.spec.tsx +++ b/packages/shared/src/components/cards/highlight/HighlightCards.spec.tsx @@ -1,6 +1,15 @@ +import type { ReactElement } from 'react'; import React from 'react'; -import { render, screen } from '@testing-library/react'; +import { QueryClient } from '@tanstack/react-query'; +import { act, fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { TestBootProvider } from '../../../../__tests__/helpers/boot'; +import loggedUser from '../../../../__tests__/fixture/loggedUser'; +import { gqlClient } from '../../../graphql/common'; +import { LogEvent, Origin, TargetType } from '../../../lib/log'; +import { ReferralCampaignKey } from '../../../lib/referral'; +import { ShareProvider } from '../../../lib/share'; +import type { LoggedUser } from '../../../lib/user'; import { HighlightGrid } from './HighlightGrid'; import { HighlightList } from './HighlightList'; @@ -16,7 +25,7 @@ const highlights = [ highlightedAt: '2026-04-05T09:00:00.000Z', post: { id: 'post-1', - commentsPermalink: '/posts/post-1', + commentsPermalink: 'https://app.daily.dev/posts/post-1', }, }, { @@ -26,14 +35,29 @@ const highlights = [ highlightedAt: '2026-04-05T08:00:00.000Z', post: { id: 'post-2', - commentsPermalink: '/posts/post-2', + commentsPermalink: 'https://app.daily.dev/posts/post-2', }, }, ]; +const renderCard = ( + card: ReactElement, + logEvent = jest.fn(), + user?: LoggedUser, +) => + render( + + {card} + , + ); + describe('Highlight cards', () => { it('should render the grid card with highlight links', () => { - render(); + renderCard(); expect(screen.getByText('Happening Now')).toBeInTheDocument(); expect(screen.getByText('The first highlight')).toBeInTheDocument(); @@ -55,7 +79,7 @@ describe('Highlight cards', () => { }); it('should render the list card with highlight links', () => { - render(); + renderCard(); expect(screen.getByText('The first highlight')).toBeInTheDocument(); expect(screen.getByText('The second highlight')).toBeInTheDocument(); @@ -66,7 +90,7 @@ describe('Highlight cards', () => { const onHighlightClick = jest.fn(); const onReadAllClick = jest.fn(); - render( + renderCard( { expect(onReadAllClick).toHaveBeenCalledTimes(1); }); }); + +describe('Highlight card share controls', () => { + const writeText = jest.fn().mockResolvedValue(undefined); + + beforeAll(() => { + Object.assign(navigator, { clipboard: { writeText } }); + }); + + beforeEach(() => { + writeText.mockClear(); + }); + + const renderShareable = (logEvent: jest.Mock, onHighlightClick?: jest.Mock) => + renderCard( + , + logEvent, + ); + + it('copies a highlight from its row without opening it', async () => { + const logEvent = jest.fn(); + const onHighlightClick = jest.fn(); + renderShareable(logEvent, onHighlightClick); + // The header's page link comes first, then one per row. + const [, firstRow] = screen.getAllByRole('button', { name: 'Copy link' }); + + await act(async () => { + fireEvent.click(firstRow); + }); + + expect(onHighlightClick).not.toHaveBeenCalled(); + // The post, not a deep link into the page: one highlight shares one link + // wherever it is copied from. + expect(writeText).toHaveBeenCalledWith( + 'https://app.daily.dev/posts/post-1', + ); + const [[event]] = logEvent.mock.calls; + expect(event).toMatchObject({ + event_name: LogEvent.SharePost, + target_id: 'post-1', + target_type: TargetType.Post, + }); + expect(JSON.parse(event.extra)).toEqual({ + provider: ShareProvider.CopyLink, + origin: Origin.HighlightsCard, + highlight_id: 'highlight-1', + }); + }); + + it('copies the page as an absolute link from the header', async () => { + const logEvent = jest.fn(); + renderShareable(logEvent); + const [header] = screen.getAllByRole('button', { name: 'Copy link' }); + + await act(async () => { + fireEvent.click(header); + }); + + // `webappUrl` is a bare `/` on the webapp, which pasted as a path. + expect(writeText).toHaveBeenCalledWith('http://localhost/highlights'); + const [[event]] = logEvent.mock.calls; + expect(event.event_name).toBe(LogEvent.ShareHighlights); + expect(event.target_id).toBeUndefined(); + expect(JSON.parse(event.extra)).toEqual({ + provider: ShareProvider.CopyLink, + origin: Origin.HighlightsCard, + }); + }); + + describe('once the short link resolves', () => { + const items: Record>[] = []; + + beforeEach(() => { + items.length = 0; + Object.assign(globalThis, { + ClipboardItem: class { + constructor(data: Record>) { + items.push(data); + } + }, + }); + Object.assign(navigator.clipboard, { + write: jest.fn().mockResolvedValue(undefined), + }); + // An unreachable shortener leaves the tracked long link in place. + jest.spyOn(gqlClient, 'request').mockRejectedValue(new Error('offline')); + }); + + afterEach(() => { + delete (globalThis as { ClipboardItem?: unknown }).ClipboardItem; + jest.mocked(gqlClient.request).mockRestore(); + }); + + const readSwappedLink = async (): Promise => { + const blob = await items[0]['text/plain']; + const text = await new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.readAsText(blob); + }); + + return new URL(text); + }; + + it.each([ + [ + 'the page', + 0, + 'http://localhost/highlights', + ReferralCampaignKey.ShareHighlights, + ], + [ + 'a row', + 1, + 'https://app.daily.dev/posts/post-1', + ReferralCampaignKey.SharePost, + ], + ])('tracks %s link to the sharer', async (_, index, expected, cid) => { + renderCard(, jest.fn(), { + ...loggedUser, + id: 'sharer', + }); + + await act(async () => { + fireEvent.click( + screen.getAllByRole('button', { name: 'Copy link' })[index], + ); + }); + + const link = await readSwappedLink(); + expect(`${link.origin}${link.pathname}`).toBe(expected); + expect(link.searchParams.get('cid')).toBe(cid); + expect(link.searchParams.get('userid')).toBe('sharer'); + }); + }); +}); diff --git a/packages/shared/src/components/cards/highlight/HighlightPostSidebarWidget.tsx b/packages/shared/src/components/cards/highlight/HighlightPostSidebarWidget.tsx index 8717308efd0..94d56ad6902 100644 --- a/packages/shared/src/components/cards/highlight/HighlightPostSidebarWidget.tsx +++ b/packages/shared/src/components/cards/highlight/HighlightPostSidebarWidget.tsx @@ -3,7 +3,8 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import classNames from 'classnames'; import { useQuery } from '@tanstack/react-query'; import { WidgetContainer } from '../../widgets/common'; -import { getHighlightsUrl, highlightsTitleGradientClassName } from './common'; +import { highlightsTitleGradientClassName } from './common'; +import { getHighlightsUrl } from '../../../lib/links'; import { majorHeadlinesQueryOptions, type PostHighlight, diff --git a/packages/shared/src/components/cards/highlight/common.tsx b/packages/shared/src/components/cards/highlight/common.tsx index e7c0806c6f2..d4c7c743981 100644 --- a/packages/shared/src/components/cards/highlight/common.tsx +++ b/packages/shared/src/components/cards/highlight/common.tsx @@ -2,9 +2,12 @@ import type { ReactElement } from 'react'; import React from 'react'; import classNames from 'classnames'; import type { PostHighlight } from '../../../graphql/highlights'; -import { webappUrl } from '../../../lib/constants'; +import { getHighlightsUrl } from '../../../lib/links'; import { RelativeTime } from '../../utilities/RelativeTime'; import Link from '../../utilities/Link'; +import { ButtonSize } from '../../buttons/common'; +import { CopyHighlightsLink } from '../../highlights/CopyHighlightsLink'; +import { Origin } from '../../../lib/log'; import { HighlightCardOptions } from './HighlightCardOptions'; export interface HighlightCardProps { @@ -16,11 +19,6 @@ export interface HighlightCardProps { export const highlightsTitleGradientClassName = 'feed-highlights-title-gradient'; -const HIGHLIGHTS_URL = `${webappUrl}highlights`; - -export const getHighlightsUrl = (highlightId?: string): string => - highlightId ? `${HIGHLIGHTS_URL}?highlight=${highlightId}` : HIGHLIGHTS_URL; - const getHighlightUrl = (highlight: PostHighlight): string => getHighlightsUrl(highlight.id); @@ -73,18 +71,26 @@ const HighlightRow = ({ return ( onHighlightClick?.(highlight, index + 1)} > {highlight.headline} - + + + + ); @@ -118,7 +124,15 @@ export const HighlightCardContent = ({ > Happening Now - + +
{highlights.map((highlight, index) => ( diff --git a/packages/shared/src/components/highlights/CopyHighlightsLink.tsx b/packages/shared/src/components/highlights/CopyHighlightsLink.tsx new file mode 100644 index 00000000000..9f84294f769 --- /dev/null +++ b/packages/shared/src/components/highlights/CopyHighlightsLink.tsx @@ -0,0 +1,63 @@ +import type { MouseEvent, ReactElement } from 'react'; +import React from 'react'; +import { Button, ButtonSize, ButtonVariant } from '../buttons/Button'; +import { LinkIcon } from '../icons/Link'; +import { CopyStateIcon } from '../share/CopyStateIcon'; +import { Tooltip } from '../tooltip/Tooltip'; +import { useCopyLink } from '../../hooks/useCopy'; +import type { PostHighlight } from '../../graphql/highlights'; +import type { Origin } from '../../lib/log'; +import { getHighlightsShareUrl } from '../../lib/links'; +import { ReferralCampaignKey } from '../../lib/referral'; +import { ShareProvider } from '../../lib/share'; +import { useLogHighlightShare } from '../../features/snapshot/useLogHighlightShare'; + +export function CopyHighlightsLink({ + highlight, + origin, + className, + size = ButtonSize.Small, +}: { + /** Links to this highlight's post, or to the page without one. */ + highlight?: Pick; + origin: Origin; + className?: string; + size?: ButtonSize; +}): ReactElement { + const [copied, copyLink] = useCopyLink(); + const logShare = useLogHighlightShare(origin, highlight); + + return ( + + {expanded && tldr && (
-

{tldr}

- - - Read more - - +

+ {tldr} +

+
+ + + Read more + + + +
)} diff --git a/packages/shared/src/components/highlights/HighlightsPage.tsx b/packages/shared/src/components/highlights/HighlightsPage.tsx index eb11f9e7566..d4d1b5ca4b3 100644 --- a/packages/shared/src/components/highlights/HighlightsPage.tsx +++ b/packages/shared/src/components/highlights/HighlightsPage.tsx @@ -11,7 +11,9 @@ import { highlightsPageQueryOptions, postHighlightsFeedQueryOptions, } from '../../graphql/highlights'; +import { Origin } from '../../lib/log'; import { Tab, TabContainer } from '../tabs/TabContainer'; +import { CopyHighlightsLink } from './CopyHighlightsLink'; import { DigestCTA } from './DigestCTA'; import { HighlightItem } from './HighlightItem'; @@ -177,6 +179,7 @@ export const HighlightsPage = (): ReactElement => {

Happening Now

+ cheese -> avocado. A still frame has to pick a position, and + * the yellow-to-green end is the one the brand shots use. + */ +const HIGHLIGHTS_EYEBROW_GRADIENT = `linear-gradient(120deg, ${colors.cheese['40']} 0%, ${colors.avocado['10']} 52%, ${colors.avocado['40']} 100%)`; + +const HappeningNowEyebrow = (): ReactElement => ( + +); + +/** + * Copy link and Snapshot for an expanded highlight, plus the quote bar over + * its TLDR. Mounts only once a row expands, so collapsed rows run none of it. + */ +export function HighlightShareActions({ + highlight, + tldr, + tldrRef, + source, +}: { + highlight: PostHighlightFeed; + tldr: string; + tldrRef: RefObject; + /** Who wrote the TLDR, credited on both cards. */ + source?: { name: string; image?: string }; +}): ReactElement { + const cardRef = useRef(null); + const { isArmed, armProps } = useArmedCard(); + const logShare = useLogHighlightShare( + Origin.HappeningNowHighlight, + highlight, + ); + const logSelectionShare = useLogHighlightShare( + Origin.HappeningNowSelection, + highlight, + ); + + const onSnapshot = useCallback( + (result: SnapshotResult) => logShare(ShareProvider.Snapshot, result), + [logShare], + ); + + return ( + <> + + + getSnapshotCaptureOptions(cardRef.current)} + filename={`daily-highlight-${highlight.id}`} + onResult={onSnapshot} + showLabel={false} + target={cardRef} + /> + + {isArmed && ( +
+ } + passage={tldr} + ref={cardRef} + seed={highlight.id} + source={source} + /> +
+ )} + } + link={highlight.post.commentsPermalink} + onShare={logSelectionShare} + seed={highlight.id} + source={source} + /> + + ); +} diff --git a/packages/shared/src/features/snapshot/SelectionSnapshotBar.tsx b/packages/shared/src/features/snapshot/SelectionSnapshotBar.tsx index 45465f13098..59877446d0f 100644 --- a/packages/shared/src/features/snapshot/SelectionSnapshotBar.tsx +++ b/packages/shared/src/features/snapshot/SelectionSnapshotBar.tsx @@ -1,4 +1,4 @@ -import type { ReactElement, RefObject } from 'react'; +import type { ReactElement, ReactNode, RefObject } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { @@ -8,6 +8,7 @@ import { } from '../../components/buttons/Button'; import { CopyIcon, LinkIcon } from '../../components/icons'; import { CopyStateIcon } from '../../components/share/CopyStateIcon'; +import type { SnapshotResult } from '../../components/imageShare/SnapshotButton'; import { SnapshotButton } from '../../components/imageShare/SnapshotButton'; import { Tooltip } from '../../components/tooltip/Tooltip'; import { useCopyText } from '../../hooks/useCopy'; @@ -23,7 +24,6 @@ import { getSnapshotCaptureOptions } from './snapshotCapture'; import { snapshotSource } from './snapshotSource'; import type { TextSelection } from './useTextSelection'; import { useTextSelection } from './useTextSelection'; -import { useLogSnapshot } from './useLogSnapshot'; const BAR_HEIGHT = 44; const GAP = 8; @@ -53,13 +53,27 @@ const position = (selection: TextSelection) => { }; }; -export function SelectionSnapshotBar({ - post, - containerRef, -}: { - post: Post; +export interface SelectionShareBarProps { containerRef: RefObject; -}): ReactElement | null { + /** The post permalink a copied link points at. */ + link: string; + /** Seeds the card's gradient and names the downloaded file. */ + seed: string; + source?: { name: string; image?: string }; + /** The surface's own label on the card's logo row. */ + label?: ReactNode; + /** Called once per action, with how a snapshot ended, so the host logs it. */ + onShare: (provider: ShareProvider, result?: SnapshotResult) => void; +} + +export function SelectionShareBar({ + containerRef, + link, + seed, + source, + label, + onShare, +}: SelectionShareBarProps): ReactElement | null { const barRef = useRef(null); const cardRef = useRef(null); const selection = useTextSelection(containerRef, true, barRef); @@ -68,39 +82,23 @@ export function SelectionSnapshotBar({ const [quote, setQuote] = useState(null); const [linkCopied, copyLink] = useCopyPostLink(); const [textCopied, copyText] = useCopyText(quote?.text); - const { logEvent } = useLogContext(); const onCopyLink = useCallback(() => { - logEvent( - postLogEvent(LogEvent.SharePost, post, { - extra: { - provider: ShareProvider.CopyLink, - origin: Origin.TextSelection, - }, - }), - ); + onShare(ShareProvider.CopyLink); // `shorten`, not an awaited short URL: the write has to stay inside the // task that handled the click or Safari refuses it. - copyLink({ - link: post.commentsPermalink, - shorten: true, - cid: ReferralCampaignKey.SharePost, - }); - }, [copyLink, logEvent, post]); + copyLink({ link, shorten: true, cid: ReferralCampaignKey.SharePost }); + }, [copyLink, link, onShare]); const onCopyText = useCallback(() => { - logEvent( - postLogEvent(LogEvent.SharePost, post, { - extra: { - provider: ShareProvider.CopyText, - origin: Origin.TextSelection, - }, - }), - ); + onShare(ShareProvider.CopyText); copyText({ message: '✅ Copied text' }); - }, [copyText, logEvent, post]); + }, [copyText, onShare]); - const logSnapshot = useLogSnapshot(post, Origin.TextSelection); + const onSnapshot = useCallback( + (result: SnapshotResult) => onShare(ShareProvider.Snapshot, result), + [onShare], + ); useEffect(() => { if (selection) { @@ -132,9 +130,9 @@ export function SelectionSnapshotBar({ {/* Snapshot leads, labelled and solid: it is the reason the bar exists, and the two copies beside it are the familiar fallbacks. */} getSnapshotCaptureOptions(cardRef.current)} - filename={`daily-quote-${post.id}`} + filename={`daily-quote-${seed}`} target={cardRef} variant={ButtonVariant.Primary} /> @@ -169,12 +167,47 @@ export function SelectionSnapshotBar({
, document.body, ); } + +export function SelectionSnapshotBar({ + post, + containerRef, +}: { + post: Post; + containerRef: RefObject; +}): ReactElement { + const { logEvent } = useLogContext(); + + const onShare = useCallback( + (provider: ShareProvider, result?: SnapshotResult) => + logEvent( + postLogEvent(LogEvent.SharePost, post, { + extra: { + provider, + origin: Origin.TextSelection, + ...(result && { result }), + }, + }), + ), + [logEvent, post], + ); + + return ( + + ); +} diff --git a/packages/shared/src/features/snapshot/snapshotSource.ts b/packages/shared/src/features/snapshot/snapshotSource.ts index 5987db7c34b..a5822ff361d 100644 --- a/packages/shared/src/features/snapshot/snapshotSource.ts +++ b/packages/shared/src/features/snapshot/snapshotSource.ts @@ -1,4 +1,5 @@ import type { Post } from '../../graphql/posts'; +import type { Source } from '../../graphql/sources'; /** * The API's catch-all source for a link it could not attribute: handle and @@ -11,7 +12,7 @@ const UNKNOWN_SOURCE = 'unknown'; /** Who to credit on a share image, or nobody rather than a placeholder. */ export function snapshotSource( - post: Pick, + post: Pick & { source?: Pick }, ): { name: string; image?: string } | undefined { const { source, domain } = post; diff --git a/packages/shared/src/features/snapshot/useLogHighlightShare.ts b/packages/shared/src/features/snapshot/useLogHighlightShare.ts new file mode 100644 index 00000000000..6adb4a4f876 --- /dev/null +++ b/packages/shared/src/features/snapshot/useLogHighlightShare.ts @@ -0,0 +1,37 @@ +import { useCallback } from 'react'; +import type { SnapshotResult } from '../../components/imageShare/SnapshotButton'; +import { useLogContext } from '../../contexts/LogContext'; +import type { PostHighlight } from '../../graphql/highlights'; +import type { Origin } from '../../lib/log'; +import { LogEvent, TargetType } from '../../lib/log'; +import type { ShareProvider } from '../../lib/share'; + +/** + * A highlight points at a post, so sharing one is a `SharePost` on that post + * with the highlight id beside it, in the same shape as the post page's + * placements. Without a highlight the link is the Happening Now page itself, + * which has no post to target. + */ +export function useLogHighlightShare( + origin: Origin, + highlight?: Pick, +): (provider: ShareProvider, result?: SnapshotResult) => void { + const { logEvent } = useLogContext(); + const highlightId = highlight?.id; + const postId = highlight?.post.id; + + return useCallback( + (provider: ShareProvider, result?: SnapshotResult) => + logEvent({ + event_name: postId ? LogEvent.SharePost : LogEvent.ShareHighlights, + ...(postId && { target_id: postId, target_type: TargetType.Post }), + extra: JSON.stringify({ + provider, + origin, + ...(result && { result }), + ...(highlightId && { highlight_id: highlightId }), + }), + }), + [highlightId, logEvent, origin, postId], + ); +} diff --git a/packages/shared/src/graphql/highlights.ts b/packages/shared/src/graphql/highlights.ts index 3de88fddf36..60c083699d7 100644 --- a/packages/shared/src/graphql/highlights.ts +++ b/packages/shared/src/graphql/highlights.ts @@ -2,6 +2,7 @@ import { gql } from 'graphql-request'; import { gqlClient } from './common'; import type { Connection } from './common'; import type { PostHighlightSignificance } from './types'; +import type { Source } from './sources'; import { ONE_MINUTE } from '../lib/time'; export interface PostHighlight { @@ -15,6 +16,8 @@ export interface PostHighlight { }; } +type HighlightFeedSource = Pick; + export interface PostHighlightFeed { id: string; channel: string; @@ -27,9 +30,13 @@ export interface PostHighlightFeed { commentsPermalink: string; summary?: string; contentHtml?: string; + domain?: string; + source?: HighlightFeedSource; sharedPost?: { summary?: string; contentHtml?: string; + domain?: string; + source?: HighlightFeedSource; }; }; } @@ -118,9 +125,19 @@ export const POST_HIGHLIGHT_FEED_FRAGMENT = gql` commentsPermalink summary contentHtml + domain + source { + name + image + } sharedPost { summary contentHtml + domain + source { + name + image + } } } } diff --git a/packages/shared/src/lib/links.ts b/packages/shared/src/lib/links.ts index fb802ebe610..61f5cb39e4c 100644 --- a/packages/shared/src/lib/links.ts +++ b/packages/shared/src/lib/links.ts @@ -186,3 +186,21 @@ export const getRedirectNextPath = (params: URLSearchParams): string => { return checkIsExtension() ? `${webappUrl}${nextPath}` : nextPath; }; + +/* A function, not a constant: `webappUrl` comes from the environment, and a + module-level template literal captures it before a test can set it. */ +export const getHighlightsUrl = (highlightId?: string): string => { + const base = `${webappUrl}highlights`; + + return highlightId ? `${base}?highlight=${highlightId}` : base; +}; + +/** The Happening Now page as a link that still works once it leaves the tab. */ +export const getHighlightsShareUrl = (): string => { + const path = getHighlightsUrl(); + // `webappUrl` is a bare path on the webapp, and the share pipeline runs + // `new URL(link)` on whatever it is handed. + const origin = globalThis?.location?.origin; + + return origin ? new URL(path, origin).toString() : path; +}; diff --git a/packages/shared/src/lib/log.ts b/packages/shared/src/lib/log.ts index bf644bf2bb5..1487e2baabe 100644 --- a/packages/shared/src/lib/log.ts +++ b/packages/shared/src/lib/log.ts @@ -59,6 +59,10 @@ export enum Origin { PostParagraph = 'post paragraph', PollResults = 'poll results', PollVotePrompt = 'poll vote prompt', + HappeningNow = 'happening now', + HappeningNowHighlight = 'happening now highlight', + HappeningNowSelection = 'happening now selection', + HighlightsCard = 'highlights card', // snapshot placements - end History = 'history', FeedbackCard = 'feedback card', @@ -361,6 +365,7 @@ export enum LogEvent { ShareLog = 'share log', ShareWorld = 'share world', ShareTool = 'share tool', + ShareHighlights = 'share highlights', // End Share /* Start World `world view` is the denominator and fires whatever happens next, so the diff --git a/packages/shared/src/lib/referral.ts b/packages/shared/src/lib/referral.ts index 49ba9fbf329..447b901c0ed 100644 --- a/packages/shared/src/lib/referral.ts +++ b/packages/shared/src/lib/referral.ts @@ -8,4 +8,5 @@ export enum ReferralCampaignKey { ShareTag = 'share_tag', ShareAgent = 'share_agent', ShareSlack = 'share_slack', + ShareHighlights = 'share_highlights', } diff --git a/packages/storybook/stories/features/snapshot/surfaces/HappeningNow.stories.tsx b/packages/storybook/stories/features/snapshot/surfaces/HappeningNow.stories.tsx deleted file mode 100644 index 1bb42e7ef4b..00000000000 --- a/packages/storybook/stories/features/snapshot/surfaces/HappeningNow.stories.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import React from 'react'; -import type { Meta, StoryObj } from '@storybook/react-vite'; -import { ButtonVariant } from '@dailydotdev/shared/src/components/buttons/Button'; -import { ArrowIcon } from '@dailydotdev/shared/src/components/icons'; -import { IconSize } from '@dailydotdev/shared/src/components/Icon'; -import type { DeviceName } from '../surfaceChrome'; -import { - Category, - Control, - Device, - Rail, - SurfacePage, - Variant, -} from '../surfaceChrome'; - -const TABS = ['Major headlines', 'All highlights', 'AI', 'Web']; - -const HIGHLIGHTS = [ - { - headline: 'OpenAI ships a cheaper model tier', - time: '2h ago', - tldr: 'Priced at a third of the previous tier with the same context window. Existing keys work unchanged, and the older tier stays available until March.', - }, - { headline: 'React 20 drops the legacy render path', time: '4h ago' }, - { headline: 'Postgres 19 lands async I/O by default', time: '6h ago' }, - { headline: 'Cloudflare open-sources its edge router', time: '9h ago' }, -]; - -const HappeningScreen = ({ device }: { device: DeviceName }) => ( - -
-
- {/* feed-highlights-title-gradient in production. */} -

- Happening Now -

- -
- -
- {TABS.map((tab, index) => ( - - {tab} - - ))} -
- - {HIGHLIGHTS.map((item, index) => { - const open = index === 0; - - return ( -
-
-
- - {item.headline} - - - {item.time} - -
- -
- - {open && item.tldr && ( -
-

{item.tldr}

-
- - Read more - - -
-
- )} -
- ); - })} -
-
-); - -const AllDevices = () => ( - - - - - -); - -const HappeningNow = () => ( - - - - - - - -); - -const meta: Meta = { - title: 'Features/Snapshot/Surfaces/Happening now', - component: HappeningNow, - parameters: { layout: 'fullscreen' }, -}; - -export default meta; - -export const Variations: StoryObj = {}; diff --git a/packages/storybook/stories/features/snapshot/surfaces/Overview.stories.tsx b/packages/storybook/stories/features/snapshot/surfaces/Overview.stories.tsx index f6ed7e190c9..424b66b466d 100644 --- a/packages/storybook/stories/features/snapshot/surfaces/Overview.stories.tsx +++ b/packages/storybook/stories/features/snapshot/surfaces/Overview.stories.tsx @@ -71,8 +71,8 @@ const PAGES: React.ReactNode[][] = [ ], [ 'Happening now', - '#6355', - 'Page, topic and highlight level — and what a page-level snapshot actually looks like at thumbnail size', + '#6570', + 'Shipped: the live Happening Now page is the reference, so it has no mockup here', ], [ 'Briefing', diff --git a/packages/webapp/pages/join/index.tsx b/packages/webapp/pages/join/index.tsx index d139df6df88..1710eb7dd1e 100644 --- a/packages/webapp/pages/join/index.tsx +++ b/packages/webapp/pages/join/index.tsx @@ -29,6 +29,7 @@ const componentsMap: ReferralRecord> = { [ReferralCampaignKey.ShareTag]: Referral, [ReferralCampaignKey.ShareAgent]: Referral, [ReferralCampaignKey.ShareSlack]: Referral, + [ReferralCampaignKey.ShareHighlights]: Referral, }; const referralCampaignValues = new Set(