From 7c0241edbf246adbefd70b4a1fa0cd57db83a673 Mon Sep 17 00:00:00 2001 From: idoshamun Date: Thu, 28 May 2026 09:08:28 +0000 Subject: [PATCH 1/2] feat(highlights): add 'All' tab to the highlights page Render a new "All" tab after Headlines that lists every highlight across channels, deduplicated by post and ordered by recency. The tab is wired to a new /highlights/all static page so it survives a hard refresh, and uses the new postHighlightsFeed GraphQL endpoint which accepts optional channel and significance filters. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/highlights/HighlightsPage.tsx | 49 +++++++++++- packages/shared/src/graphql/highlights.ts | 78 +++++++++++++++++++ packages/webapp/pages/highlights/all.tsx | 64 +++++++++++++++ 3 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 packages/webapp/pages/highlights/all.tsx diff --git a/packages/shared/src/components/highlights/HighlightsPage.tsx b/packages/shared/src/components/highlights/HighlightsPage.tsx index 784d9d1cd61..0602d052680 100644 --- a/packages/shared/src/components/highlights/HighlightsPage.tsx +++ b/packages/shared/src/components/highlights/HighlightsPage.tsx @@ -9,14 +9,17 @@ import type { import { channelHighlightsFeedQueryOptions, highlightsPageQueryOptions, + postHighlightsFeedQueryOptions, } from '../../graphql/highlights'; import { Tab, TabContainer } from '../tabs/TabContainer'; import { DigestCTA } from './DigestCTA'; import { HighlightItem } from './HighlightItem'; const MAJOR_HEADLINES_LABEL = 'Headlines'; +const ALL_HIGHLIGHTS_LABEL = 'All'; const SKELETON_COUNT = 5; const HIGHLIGHTS_BASE_URL = '/highlights'; +const ALL_HIGHLIGHTS_URL = `${HIGHLIGHTS_BASE_URL}/all`; const HighlightSkeleton = (): ReactElement => (
@@ -41,6 +44,12 @@ const useChannelHighlights = (channel: string | undefined) => enabled: !!channel, }); +const useAllHighlights = (enabled: boolean) => + useQuery({ + ...postHighlightsFeedQueryOptions(), + enabled, + }); + interface HighlightFeedListProps { highlights: PostHighlightFeed[]; loading: boolean; @@ -124,10 +133,34 @@ const ChannelTab = ({ ); }; +const AllHighlightsTab = ({ + active, + expandedId, +}: { + active: boolean; + expandedId?: string; +}): ReactElement => { + const { data, isFetching } = useAllHighlights(active); + const highlights = useMemo( + () => data?.postHighlightsFeed?.edges?.map((edge) => edge.node) ?? [], + [data], + ); + const loading = isFetching && !data; + + return ( + + ); +}; + export const HighlightsPage = (): ReactElement => { const router = useRouter(); const channel = getSingleQueryParam(router.query.channel); const expandedId = getSingleQueryParam(router.query.highlight); + const isAllTab = router.pathname === ALL_HIGHLIGHTS_URL; const { data, isFetching } = useQuery(highlightsPageQueryOptions()); const channels = data?.channelConfigurations ?? []; @@ -137,9 +170,16 @@ export const HighlightsPage = (): ReactElement => { ); const majorLoading = isFetching && !data; - const activeTab = - channels.find((c) => c.channel === channel)?.displayName ?? - MAJOR_HEADLINES_LABEL; + let activeTab: string; + if (isAllTab) { + activeTab = ALL_HIGHLIGHTS_LABEL; + } else if (channel) { + activeTab = + channels.find((c) => c.channel === channel)?.displayName ?? + MAJOR_HEADLINES_LABEL; + } else { + activeTab = MAJOR_HEADLINES_LABEL; + } return (
@@ -172,6 +212,9 @@ export const HighlightsPage = (): ReactElement => { expandedId={expandedId} /> , + + + , ...channels.map((ch) => ( ; +} + +interface PostHighlightsFeedQueryArgs { + channel?: string; + significance?: PostHighlightSignificance[]; + first?: number; + after?: string; +} + +export const POST_HIGHLIGHTS_FEED_PAGE_QUERY = gql` + query PostHighlightsFeedPage( + $channel: String + $significance: [String!] + $first: Int + $after: String + ) { + postHighlightsFeed( + channel: $channel + significance: $significance + first: $first + after: $after + ) { + pageInfo { + endCursor + hasNextPage + } + edges { + node { + ...PostHighlightFeedCard + } + } + } + } + ${POST_HIGHLIGHT_FEED_FRAGMENT} +`; + +export const getPostHighlightsFeedQueryKey = ({ + channel, + significance, +}: Pick) => [ + 'post-highlights-feed', + channel ?? '', + [...(significance ?? [])].sort().join(','), +]; + +export const postHighlightsFeedQueryOptions = ({ + channel, + significance, + first = MAJOR_HEADLINES_MAX_FIRST, + after, +}: PostHighlightsFeedQueryArgs = {}) => ({ + queryKey: [ + ...getPostHighlightsFeedQueryKey({ channel, significance }), + first, + ], + queryFn: () => + gqlClient.request( + POST_HIGHLIGHTS_FEED_PAGE_QUERY, + { + channel: channel ?? null, + significance: significance ?? null, + first, + after, + }, + ), + staleTime: ONE_MINUTE, +}); + export interface ChannelDigestConfiguration { frequency: string; source?: { diff --git a/packages/webapp/pages/highlights/all.tsx b/packages/webapp/pages/highlights/all.tsx new file mode 100644 index 00000000000..b6e4c2685b6 --- /dev/null +++ b/packages/webapp/pages/highlights/all.tsx @@ -0,0 +1,64 @@ +import type { GetStaticPropsResult } from 'next'; +import type { ReactElement } from 'react'; +import React from 'react'; +import type { DehydratedState } from '@tanstack/react-query'; +import { dehydrate, QueryClient } from '@tanstack/react-query'; +import { + highlightsPageQueryOptions, + postHighlightsFeedQueryOptions, +} from '@dailydotdev/shared/src/graphql/highlights'; +import { HighlightsPage } from '@dailydotdev/shared/src/components/highlights/HighlightsPage'; +import { getLayout as getFooterNavBarLayout } from '../../components/layouts/FooterNavBarLayout'; +import { getLayout } from '../../components/layouts/MainLayout'; +import { defaultOpenGraph, defaultSeo } from '../../next-seo'; + +const HIGHLIGHTS_TITLE = 'All highlights | daily.dev'; +const HIGHLIGHTS_DESCRIPTION = + 'Every highlight across the developer ecosystem in one place. Browse the most recent stories from every channel, newest first.'; + +const AllHighlightsPage = (): ReactElement => ; + +const getHighlightsLayout: typeof getLayout = (...props) => + getFooterNavBarLayout(getLayout(...props)); + +AllHighlightsPage.getLayout = getHighlightsLayout; +AllHighlightsPage.layoutProps = { + screenCentered: false, + seo: { + title: HIGHLIGHTS_TITLE, + description: HIGHLIGHTS_DESCRIPTION, + canonical: 'https://app.daily.dev/highlights/all', + openGraph: { + ...defaultOpenGraph, + title: HIGHLIGHTS_TITLE, + description: HIGHLIGHTS_DESCRIPTION, + url: 'https://app.daily.dev/highlights/all', + type: 'website', + }, + ...defaultSeo, + }, +}; + +export default AllHighlightsPage; + +interface AllHighlightsPageProps { + dehydratedState: DehydratedState; +} + +export async function getStaticProps(): Promise< + GetStaticPropsResult +> { + const queryClient = new QueryClient(); + + await Promise.all([ + queryClient.prefetchQuery(highlightsPageQueryOptions()), + queryClient.prefetchQuery(postHighlightsFeedQueryOptions()), + ]); + + return { + props: { + dehydratedState: dehydrate(queryClient), + }, + revalidate: 60, + }; +} From 909d12170ebc90d27832f718798a9eb101821c05 Mon Sep 17 00:00:00 2001 From: idoshamun Date: Thu, 28 May 2026 09:16:58 +0000 Subject: [PATCH 2/2] refactor(highlights): tighten All tab plumbing - Include after in postHighlightsFeed query key so paginated cursors don't collide in cache - Drop unused exports (POST_HIGHLIGHTS_FEED_PAGE_QUERY, query-key helper, PostHighlightsFeedPageData) and the single-call useAllHighlights hook - Collapse the activeTab if/else chain into a direct expression Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/highlights/HighlightsPage.tsx | 28 ++++++---------- packages/shared/src/graphql/highlights.ts | 32 +++++++------------ 2 files changed, 21 insertions(+), 39 deletions(-) diff --git a/packages/shared/src/components/highlights/HighlightsPage.tsx b/packages/shared/src/components/highlights/HighlightsPage.tsx index 0602d052680..eb11f9e7566 100644 --- a/packages/shared/src/components/highlights/HighlightsPage.tsx +++ b/packages/shared/src/components/highlights/HighlightsPage.tsx @@ -44,12 +44,6 @@ const useChannelHighlights = (channel: string | undefined) => enabled: !!channel, }); -const useAllHighlights = (enabled: boolean) => - useQuery({ - ...postHighlightsFeedQueryOptions(), - enabled, - }); - interface HighlightFeedListProps { highlights: PostHighlightFeed[]; loading: boolean; @@ -140,17 +134,19 @@ const AllHighlightsTab = ({ active: boolean; expandedId?: string; }): ReactElement => { - const { data, isFetching } = useAllHighlights(active); + const { data, isFetching } = useQuery({ + ...postHighlightsFeedQueryOptions(), + enabled: active, + }); const highlights = useMemo( () => data?.postHighlightsFeed?.edges?.map((edge) => edge.node) ?? [], [data], ); - const loading = isFetching && !data; return ( ); @@ -170,16 +166,10 @@ export const HighlightsPage = (): ReactElement => { ); const majorLoading = isFetching && !data; - let activeTab: string; - if (isAllTab) { - activeTab = ALL_HIGHLIGHTS_LABEL; - } else if (channel) { - activeTab = - channels.find((c) => c.channel === channel)?.displayName ?? - MAJOR_HEADLINES_LABEL; - } else { - activeTab = MAJOR_HEADLINES_LABEL; - } + const channelLabel = channels.find((c) => c.channel === channel)?.displayName; + const activeTab = isAllTab + ? ALL_HIGHLIGHTS_LABEL + : channelLabel ?? MAJOR_HEADLINES_LABEL; return (
diff --git a/packages/shared/src/graphql/highlights.ts b/packages/shared/src/graphql/highlights.ts index c4f498365a5..fbba785beb0 100644 --- a/packages/shared/src/graphql/highlights.ts +++ b/packages/shared/src/graphql/highlights.ts @@ -149,18 +149,11 @@ export type PostHighlightSignificance = | 'notable' | 'routine'; -export interface PostHighlightsFeedPageData { +interface PostHighlightsFeedPageData { postHighlightsFeed: Connection; } -interface PostHighlightsFeedQueryArgs { - channel?: string; - significance?: PostHighlightSignificance[]; - first?: number; - after?: string; -} - -export const POST_HIGHLIGHTS_FEED_PAGE_QUERY = gql` +const POST_HIGHLIGHTS_FEED_PAGE_QUERY = gql` query PostHighlightsFeedPage( $channel: String $significance: [String!] @@ -187,24 +180,23 @@ export const POST_HIGHLIGHTS_FEED_PAGE_QUERY = gql` ${POST_HIGHLIGHT_FEED_FRAGMENT} `; -export const getPostHighlightsFeedQueryKey = ({ - channel, - significance, -}: Pick) => [ - 'post-highlights-feed', - channel ?? '', - [...(significance ?? [])].sort().join(','), -]; - export const postHighlightsFeedQueryOptions = ({ channel, significance, first = MAJOR_HEADLINES_MAX_FIRST, after, -}: PostHighlightsFeedQueryArgs = {}) => ({ +}: { + channel?: string; + significance?: PostHighlightSignificance[]; + first?: number; + after?: string; +} = {}) => ({ queryKey: [ - ...getPostHighlightsFeedQueryKey({ channel, significance }), + 'post-highlights-feed', + channel ?? '', + [...(significance ?? [])].sort().join(','), first, + after ?? '', ], queryFn: () => gqlClient.request(