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
14 changes: 14 additions & 0 deletions packages/shared/src/graphql/feed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,20 @@ export const TAG_FEED_QUERY = gql`
${FEED_POST_CONNECTION_FRAGMENT}
`;

export const TAG_TOP_POSTS_QUERY = gql`
query TagTopPosts($tag: String!, $first: Int) {
page: tagFeed(tag: $tag, first: $first, ranking: POPULARITY) {
edges {
node {
id
title
slug
}
}
}
}
`;

export const SOURCE_FEED_QUERY = gql`
query SourceFeed(
$source: ID!
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/lib/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export enum OtherFeedPage {
SourcePage = 'sources[source]',
SourceMostUpvoted = 'sources[source]/most-upvoted',
SourceBestDiscussed = 'sources[source]/best-discussed',
TagsTopPosts = 'tags[tag]/top-posts',
TagsMostUpvoted = 'tags[tag]/most-upvoted',
TagsBestDiscussed = 'tags[tag]/best-discussed',
Explore = 'posts',
Expand Down
7 changes: 6 additions & 1 deletion packages/webapp/__tests__/TagPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,12 @@ const renderComponent = (
>
<AlertContextProvider alerts={{}} updateAlerts={jest.fn()} loadedAlerts>
<SettingsContext.Provider value={settingsContext}>
<TagPage tag="react" initialData={initialData} />
<TagPage
tag="react"
initialData={initialData}
topPosts={[]}
recommendedTags={[]}
/>
</SettingsContext.Provider>
</AlertContextProvider>
</AuthContext.Provider>
Expand Down
153 changes: 135 additions & 18 deletions packages/webapp/pages/tags/[tag].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
MOST_DISCUSSED_FEED_QUERY,
MOST_UPVOTED_FEED_QUERY,
TAG_FEED_QUERY,
TAG_TOP_POSTS_QUERY,
} from '@dailydotdev/shared/src/graphql/feed';
import AuthContext from '@dailydotdev/shared/src/contexts/AuthContext';
import type { ButtonProps } from '@dailydotdev/shared/src/components/buttons/Button';
Expand Down Expand Up @@ -74,10 +75,36 @@ import { defaultOpenGraph, defaultSeo } from '../../next-seo';

interface TagPageProps extends DynamicSeoProps {
tag: string;
initialData: Keyword;
initialData: Keyword | null;
topPosts: TagTopPost[];
recommendedTags: TagsData['tags'];
}

const TagRecommendedTags = ({ tag, blockedTags }): ReactElement => {
interface TagTopPost {
id: string;
title?: string;
slug?: string;
}

interface TagTopPostsData {
page?: {
edges?: {
node: TagTopPost;
}[];
};
}

interface TagRecommendedTagsProps {
tag: string;
blockedTags?: string[];
initialTags?: TagsData['tags'];
}

const TagRecommendedTags = ({
tag,
blockedTags,
initialTags = [],
}: TagRecommendedTagsProps): ReactElement => {
const { data: recommendedTags, isPending } = useQuery({
queryKey: [RequestKey.RecommendedTags, null, tag],

Expand All @@ -92,10 +119,12 @@ const TagRecommendedTags = ({ tag, blockedTags }): ReactElement => {
staleTime: StaleTime.OneHour,
});

const tags = recommendedTags?.recommendedTags?.tags ?? initialTags;

return (
<RecommendedTags
isLoading={isPending}
tags={recommendedTags?.recommendedTags?.tags}
isLoading={isPending && initialTags.length === 0}
tags={tags}
/>
);
};
Expand Down Expand Up @@ -133,8 +162,13 @@ const TagTopSources = ({ tag }: { tag: string }) => {
);
};

const TagPage = ({ tag, initialData }: TagPageProps): ReactElement => {
const { isFallback, push } = useRouter();
const TagPage = ({
tag,
initialData,
topPosts,
recommendedTags,
}: TagPageProps): ReactElement => {
const { isFallback, push, query } = useRouter();
const showRoadmap = useFeature(feature.showRoadmap);
const { user, showLogin } = useContext(AuthContext);
const mostUpvotedQueryVariables = useMemo(
Expand All @@ -149,6 +183,18 @@ const TagPage = ({ tag, initialData }: TagPageProps): ReactElement => {
}),
[tag],
);
const topPostsQueryVariables = useMemo(
() => ({
tag,
ranking: 'POPULARITY',
supportedTypes: [
PostType.Article,
PostType.VideoYouTube,
PostType.Collection,
],
}),
[tag],
);
const bestDiscussedQueryVariables = useMemo(
() => ({
tag,
Expand Down Expand Up @@ -187,7 +233,17 @@ const TagPage = ({ tag, initialData }: TagPageProps): ReactElement => {
}, [feedSettings, tag]);

if (isFallback) {
return <></>;
const fallbackTag = typeof query.tag === 'string' ? query.tag : tag;
return (
<FeedPageLayoutComponent>
<PageInfoHeader className={shouldUseListFeedLayout && 'mx-4 !w-auto'}>
<div className="flex items-center font-bold">
<HashtagIcon size={IconSize.XXLarge} />
<h1 className="ml-2 w-fit typo-title2">{fallbackTag}</h1>
</div>
</PageInfoHeader>
</FeedPageLayoutComponent>
);
}

const followButtonProps: ButtonProps<'button'> = {
Expand Down Expand Up @@ -284,10 +340,24 @@ const TagPage = ({ tag, initialData }: TagPageProps): ReactElement => {
{initialData?.flags?.description && (
<p className="typo-body">{initialData?.flags?.description}</p>
)}
{topPosts.length > 0 && (
<div className="sr-only">
{topPosts.map((post) => (
<Link
key={post.id}
href={`/posts/${post.slug || post.id}`}
prefetch={false}
>
<a>{post.title}</a>
</Link>
))}
</div>
)}
{tag && (
<TagRecommendedTags
tag={tag}
blockedTags={feedSettings?.blockedTags}
initialTags={recommendedTags}
/>
)}
{showRoadmap && initialData?.flags?.roadmap && (
Expand Down Expand Up @@ -320,6 +390,25 @@ const TagPage = ({ tag, initialData }: TagPageProps): ReactElement => {
)}
</PageInfoHeader>
<TagTopSources tag={tag} />
<ActiveFeedNameContext.Provider
value={{ feedName: OtherFeedPage.TagsTopPosts }}
>
<HorizontalFeed
feedName={OtherFeedPage.TagsTopPosts}
feedQueryKey={[
'tagsTopPosts',
user?.id ?? 'anonymous',
Object.values(topPostsQueryVariables),
]}
query={TAG_FEED_QUERY}
variables={topPostsQueryVariables}
title={{
copy: 'Top posts',
icon: <HashtagIcon size={IconSize.Medium} className="mr-1.5" />,
}}
emptyScreen={<></>}
/>
</ActiveFeedNameContext.Provider>
<ActiveFeedNameContext.Provider
value={{ feedName: OtherFeedPage.TagsMostUpvoted }}
>
Expand Down Expand Up @@ -405,41 +494,69 @@ export async function getStaticProps({
}: GetStaticPropsContext<TagPageParams>): Promise<
GetStaticPropsResult<TagPageProps>
> {
const tag = params?.tag;
if (!tag) {
return { notFound: true, revalidate: 3600 };
}

const notFoundResponse = {
revalidate: 3600,
props: {
tag: params.tag,
tag,
initialData: null,
seo: getSeoData(params.tag),
topPosts: [],
recommendedTags: [],
seo: getSeoData(tag),
},
};

try {
const result = await gqlClient.request<{ keyword: Keyword }>(
KEYWORD_QUERY,
{ value: params.tag },
);
const [keywordResult, topPostsResult, recommendedTagsResult] =
await Promise.all([
gqlClient.request<{ keyword: Keyword }>(KEYWORD_QUERY, {
value: tag,
}),
gqlClient
.request<TagTopPostsData>(TAG_TOP_POSTS_QUERY, {
tag,
first: 10,
})
.catch(() => null),
gqlClient
.request<{ recommendedTags: TagsData }>(GET_RECOMMENDED_TAGS_QUERY, {
tags: [tag],
excludedTags: [],
})
.catch(() => null),
]);

if (!result?.keyword) {
if (!keywordResult?.keyword) {
return notFoundResponse;
}

const initialData = result.keyword;
const initialData = keywordResult.keyword;
const topPosts =
topPostsResult?.page?.edges
?.map((edge) => edge.node)
.filter((post) => !!post.title) ?? [];
const recommendedTags = recommendedTagsResult?.recommendedTags?.tags ?? [];
const seo = getSeoData(
initialData.flags?.title || params.tag,
initialData.flags?.title || tag,
initialData.flags?.description,
);

return {
props: {
seo,
initialData,
tag: params.tag,
tag,
topPosts,
recommendedTags,
},
revalidate: 3600,
};
} catch (error) {
// keyword not found, ignoring for now
// Return fallback props for any request failure in getStaticProps.
return notFoundResponse;
}
}