Skip to content
Open
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
126 changes: 126 additions & 0 deletions packages/shared/src/components/tags/TagBestOfShowcase.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import type { ReactElement, ReactNode } from 'react';
import React, { useState } from 'react';
import classNames from 'classnames';
import { DiscussIcon, TrendingIcon, UpvoteIcon } from '../icons';
import { IconSize } from '../Icon';
import {
Typography,
TypographyColor,
TypographyTag,
TypographyType,
} from '../typography/Typography';

interface TagBestOfShowcaseProps {
tag: string;
topPosts: ReactNode;
mostUpvoted: ReactNode;
bestDiscussed: ReactNode;
className?: string;
}

type LensId = 'top' | 'upvoted' | 'discussed';

interface Lens {
id: LensId;
label: string;
description: string;
icon: ReactNode;
}

const lenses: Lens[] = [
{
id: 'top',
label: 'Top posts',
description: 'The broadest signal — start here to get oriented fast.',
icon: <TrendingIcon size={IconSize.Size16} />,
},
{
id: 'upvoted',
label: 'Most upvoted',
description: 'What the community decided was worth saving.',
icon: <UpvoteIcon size={IconSize.Size16} />,
},
{
id: 'discussed',
label: 'Best discussed',
description: 'The threads developers had the most to say about.',
icon: <DiscussIcon size={IconSize.Size16} />,
},
];

export const TagBestOfShowcase = ({
tag,
topPosts,
mostUpvoted,
bestDiscussed,
className,
}: TagBestOfShowcaseProps): ReactElement => {
const [active, setActive] = useState<LensId>('top');
const activeLens = lenses.find((lens) => lens.id === active) ?? lenses[0];
const railById: Record<LensId, ReactNode> = {
top: topPosts,
upvoted: mostUpvoted,
discussed: bestDiscussed,
};

return (
<section
id="best-posts"
className={classNames(
'overflow-hidden rounded-16 border border-border-subtlest-tertiary',
className,
)}
>
<header className="flex flex-col gap-4 p-5 laptop:p-6">
<div>
<Typography tag={TypographyTag.H2} type={TypographyType.Title1} bold>
The best of #{tag}
</Typography>
<Typography
type={TypographyType.Callout}
color={TypographyColor.Tertiary}
className="mt-1"
>
{activeLens.description}
</Typography>
</div>
<div
role="tablist"
aria-label="Choose how to explore the best posts"
className="no-scrollbar flex gap-1 overflow-x-auto rounded-12 border border-border-subtlest-tertiary p-1"
>
{lenses.map((lens) => {
const isActive = lens.id === active;
return (
<button
key={lens.id}
type="button"
role="tab"
aria-selected={isActive}
onClick={() => setActive(lens.id)}
className={classNames(
'flex flex-1 shrink-0 items-center justify-center gap-1.5 rounded-10 px-3 py-2 font-bold transition-colors duration-200 typo-footnote',
isActive
? 'bg-surface-float text-text-primary'
: 'text-text-tertiary hover:bg-surface-hover hover:text-text-primary',
)}
>
<span
className={classNames(
isActive && 'text-accent-cabbage-default',
)}
>
{lens.icon}
</span>
<span className="whitespace-nowrap">{lens.label}</span>
</button>
);
})}
</div>
</header>
<div className="border-t border-border-subtlest-tertiary py-5">
{railById[active]}
</div>
</section>
);
};
54 changes: 54 additions & 0 deletions packages/shared/src/components/tags/TagJoinStrip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { ReactElement } from 'react';
import React from 'react';
import classNames from 'classnames';
import { Button, ButtonSize, ButtonVariant } from '../buttons/Button';
import { SparkleIcon } from '../icons';
import {
Typography,
TypographyColor,
TypographyTag,
TypographyType,
} from '../typography/Typography';

interface TagJoinStripProps {
tag: string;
onJoin: () => void;
className?: string;
}

export const TagJoinStrip = ({
tag,
onJoin,
className,
}: TagJoinStripProps): ReactElement => (
<section
className={classNames(
'border-accent-cabbage-default/30 flex flex-col items-start gap-5 rounded-16 border bg-accent-cabbage-subtlest p-6 laptop:flex-row laptop:items-center laptop:justify-between laptop:p-8',
className,
)}
>
<div className="min-w-0">
<Typography tag={TypographyTag.H2} type={TypographyType.Title1} bold>
Stop browsing #{tag}. Make it yours.
</Typography>
<Typography
type={TypographyType.Callout}
color={TypographyColor.Secondary}
className="mt-2 max-w-[44rem]"
>
This page is the map. A free daily.dev account turns it into a living
feed tuned by what you read, save, upvote, and discuss.
</Typography>
</div>
<Button
type="button"
variant={ButtonVariant.Primary}
size={ButtonSize.Large}
icon={<SparkleIcon />}
onClick={onJoin}
className="shrink-0"
>
Build my #{tag} feed
</Button>
</section>
);
169 changes: 169 additions & 0 deletions packages/shared/src/components/tags/TagPageHero.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import type { ReactElement, ReactNode } from 'react';
import React, { Fragment } from 'react';
import classNames from 'classnames';
import type { ButtonProps } from '../buttons/Button';
import { Button, ButtonSize, ButtonVariant } from '../buttons/Button';
import { BlockIcon, PlusIcon } from '../icons';
import {
Typography,
TypographyColor,
TypographyTag,
TypographyType,
} from '../typography/Typography';

