Skip to content

Commit 4607dc8

Browse files
brianlovinclaude
andauthored
Add HN CLI to projects and install upsell (#2177)
Co-authored-by: Claude Haiku 4.5 <[email protected]>
1 parent e5aea32 commit 4607dc8

File tree

6 files changed

+65
-6
lines changed

6 files changed

+65
-6
lines changed

src/app/hn/HNDigestCard.tsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,44 @@
11
"use client";
22
import { useAtom } from "jotai";
3+
import { useState } from "react";
34

4-
import { hnSubscribedAtom } from "@/atoms/hnSubscription";
5+
import { hnCliCopiedAtom, hnSubscribedAtom } from "@/atoms/hnSubscription";
56
import { Close } from "@/components/icons/Close";
67
import { Triangle } from "@/components/icons/Triangle";
78
import { cn } from "@/lib/utils";
89

910
import { SubscribeForm } from "./SubscribeForm";
1011

12+
const CLI_COMMAND = "npm install -g @brianlovin/hn-cli";
13+
14+
export function HNCLIUpsell({ className }: { className?: string }) {
15+
const [isSubscribed] = useAtom(hnSubscribedAtom);
16+
const [hasCopied, setHasCopied] = useAtom(hnCliCopiedAtom);
17+
const [showCopied, setShowCopied] = useState(false);
18+
19+
const handleCopy = async () => {
20+
await navigator.clipboard.writeText(CLI_COMMAND);
21+
setShowCopied(true);
22+
setTimeout(() => {
23+
setShowCopied(false);
24+
setHasCopied(true);
25+
}, 1500);
26+
};
27+
28+
if (isSubscribed || hasCopied) return null;
29+
30+
return (
31+
<div className={cn("text-center font-mono text-sm", className)}>
32+
<button
33+
onClick={handleCopy}
34+
className="text-tertiary hover:text-secondary cursor-pointer transition-colors"
35+
>
36+
{showCopied ? "Copied!" : CLI_COMMAND}
37+
</button>
38+
</div>
39+
);
40+
}
41+
1142
export function HNDigestCard({ className }: { className?: string }) {
1243
const [isSubscribed, setIsSubscribed] = useAtom(hnSubscribedAtom);
1344

src/app/hn/HNPageClient.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useAtomValue } from "jotai";
55
import { hnSubscribedAtom } from "@/atoms/hnSubscription";
66
import { SpeechBubble } from "@/components/icons/SpeechBubble";
77

8-
import { HNDigestCard } from "./HNDigestCard";
8+
import { HNCLIUpsell, HNDigestCard } from "./HNDigestCard";
99

1010
export function HNPageClient() {
1111
const isSubscribed = useAtomValue(hnSubscribedAtom);
@@ -15,7 +15,10 @@ export function HNPageClient() {
1515
{isSubscribed ? (
1616
<SpeechBubble size={100} className="opacity-10" />
1717
) : (
18-
<HNDigestCard className="bg-elevated max-w-xl shadow-xs" />
18+
<div className="flex w-full max-w-xl flex-col gap-4">
19+
<HNDigestCard className="bg-elevated shadow-xs" />
20+
<HNCLIUpsell />
21+
</div>
1922
)}
2023
</div>
2124
);

src/app/hn/[id]/HNPostPageClient.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { useHNPost } from "@/lib/hooks/useHn";
1717
import { stripHtmlTags } from "@/lib/utils";
1818
import { HackerNewsComment, HackerNewsPost } from "@/types/hackernews";
1919

20-
import { HNDigestCard } from "../HNDigestCard";
20+
import { HNCLIUpsell, HNDigestCard } from "../HNDigestCard";
2121
import { useHNPostsContext } from "../HNPostsContext";
2222

2323
// Lazily initialize DOMPurify to avoid SSR issues
@@ -183,7 +183,12 @@ export default function HNPostPageClient({ initialPost }: HNPostPageClientProps)
183183
)}
184184
</div>
185185

186-
<HNDigestCard className="mb-12" />
186+
{!isSubscribed && (
187+
<div className="mb-12 flex flex-col gap-4">
188+
<HNDigestCard />
189+
<HNCLIUpsell />
190+
</div>
191+
)}
187192

188193
<FancySeparator />
189194

@@ -196,7 +201,10 @@ export default function HNPostPageClient({ initialPost }: HNPostPageClientProps)
196201
{!isSubscribed && post?.comments && !!post.comments.length && (
197202
<div className="pt-8 md:pt-12">
198203
<FancySeparator />
199-
<HNDigestCard className="mt-8" />
204+
<div className="mt-8 flex flex-col gap-4">
205+
<HNDigestCard />
206+
<HNCLIUpsell />
207+
</div>
200208
</div>
201209
)}
202210
</div>

src/atoms/hnSubscription.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
import { atomWithStorage } from "jotai/utils";
22

33
export const hnSubscribedAtom = atomWithStorage("hn-subscribed", false);
4+
export const hnCliCopiedAtom = atomWithStorage("hn-cli-copied", false);

src/components/home/ProjectsList.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ const projects = [
7979
description: "A visual guide to understand terminals",
8080
external: true,
8181
},
82+
{
83+
name: "HN CLI",
84+
href: "https://github.com/brianlovin/hn-cli",
85+
description: "A command-line interface for Hacker News",
86+
external: true,
87+
},
8288
];
8389

8490
interface ProjectsListProps {

src/config/navigation.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Headphones3 } from "@/components/icons/Headphones3";
99
import { Home } from "@/components/icons/Home";
1010
import { LightBulb } from "@/components/icons/LightBulb";
1111
import { Person } from "@/components/icons/Person";
12+
import { Terminal } from "@/components/icons/Terminal";
1213
import { Triangle } from "@/components/icons/Triangle";
1314
import { IconProps } from "@/components/icons/types";
1415

@@ -114,6 +115,15 @@ export const navigationItems: NavigationItem[] = [
114115
isActive: (pathname) => pathname.startsWith("/sites"),
115116
section: "projects",
116117
},
118+
{
119+
id: "hn-cli",
120+
label: "HN CLI",
121+
href: "https://github.com/brianlovin/hn-cli",
122+
icon: Terminal,
123+
keywords: ["hn", "cli", "hacker news", "terminal"],
124+
isActive: () => false,
125+
section: "projects",
126+
},
117127
];
118128

119129
// Helper functions to filter navigation items

0 commit comments

Comments
 (0)