Skip to content

Commit 41756ad

Browse files
committed
Merge pull request #14 from R44VC0RP/cursor/implement-try-in-cursor-button-for-prompts-fa02
Implement try in cursor button for prompts
2 parents baa97ce + 8fcfae0 commit 41756ad

6 files changed

Lines changed: 211 additions & 3 deletions

File tree

app/feed/page.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Eye, Clock } from "lucide-react"
77
import { toast } from "sonner"
88
import { track } from "@vercel/analytics"
99
import { AddToListButton } from "@/components/lists/add-to-list-button"
10+
import { TryInCursorButton } from "@/components/ui/try-in-cursor-button"
1011
import { useSession } from "@/lib/auth-client"
1112
import Link from "next/link"
1213
import {
@@ -378,8 +379,22 @@ export default function FeedPage() {
378379
</Popover>
379380
</div>
380381

381-
{/* Second row - Download and Add to List */}
382+
{/* Second row - Try in Cursor, Download and Add to List */}
382383
<div className="flex items-center gap-2 justify-start sm:justify-end w-full sm:w-auto">
384+
<div onClick={(e) => e.stopPropagation()}>
385+
<TryInCursorButton
386+
title={rule.title}
387+
content={rule.content}
388+
ruleId={rule.id}
389+
variant="ghost"
390+
size="sm"
391+
analyticsContext="feed"
392+
className="h-auto p-1 hover:bg-white/10 text-xs text-gray-400 hover:text-white flex items-center gap-1.5"
393+
>
394+
Try in Cursor
395+
</TryInCursorButton>
396+
</div>
397+
383398
<button
384399
onClick={(e) => {
385400
e.stopPropagation()

app/rule/[slug]/page.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { toast } from "sonner"
1212
import { Button } from "@/components/ui/button"
1313
import { track } from "@vercel/analytics"
1414
import { AddToListButton } from "@/components/lists/add-to-list-button"
15+
import { TryInCursorButton } from "@/components/ui/try-in-cursor-button"
1516
import { useSession } from "@/lib/auth-client"
1617

1718
interface CursorRule {
@@ -308,6 +309,16 @@ export default function PublicRulePage() {
308309
{/* Action Buttons */}
309310
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 sm:gap-0 pt-[0]">
310311
<div className="flex items-center gap-3 flex-wrap">
312+
<TryInCursorButton
313+
title={rule.title}
314+
content={rule.content}
315+
ruleId={rule.id}
316+
variant="default"
317+
size="sm"
318+
analyticsContext="rule-page"
319+
className="bg-[#70A7D7] hover:bg-[#8BB8E8] text-white"
320+
/>
321+
311322
<Button
312323
onClick={handleDownload}
313324
variant="secondary"
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"use client"
2+
3+
import { useState } from "react"
4+
import { Button } from "@/components/ui/button"
5+
import { generateWebPromptDeeplink, formatRuleAsPrompt, isPromptValidForDeeplink } from "@/lib/cursor-deeplink"
6+
import { track } from "@vercel/analytics"
7+
import { toast } from "sonner"
8+
import { ExternalLink } from "lucide-react"
9+
10+
interface TryInCursorButtonProps {
11+
/** The rule title */
12+
title: string
13+
/** The rule content */
14+
content: string
15+
/** The rule ID for analytics tracking */
16+
ruleId: string
17+
/** Button variant */
18+
variant?: "default" | "secondary" | "ghost" | "outline"
19+
/** Button size */
20+
size?: "sm" | "default" | "lg"
21+
/** Additional CSS classes */
22+
className?: string
23+
/** Button text (defaults to "Try in Cursor") */
24+
children?: React.ReactNode
25+
/** Analytics context for tracking */
26+
analyticsContext?: string
27+
}
28+
29+
export function TryInCursorButton({
30+
title,
31+
content,
32+
ruleId,
33+
variant = "default",
34+
size = "sm",
35+
className,
36+
children = "Try in Cursor",
37+
analyticsContext = "unknown"
38+
}: TryInCursorButtonProps) {
39+
const [isClicked, setIsClicked] = useState(false)
40+
41+
const handleClick = () => {
42+
try {
43+
const promptText = formatRuleAsPrompt(title, content)
44+
45+
// Check if the prompt would fit in a deeplink URL
46+
if (!isPromptValidForDeeplink(promptText)) {
47+
toast.error("This rule is too long to share as a Cursor deeplink")
48+
return
49+
}
50+
51+
const deeplink = generateWebPromptDeeplink(promptText)
52+
53+
// Open the deeplink in a new window/tab
54+
window.open(deeplink, '_blank', 'noopener,noreferrer')
55+
56+
// Show feedback
57+
setIsClicked(true)
58+
setTimeout(() => setIsClicked(false), 2000)
59+
60+
// Track the event
61+
track("Try in Cursor Clicked", {
62+
ruleId,
63+
ruleTitle: title,
64+
context: analyticsContext,
65+
promptLength: promptText.length
66+
})
67+
68+
toast.success("Opening in Cursor...")
69+
} catch (error) {
70+
console.error("Error generating Cursor deeplink:", error)
71+
toast.error("Failed to generate Cursor deeplink")
72+
}
73+
}
74+
75+
return (
76+
<Button
77+
onClick={handleClick}
78+
variant={variant}
79+
size={size}
80+
className={className}
81+
disabled={isClicked}
82+
>
83+
{isClicked ? (
84+
<>
85+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 18 18">
86+
<g fill="currentColor">
87+
<path d="M9 1.5C4.86 1.5 1.5 4.86 1.5 9C1.5 13.14 4.86 16.5 9 16.5C13.14 16.5 16.5 13.14 16.5 9C16.5 4.86 13.14 1.5 9 1.5ZM7.5 12.75L3.75 9L5.16 7.59L7.5 9.93L12.84 4.59L14.25 6L7.5 12.75Z"/>
88+
</g>
89+
</svg>
90+
Opened!
91+
</>
92+
) : (
93+
<>
94+
<svg
95+
xmlns="http://www.w3.org/2000/svg"
96+
width="16"
97+
height="16"
98+
viewBox="0 0 24 24"
99+
fill="none"
100+
stroke="currentColor"
101+
strokeWidth="2"
102+
strokeLinecap="round"
103+
strokeLinejoin="round"
104+
>
105+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
106+
<polyline points="15,3 21,3 21,9"/>
107+
<line x1="10" y1="14" x2="21" y2="3"/>
108+
</svg>
109+
{children}
110+
</>
111+
)}
112+
</Button>
113+
)
114+
}

lib/cursor-deeplink.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* Utility functions for generating Cursor deeplinks
3+
* Based on the Cursor deeplink documentation
4+
*/
5+
6+
const MAX_URL_LENGTH = 8000;
7+
8+
/**
9+
* Generates a Cursor app deeplink for a prompt
10+
* @param promptText The text to pre-fill in the chat
11+
* @returns Cursor app deeplink URL
12+
*/
13+
export function generatePromptDeeplink(promptText: string): string {
14+
const url = new URL('cursor://anysphere.cursor-deeplink/prompt');
15+
url.searchParams.set('text', promptText);
16+
17+
// Check URL length limit
18+
if (url.toString().length > MAX_URL_LENGTH) {
19+
throw new Error(`Deeplink URL exceeds maximum length of ${MAX_URL_LENGTH} characters`);
20+
}
21+
22+
return url.toString();
23+
}
24+
25+
/**
26+
* Generates a web-based Cursor deeplink for a prompt (works in browsers)
27+
* @param promptText The text to pre-fill in the chat
28+
* @returns Web-based Cursor deeplink URL
29+
*/
30+
export function generateWebPromptDeeplink(promptText: string): string {
31+
const url = new URL('https://cursor.com/link/prompt');
32+
url.searchParams.set('text', promptText);
33+
34+
// Check URL length limit
35+
if (url.toString().length > MAX_URL_LENGTH) {
36+
throw new Error(`Deeplink URL exceeds maximum length of ${MAX_URL_LENGTH} characters`);
37+
}
38+
39+
return url.toString();
40+
}
41+
42+
/**
43+
* Formats a cursor rule into a prompt suitable for deeplinks
44+
* @param title The rule title
45+
* @param content The rule content
46+
* @returns Formatted prompt text
47+
*/
48+
export function formatRuleAsPrompt(title: string, content: string): string {
49+
return `Apply this cursor rule: "${title}"
50+
51+
${content}`;
52+
}
53+
54+
/**
55+
* Checks if a prompt text would exceed URL length limits when encoded
56+
* @param promptText The prompt text to check
57+
* @returns Whether the prompt would fit in a deeplink URL
58+
*/
59+
export function isPromptValidForDeeplink(promptText: string): boolean {
60+
try {
61+
generateWebPromptDeeplink(promptText);
62+
return true;
63+
} catch {
64+
return false;
65+
}
66+
}

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"start": "next start",
1010
"db:generate": "drizzle-kit generate",
1111
"db:migrate": "drizzle-kit migrate",
12-
"db:push": "drizzle-kit push"
12+
"db:push": "drizzle-kit push",
13+
"preinstall": "node preinstall.js"
1314
},
1415
"dependencies": {
1516
"@hookform/resolvers": "^3.10.0",
@@ -83,4 +84,4 @@
8384
"tw-animate-css": "1.3.3",
8485
"typescript": "^5"
8586
}
86-
}
87+
}

preinstall.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)