export type TagStatus = 'followed' | 'blocked' | 'unfollowed';

export interface TagPageStat {
label: string;
value: string;
caption?: string;
}

const TagStatTicker = ({ stats }: { stats: TagPageStat[] }): ReactElement => (
<dl className="flex flex-wrap items-center gap-x-6 gap-y-4">
{stats.map((stat, index) => (
<Fragment key={stat.label}>
{index > 0 && (
<span
aria-hidden
className="hidden h-9 w-px bg-border-subtlest-tertiary mobileL:block"
/>
)}
<div className="flex flex-col">
<dd className="font-bold tabular-nums text-text-primary typo-title1">
{stat.value}
</dd>
<dt className="text-text-tertiary typo-caption1">{stat.label}</dt>
</div>
</Fragment>
))}
</dl>
);

interface TagPageHeroProps {
tag: string;
title: string;
description?: string;
stats: TagPageStat[];
tagStatus: TagStatus;
followButtonProps: ButtonProps<'button'>;
blockButtonProps: ButtonProps<'button'>;
optionsMenu: ReactNode;
sponsoredHero?: ReactNode;
recommendedTags?: ReactNode;
roadmap?: ReactNode;
crawlLinks?: ReactNode;
className?: string;
}

export const TagPageHero = ({
tag,
title,
description,
stats,
tagStatus,
followButtonProps,
blockButtonProps,
optionsMenu,
sponsoredHero,
recommendedTags,
roadmap,
crawlLinks,
className,
}: TagPageHeroProps): ReactElement => {
const microcopy = {
blocked: 'This topic is blocked from your feed.',
followed: `#${tag} is shaping your feed.`,
unfollowed:
'Free account, no card. Your feed starts learning the moment you follow.',
}[tagStatus];

return (
<section
className={classNames(
'flex flex-col gap-6 rounded-16 border border-border-subtlest-tertiary p-5 laptop:p-8',
className,
)}
>
{sponsoredHero}

<div className="flex flex-col gap-5">
<div className="flex flex-col gap-4 laptop:flex-row laptop:items-start laptop:justify-between">
<div className="min-w-0">
<Typography
tag={TypographyTag.H1}
type={TypographyType.LargeTitle}
bold
className="break-words laptop:typo-mega2"
>
<span className="text-accent-cabbage-default">#</span>
{title}
</Typography>
<Typography
type={TypographyType.Body}
color={TypographyColor.Secondary}
className="mt-3 max-w-[44rem]"
>
{description ||
`Everything developers are reading, saving, upvoting, and debating around #${tag} — curated first, then the full live stream.`}
</Typography>
</div>

<div className="flex shrink-0 flex-wrap items-center gap-2">
{tagStatus !== 'blocked' && (
<Button
type="button"
variant={ButtonVariant.Primary}
size={ButtonSize.Large}
icon={<PlusIcon />}
bold
{...followButtonProps}
aria-label={tagStatus === 'followed' ? 'Unfollow' : 'Follow'}
className={classNames(followButtonProps.className)}
>
{tagStatus === 'followed'
? `Following #${tag}`
: `Follow #${tag}`}
</Button>
)}
{tagStatus !== 'followed' && (
<Button
type="button"
variant={ButtonVariant.Float}
size={ButtonSize.Large}
icon={<BlockIcon />}
{...blockButtonProps}
aria-label={tagStatus === 'blocked' ? 'Unblock' : 'Block'}
>
{tagStatus === 'blocked' ? 'Unblock' : 'Block'}
</Button>
)}
{optionsMenu}
</div>
</div>

<Typography
type={TypographyType.Footnote}
color={TypographyColor.Tertiary}
>
{microcopy}
</Typography>
</div>

{stats.length > 0 && (
<div className="border-t border-border-subtlest-tertiary pt-6">
<TagStatTicker stats={stats} />
</div>
)}

{(recommendedTags || roadmap) && (
<div className="flex flex-col gap-4 border-t border-border-subtlest-tertiary pt-6">
{recommendedTags}
{roadmap}
</div>
)}

{crawlLinks}
</section>
);
};
Loading
Loading