Skip to content
Open
107 changes: 25 additions & 82 deletions packages/shared/src/components/CustomFeedEmptyScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import type { Dispatch, ReactElement, SetStateAction } from 'react';
import type { Dispatch, ReactElement, ReactNode, SetStateAction } from 'react';
import React from 'react';
import { EmptyScreenIcon } from './EmptyScreen';
import { DevPlusIcon, HashtagIcon } from './icons';
import { HashtagIcon } from './icons';
import { PageContainer, SharedFeedPage } from './utilities';
import { ButtonSize, ButtonVariant } from './buttons/common';
import { plusUrl } from '../lib/constants';
import {
DEFAULT_ALGORITHM_INDEX,
DEFAULT_ALGORITHM_KEY,
Expand All @@ -14,23 +12,16 @@ import usePersistentContext from '../hooks/usePersistentContext';
import {
Typography,
TypographyColor,
TypographyTag,
TypographyType,
} from './typography/Typography';
import { LogEvent, TargetId } from '../lib/log';
import { Button } from './buttons/Button';
import { useConditionalFeature, usePlusSubscription } from '../hooks';
import { IconSize } from './Icon';
import { featurePlusApiLanding } from '../lib/featureManagement';
import Link from './utilities/Link';

export const CustomFeedEmptyScreen = (): ReactElement => {
const { logSubscriptionEvent, isPlus } = usePlusSubscription();
const { value: isApiLanding } = useConditionalFeature({
feature: featurePlusApiLanding,
shouldEvaluate: !isPlus,
});
const plusCta = isApiLanding ? 'Get API Access' : 'Level Up with Plus';
type CustomFeedEmptyScreenProps = {
chips?: ReactNode;
};

export const CustomFeedEmptyScreen = ({
chips,
}: CustomFeedEmptyScreenProps = {}): ReactElement => {
const [selectedAlgo, setSelectedAlgo] = usePersistentContext(
DEFAULT_ALGORITHM_KEY,
DEFAULT_ALGORITHM_INDEX,
Expand All @@ -48,10 +39,11 @@ export const CustomFeedEmptyScreen = (): ReactElement => {

return (
<div className="flex w-full flex-col">
<div className="mr-auto mt-0 flex gap-3 tablet:mr-0 tablet:mt-2 laptop:mr-auto laptop:w-auto">
<div className="mt-0 flex w-full gap-3 tablet:mt-2">
<SearchControlHeader
algoState={algoState}
feedName={SharedFeedPage.Custom}
chips={chips}
/>
</div>
<PageContainer className="mx-auto">
Expand All @@ -60,69 +52,20 @@ export const CustomFeedEmptyScreen = (): ReactElement => {
className={EmptyScreenIcon.className}
style={EmptyScreenIcon.style}
/>
{!isPlus ? (
<>
<Typography
tag={TypographyTag.Span}
type={TypographyType.Caption1}
className="flex gap-0.5 rounded-4 bg-action-plus-float p-0.5 pr-1"
color={TypographyColor.Plus}
>
<DevPlusIcon size={IconSize.Size16} /> Plus
</Typography>
<Typography
type={TypographyType.Title1}
color={TypographyColor.Primary}
bold
>
Custom feeds got a massive upgrade!
</Typography>
<Typography
type={TypographyType.Callout}
color={TypographyColor.Tertiary}
>
{`Custom Feeds is now more powerful than ever before, with
advanced filters, extensive customization options, and complete
feed control. Upgrade to Plus to unlock this ultimate tool for
tailoring your content.`}
</Typography>
<Link href={plusUrl} passHref>
<Button
className="mt-10"
tag="a"
type="button"
variant={ButtonVariant.Primary}
size={ButtonSize.Medium}
icon={<DevPlusIcon className="text-action-plus-default" />}
onClick={() => {
logSubscriptionEvent({
event_name: LogEvent.UpgradeSubscription,
target_id: TargetId.CustomFeed,
});
}}
>
{plusCta}
</Button>
</Link>
</>
) : (
<>
<Typography
type={TypographyType.Title1}
color={TypographyColor.Primary}
bold
>
Your feed filters are too specific.
</Typography>
<Typography
type={TypographyType.Callout}
color={TypographyColor.Tertiary}
>
We couldn&apos;t fetch enough posts based on your selected tags.
Try adding more tags using the feed settings.
</Typography>
</>
)}
<Typography
type={TypographyType.Title1}
color={TypographyColor.Primary}
bold
>
Your feed filters are too specific.
</Typography>
<Typography
type={TypographyType.Callout}
color={TypographyColor.Tertiary}
>
We couldn&apos;t fetch enough posts based on your selected tags. Try
adding more tags using the feed settings.
</Typography>
</div>
</PageContainer>
</div>
Expand Down
45 changes: 30 additions & 15 deletions packages/shared/src/components/MainFeedLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ReactElement, ReactNode, SetStateAction } from 'react';
import React, {
cloneElement,
useCallback,
useContext,
useEffect,
Expand All @@ -14,7 +15,7 @@ import Feed from './Feed';
import { FeedPageLayoutMobile } from './utilities/common';
import { ExploreChipsBar } from './feeds/ExploreChipsBar';
import { buildPersonalizedCategories } from './feeds/exploreCategories';
import { useFeedTagsList } from '../hooks/useFeedTagsList';
import { useFeeds } from '../hooks/feed/useFeeds';
import ReadingReminderHero from './marketing/banners/ReadingReminderHero';
import { WebappShortcutsRow } from '../features/shortcuts/components/WebappShortcutsRow';
import { LiveStandupsStrip } from './liveRooms/LiveStandupsStrip';
Expand Down Expand Up @@ -323,29 +324,32 @@ export default function MainFeedLayout({
});

const isChipStripPage =
router.pathname === '/' || router.pathname === '/explore/[tag]';
router.pathname === '/' ||
router.pathname === '/my-feed' ||
router.pathname === '/explore/[tag]' ||
router.pathname === '/feeds/[slugOrId]' ||
router.pathname === '/feeds/[slugOrId]/edit';
const { value: isFeedTagChipsEnabled } = useConditionalFeature({
feature: featureFeedTagChips,
shouldEvaluate: !!user && isLaptop && isChipStripPage,
});
const showExploreChips =
!!user && isLaptop && isChipStripPage && isFeedTagChipsEnabled;
const { tags: feedTags, isPending: isFeedTagsPending } = useFeedTagsList({
enabled: showExploreChips,
});
const { feeds } = useFeeds();
const exploreCategories = useMemo(
() => buildPersonalizedCategories(feedTags),
[feedTags],
() =>
buildPersonalizedCategories(feeds?.edges ?? [], {
defaultFeedId,
isCustomDefaultFeed,
}),
[feeds?.edges, defaultFeedId, isCustomDefaultFeed],
);
const chipsNode = useMemo(
() =>
showExploreChips ? (
<ExploreChipsBar
categories={exploreCategories}
isPending={isFeedTagsPending}
/>
<ExploreChipsBar categories={exploreCategories} isPending={!feeds} />
) : null,
[showExploreChips, exploreCategories, isFeedTagsPending],
[showExploreChips, exploreCategories, feeds],
);

const { isSearchPageLaptop } = useSearchResultsLayout();
Expand Down Expand Up @@ -505,6 +509,14 @@ export default function MainFeedLayout({
return null;
}

const baseEmptyScreen = propsByFeed[feedName]?.emptyScreen || (
<FeedEmptyScreen />
);
const emptyScreenWithChips = cloneElement(
baseEmptyScreen as ReactElement<{ chips?: ReactNode }>,
{ chips: chipsNode },
);

if (feedNameProp === 'default' && isCustomDefaultFeed) {
if (!defaultFeedId) {
return null;
Expand All @@ -522,11 +534,14 @@ export default function MainFeedLayout({
feedId: defaultFeedId,
feedName: SharedFeedPage.Custom,
},
emptyScreen: propsByFeed[feedName]?.emptyScreen || <FeedEmptyScreen />,
emptyScreen: emptyScreenWithChips,
actionButtons: feedWithActions && (
<SearchControlHeader
algoState={[selectedAlgo, handleSelectedAlgoChange]}
feedName={feedName}
// On `/` with a custom default feed the rendered feed is the
// custom feed (not MyFeed) — pass `Custom` so derived flags
// (isSortableFeed, etc.) reflect that, not the outer 'default'.
feedName={SharedFeedPage.Custom}
chips={shouldUseListFeedLayout ? undefined : chipsNode}
/>
),
Expand Down Expand Up @@ -602,7 +617,7 @@ export default function MainFeedLayout({
),
query: config.query,
variables,
emptyScreen: propsByFeed[feedName]?.emptyScreen || <FeedEmptyScreen />,
emptyScreen: emptyScreenWithChips,
actionButtons: feedWithActions && (
<SearchControlHeader
algoState={[selectedAlgo, handleSelectedAlgoChange]}
Expand Down
64 changes: 35 additions & 29 deletions packages/shared/src/components/feeds/ExploreChipsBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import React, { useEffect, useMemo, useRef } from 'react';
import classNames from 'classnames';
import { useRouter } from 'next/router';
import Link from '../utilities/Link';
import { PlusIcon } from '../icons';
import { webappUrl } from '../../lib/constants';
import useCustomDefaultFeed from '../../hooks/feed/useCustomDefaultFeed';
import { ElementPlaceholder } from '../ElementPlaceholder';
import { useLogContext } from '../../contexts/LogContext';
import type { ExploreCategory } from './exploreCategories';
import { findActiveChipId } from './exploreCategories';
import { LogEvent } from '../../lib/log';
import { NewStripCta } from './NewStripCta';

interface ExploreChipsBarProps {
categories: ExploreCategory[];
Expand All @@ -20,14 +23,6 @@ const PLACEHOLDER_WIDTHS = ['w-20', 'w-16', 'w-24', 'w-20', 'w-28', 'w-16'];

const FOR_YOU_CATEGORY_ID = 'foryou';

const normalizePath = (p: string): string => {
const noQuery = p.split('?')[0];
if (!noQuery || noQuery === '/') {
return '/';
}
return noQuery.replace(/\/$/, '');
};

export function ExploreChipsBar({
categories,
isPending,
Expand All @@ -37,24 +32,34 @@ export function ExploreChipsBar({
const { isCustomDefaultFeed } = useCustomDefaultFeed();
const { logEvent } = useLogContext();

const forYouCategory: ExploreCategory = useMemo(
() => ({
const forYouCategory: ExploreCategory = useMemo(() => {
const path = isCustomDefaultFeed ? `${webappUrl}my-feed` : webappUrl;
return {
id: FOR_YOU_CATEGORY_ID,
label: 'For you',
path: isCustomDefaultFeed ? `${webappUrl}my-feed` : webappUrl,
}),
[isCustomDefaultFeed],
);
const activePath = useMemo(
() => normalizePath(router.asPath),
[router.asPath],
);
path,
// When a custom feed is the default, `/` shows that feed (not "For you"
// content) — restrict matching to `/my-feed`. Without a custom default
// `/` is MyFeed, so include both.
matchPaths: isCustomDefaultFeed
? [`${webappUrl}my-feed`]
: [path, webappUrl, `${webappUrl}my-feed`],
};
}, [isCustomDefaultFeed]);

const allCategories = useMemo(
() => [forYouCategory, ...categories],
[forYouCategory, categories],
);

const activeId = useMemo(
() =>
findActiveChipId(allCategories, router.asPath, {
preferId: FOR_YOU_CATEGORY_ID,
}),
[allCategories, router.asPath],
);

const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const active = scrollRef.current?.querySelector<HTMLElement>(
Expand All @@ -64,25 +69,17 @@ export function ExploreChipsBar({
return;
}
active.scrollIntoView({ block: 'nearest', inline: 'center' });
}, [activePath, allCategories]);
}, [activeId, allCategories]);

return (
<div className={classNames('relative', className)}>
<div
ref={scrollRef}
className="no-scrollbar flex items-center gap-2 overflow-x-auto pr-12"
>
<NewStripCta className="h-10 rounded-12 px-3" />
{allCategories.map((category) => {
// For You owns the homepage. Match it against both `/` and `/my-feed`
// so the user's default custom feed (also at `/`) doesn't steal the
// active state.
const isForYou = category.id === FOR_YOU_CATEGORY_ID;
const candidates = isForYou
? [category.path, webappUrl, `${webappUrl}my-feed`]
: [category.path];
const isActive = candidates.some(
(candidate) => normalizePath(candidate) === activePath,
);
const isActive = category.id === activeId;
return (
<Link key={category.id} href={category.path}>
<a
Expand Down Expand Up @@ -121,6 +118,15 @@ export function ExploreChipsBar({
className={classNames('h-10 shrink-0 rounded-12', width)}
/>
))}
<Link href={`${webappUrl}feeds/new`}>
<a
href={`${webappUrl}feeds/new`}
aria-label="New feed"
className="inline-flex h-10 shrink-0 items-center justify-center rounded-12 border border-transparent bg-background-subtle px-3 text-text-tertiary transition-colors hover:bg-surface-hover hover:text-text-primary"
>
<PlusIcon />
</a>
</Link>
</div>
<div
aria-hidden
Expand Down
Loading
Loading