{hasItems ? (
@@ -215,6 +242,7 @@ export function ProfileUserHotTakes({
key={item.id}
item={item}
isOwner={isOwner}
+ ownerUsername={user.username}
onEdit={handleEdit}
onDelete={handleDelete}
onUpvoteClick={handleUpvote}
diff --git a/packages/shared/src/features/profile/components/hotTakes/common.ts b/packages/shared/src/features/profile/components/hotTakes/common.ts
index a788264f847..3d0d61b8a52 100644
--- a/packages/shared/src/features/profile/components/hotTakes/common.ts
+++ b/packages/shared/src/features/profile/components/hotTakes/common.ts
@@ -7,6 +7,24 @@ export const OPEN_ADD_HOT_TAKE_QUERY_VALUE = '1';
export const getAddHotTakeProfileUrl = (username: string): string =>
`${webappUrl}${username}?${OPEN_ADD_HOT_TAKE_QUERY_PARAM}=${OPEN_ADD_HOT_TAKE_QUERY_VALUE}#${HOT_TAKES_ANCHOR}`;
+// View-only deep link (no `addHotTake` query, which would pop the add modal for
+// owners): a shared link lands on the hot-takes section of the owner's profile.
+export const getHotTakesProfileUrl = (username: string): string =>
+ `${webappUrl}${username}#${HOT_TAKES_ANCHOR}`;
+
+// Pre-filled share text quotes the take itself so the share reads like
+// something a dev would post, not like a product template.
+export const getHotTakeShareText = ({
+ title,
+ username,
+}: {
+ title: string;
+ username?: string;
+}): string =>
+ username
+ ? `Hot take: "${title}" โ @${username} on daily.dev`
+ : `Hot take: "${title}" โ on daily.dev`;
+
export const isOpenAddHotTakeQuery = (
value: string | string[] | undefined,
): boolean =>
diff --git a/packages/shared/src/hooks/useHotTakeShareEnabled.ts b/packages/shared/src/hooks/useHotTakeShareEnabled.ts
new file mode 100644
index 00000000000..45ea42b2489
--- /dev/null
+++ b/packages/shared/src/hooks/useHotTakeShareEnabled.ts
@@ -0,0 +1,17 @@
+import { featureShareHotTakes } from '../lib/featureManagement';
+import { useConditionalFeature } from './useConditionalFeature';
+import { useSharingVisibility } from './useSharingVisibility';
+
+// Gate for every hot-take share affordance (profile list items and header, the
+// Hot & Cold modal card, the popular hot takes leaderboard). The per-topic flag
+// is only evaluated once the sharing-visibility master switch is on, so control
+// users never get bucketed into the experiment.
+export const useHotTakeShareEnabled = (shouldEvaluate = true): boolean => {
+ const { isEnabled: isSharingEnabled } = useSharingVisibility(shouldEvaluate);
+ const { value } = useConditionalFeature({
+ feature: featureShareHotTakes,
+ shouldEvaluate: isSharingEnabled,
+ });
+
+ return isSharingEnabled && value;
+};
diff --git a/packages/shared/src/lib/featureManagement.ts b/packages/shared/src/lib/featureManagement.ts
index 97b38cbdd2b..9e8a923f04b 100644
--- a/packages/shared/src/lib/featureManagement.ts
+++ b/packages/shared/src/lib/featureManagement.ts
@@ -308,3 +308,9 @@ export const featureSharingVisibility = new Feature(
// high-traffic icon, so it ramps on its own flag to watch share/copy metrics.
// Keep the default `false` (control = existing `LinkIcon`).
export const featureShareCopyIcon = new Feature('share_copy_icon', false);
+
+// Adds share/copy affordances to the hot-take surfaces: profile list items and
+// the section header, the Hot & Cold discovery modal card, and the popular hot
+// takes leaderboard card. Gated together with the sharing-visibility master
+// switch. Keep the default `false` โ GrowthBook ramps it.
+export const featureShareHotTakes = new Feature('share_hot_takes', false);
diff --git a/packages/shared/src/lib/log.ts b/packages/shared/src/lib/log.ts
index 9696707fa61..4cd3c53a40c 100644
--- a/packages/shared/src/lib/log.ts
+++ b/packages/shared/src/lib/log.ts
@@ -108,6 +108,9 @@ export enum Origin {
GameCenter = 'game center',
DevCard = 'devcard',
CopyMyFeed = 'copy my feed',
+ // Popular hot-takes leaderboard card; the other hot-take share surfaces
+ // reuse the existing HotTakeList / HotAndCold origins.
+ PopularHotTakes = 'popular hot takes',
}
export enum LogEvent {
diff --git a/packages/storybook/stories/components/HotTakeShare.stories.tsx b/packages/storybook/stories/components/HotTakeShare.stories.tsx
new file mode 100644
index 00000000000..5480e43ae44
--- /dev/null
+++ b/packages/storybook/stories/components/HotTakeShare.stories.tsx
@@ -0,0 +1,251 @@
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import React from 'react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { fn } from 'storybook/test';
+import { HotTakeItem } from '@dailydotdev/shared/src/features/profile/components/hotTakes/HotTakeItem';
+import { HotTakeCard } from '@dailydotdev/shared/src/components/modals/hotTakes/HotAndColdModal';
+import { PopularHotTakesList } from '@dailydotdev/shared/src/components/cards/Leaderboard/PopularHotTakesList';
+import type { HotTake } from '@dailydotdev/shared/src/graphql/user/userHotTake';
+import { getLogContextStatic } from '@dailydotdev/shared/src/contexts/LogContext';
+import AuthContext from '@dailydotdev/shared/src/contexts/AuthContext';
+import {
+ FeaturesReadyContext,
+ GrowthBookProvider,
+} from '@dailydotdev/shared/src/components/GrowthBookProvider';
+import { BootApp } from '@dailydotdev/shared/src/lib/boot';
+import type { LoggedUser } from '@dailydotdev/shared/src/lib/user';
+
+const mockUser = {
+ id: '1',
+ name: 'Test User',
+ username: 'testuser',
+ email: 'test@example.com',
+ image: 'https://daily-now-res.cloudinary.com/image/upload/placeholder.jpg',
+ providers: ['google'],
+ createdAt: '2024-01-01T00:00:00.000Z',
+ permalink: 'https://daily.dev/testuser',
+} as unknown as LoggedUser;
+
+const hotTake: HotTake = {
+ id: 'take-1',
+ emoji: '๐ฅ',
+ title: 'Code reviews longer than 20 minutes are a management failure',
+ subtitle: 'Small PRs or bust',
+ position: 1,
+ createdAt: '2026-01-01T00:00:00.000Z',
+ upvotes: 42,
+ upvoted: false,
+};
+
+const modalTake: HotTake = {
+ ...hotTake,
+ id: 'take-2',
+ user: {
+ id: '2',
+ name: 'Spicy Dev',
+ username: 'spicydev',
+ image: 'https://daily-now-res.cloudinary.com/image/upload/placeholder.jpg',
+ permalink: 'https://daily.dev/spicydev',
+ bio: '',
+ createdAt: '2024-01-01T00:00:00.000Z',
+ reputation: 1337,
+ companies: [],
+ isPlus: false,
+ } as unknown as HotTake['user'],
+};
+
+const popularItems = [
+ {
+ score: 1,
+ hotTake: {
+ id: 'p-1',
+ title: 'TypeScript strict mode should be the default everywhere',
+ subtitle: 'by @spicydev',
+ emoji: '๐งจ',
+ },
+ user: { username: 'spicydev' },
+ },
+ {
+ score: 2,
+ hotTake: {
+ id: 'p-2',
+ title: 'Monorepos are a people problem, not a tooling problem',
+ subtitle: null,
+ emoji: '๐ถ๏ธ',
+ },
+ user: { username: 'testuser' },
+ },
+];
+
+// Storybook aliases `@growthbook/growthbook` to a mock whose `getFeatureValue`
+// coerces every falsy default to the truthy string `'control'`, so a flag can't
+// evaluate as `false` here. Flag-off is therefore simulated by holding the
+// features context "not ready" โ the exact path `useConditionalFeature` takes
+// to fall back to the (false) default. Jest is the real flag-off guarantee.
+const withProviders =
+ (enabled: boolean, user: LoggedUser | undefined = mockUser) =>
+ (Story: React.ComponentType): React.ReactElement => {
+ const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false, staleTime: Infinity } },
+ });
+ const LogContext = getLogContextStatic();
+
+ return (
+
+
+
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ feature.defaultValue as any,
+ }}
+ >
+ false,
+ }}
+ >
+
+
+
+
+
+
+
+
+ );
+ };
+
+const meta: Meta = {
+ title: 'Components/Share/HotTakeShare',
+ parameters: {
+ docs: {
+ description: {
+ component:
+ 'Share affordances on the hot-take surfaces (`share_hot_takes` + the `sharing_visibility` master flag): profile list item, profile section header, the Hot & Cold modal card, and the popular hot takes leaderboard. All share the one `HotTakeShareControl`, deep-linking to the hot-takes section of the owner profile.',
+ },
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+// Flag on โ profile list item with the share control in the action area. All
+// boolean flags read truthy under the Storybook GrowthBook mock, so the item
+// wrapper resolves to the V2 engagement-bar variant here.
+export const ProfileItem: Story = {
+ decorators: [withProviders(true)],
+ render: () => (
+
+ ),
+};
+
+// V1 variant of the item: the engagement-bar flag is only evaluated for
+// authenticated users, so rendering without a user keeps V1 while the share
+// flag (evaluated regardless) stays on.
+export const ProfileItemV1: Story = {
+ decorators: [withProviders(true, undefined)],
+ render: () => (
+
+ ),
+};
+
+// Flag off โ must render exactly what ships today (no share control).
+export const ProfileItemControl: Story = {
+ decorators: [withProviders(false)],
+ render: () => (
+
+ ),
+};
+
+// The Hot & Cold modal card with the share control pinned top-right. Only the
+// top card of the swipe stack gets a live control.
+export const ModalCard: Story = {
+ decorators: [withProviders(true)],
+ render: () => (
+
+
+
+ ),
+};
+
+// The popular hot takes leaderboard card with a header share for the board.
+export const PopularList: Story = {
+ decorators: [withProviders(true)],
+ render: () => (
+
+ ),
+};
+
+// Flag off โ the leaderboard card keeps its stock heading, byte-identical to
+// the version without this PR.
+export const PopularListControl: Story = {
+ decorators: [withProviders(false)],
+ render: () => (
+
+ ),
+};