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
39 changes: 36 additions & 3 deletions packages/shared/src/components/highlights/HighlightsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 => (
<div className="flex flex-col gap-1 px-4 py-3">
Expand Down Expand Up @@ -124,10 +127,36 @@ const ChannelTab = ({
);
};

const AllHighlightsTab = ({
active,
expandedId,
}: {
active: boolean;
expandedId?: string;
}): ReactElement => {
const { data, isFetching } = useQuery({
...postHighlightsFeedQueryOptions(),
enabled: active,
});
const highlights = useMemo(
() => data?.postHighlightsFeed?.edges?.map((edge) => edge.node) ?? [],
[data],
);

return (
<HighlightFeedList
highlights={highlights}
loading={isFetching && !data}
expandedId={expandedId}
/>
);
};

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 ?? [];
Expand All @@ -137,9 +166,10 @@ export const HighlightsPage = (): ReactElement => {
);
const majorLoading = isFetching && !data;

const activeTab =
channels.find((c) => c.channel === channel)?.displayName ??
MAJOR_HEADLINES_LABEL;
const channelLabel = channels.find((c) => c.channel === channel)?.displayName;
const activeTab = isAllTab
? ALL_HIGHLIGHTS_LABEL
: channelLabel ?? MAJOR_HEADLINES_LABEL;

return (
<main className="mx-auto flex w-full max-w-2xl flex-col pb-8 laptop:min-h-page laptop:border-x laptop:border-border-subtlest-tertiary">
Expand Down Expand Up @@ -172,6 +202,9 @@ export const HighlightsPage = (): ReactElement => {
expandedId={expandedId}
/>
</Tab>,
<Tab key="all" label={ALL_HIGHLIGHTS_LABEL} url={ALL_HIGHLIGHTS_URL}>
<AllHighlightsTab active={isAllTab} expandedId={expandedId} />
</Tab>,
...channels.map((ch) => (
<Tab
key={ch.channel}
Expand Down
70 changes: 70 additions & 0 deletions packages/shared/src/graphql/highlights.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface PostHighlightFeed {
channel: string;
headline: string;
highlightedAt: string;
significance?: string | null;
post: {
id: string;
type: string;
Expand Down Expand Up @@ -109,6 +110,7 @@ export const POST_HIGHLIGHT_FEED_FRAGMENT = gql`
channel
headline
highlightedAt
significance
post {
id
type
Expand Down Expand Up @@ -141,6 +143,74 @@ export const POST_HIGHLIGHTS_FEED_QUERY = gql`
${POST_HIGHLIGHT_FEED_FRAGMENT}
`;

export type PostHighlightSignificance =
| 'breaking'
| 'major'
| 'notable'
| 'routine';

interface PostHighlightsFeedPageData {
postHighlightsFeed: Connection<PostHighlightFeed>;
}

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 postHighlightsFeedQueryOptions = ({
channel,
significance,
first = MAJOR_HEADLINES_MAX_FIRST,
after,
}: {
channel?: string;
significance?: PostHighlightSignificance[];
first?: number;
after?: string;
} = {}) => ({
queryKey: [
'post-highlights-feed',
channel ?? '',
[...(significance ?? [])].sort().join(','),
first,
after ?? '',
],
queryFn: () =>
gqlClient.request<PostHighlightsFeedPageData>(
POST_HIGHLIGHTS_FEED_PAGE_QUERY,
{
channel: channel ?? null,
significance: significance ?? null,
first,
after,
},
),
staleTime: ONE_MINUTE,
});

export interface ChannelDigestConfiguration {
frequency: string;
source?: {
Expand Down
64 changes: 64 additions & 0 deletions packages/webapp/pages/highlights/all.tsx
Original file line number Diff line number Diff line change
@@ -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 => <HighlightsPage />;

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<AllHighlightsPageProps>
> {
const queryClient = new QueryClient();

await Promise.all([
queryClient.prefetchQuery(highlightsPageQueryOptions()),
queryClient.prefetchQuery(postHighlightsFeedQueryOptions()),
]);

return {
props: {
dehydratedState: dehydrate(queryClient),
},
revalidate: 60,
};
}
Loading