Skip to content
Open
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
163 changes: 117 additions & 46 deletions app/rule/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
"use client"

import { useEffect, useState } from "react"
import { useEffect, useState, useRef } from "react"
import { useParams } from "next/navigation"
import { Header } from "@/components/header"
import { Card } from "@/components/ui/card"
import { Textarea } from "@/components/ui/textarea"
import { Input } from "@/components/ui/input"
import { Copy, Eye, Download, Terminal, Check } from "lucide-react"
import { Copy, Eye, Download, Terminal, Check, Share2 } from "lucide-react"
import { countTokens } from "gpt-tokenizer"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
Expand Down Expand Up @@ -82,6 +82,8 @@ export default function PublicRulePage() {
const [tokenCount, setTokenCount] = useState(0)
const [copied, setCopied] = useState(false)
const [cliCopied, setCliCopied] = useState(false)
const [urlCopied, setUrlCopied] = useState(false)
const textareaRef = useRef<HTMLTextAreaElement>(null)

useEffect(() => {
const fetchRule = async () => {
Expand Down Expand Up @@ -140,8 +142,6 @@ export default function PublicRulePage() {
toast.success("Install command copied to clipboard!")
}



const handleDownload = () => {
if (!rule) return

Expand All @@ -157,6 +157,45 @@ export default function PublicRulePage() {
track("Public Rule Downloaded", { ruleId: rule.id })
}

const handleCopyViewURL = async () => {
if (!rule) return

const viewURL = window.location.href
await navigator.clipboard.writeText(viewURL)
setUrlCopied(true)
track("Public Rule View URL Copied", { ruleId: rule.id })
setTimeout(() => setUrlCopied(false), 2000)
toast.success("View URL copied to clipboard!")
}
Comment on lines +160 to +169
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Harden clipboard copy with error handling.

Wrap in try/catch and show a failure toast.

-  const handleCopyViewURL = async () => {
-    if (!rule) return
-    
-    const viewURL = window.location.href
-    await navigator.clipboard.writeText(viewURL)
-    setUrlCopied(true)
-    track("Public Rule View URL Copied", { ruleId: rule.id })
-    setTimeout(() => setUrlCopied(false), 2000)
-    toast.success("View URL copied to clipboard!")
-  }
+  const handleCopyViewURL = async () => {
+    if (!rule) return
+    try {
+      const viewURL = window.location.href
+      await navigator.clipboard.writeText(viewURL)
+      setUrlCopied(true)
+      track("Public Rule View URL Copied", { ruleId: rule.id })
+      setTimeout(() => setUrlCopied(false), 2000)
+      toast.success("View URL copied to clipboard!")
+    } catch (e) {
+      toast.error("Failed to copy URL. Please copy it manually.")
+    }
+  }

Apply the same pattern to handleCopy and handleInstallCopy.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleCopyViewURL = async () => {
if (!rule) return
const viewURL = window.location.href
await navigator.clipboard.writeText(viewURL)
setUrlCopied(true)
track("Public Rule View URL Copied", { ruleId: rule.id })
setTimeout(() => setUrlCopied(false), 2000)
toast.success("View URL copied to clipboard!")
}
const handleCopyViewURL = async () => {
if (!rule) return
try {
const viewURL = window.location.href
await navigator.clipboard.writeText(viewURL)
setUrlCopied(true)
track("Public Rule View URL Copied", { ruleId: rule.id })
setTimeout(() => setUrlCopied(false), 2000)
toast.success("View URL copied to clipboard!")
} catch (e) {
toast.error("Failed to copy URL. Please copy it manually.")
}
}


const handleCopyCLI = async () => {
if (!rule) return

const installCommand = `npx shadcn add ${window.location.origin}/api/registry/${rule.id}`
await navigator.clipboard.writeText(installCommand)
setCliCopied(true)
track("CLI Command Copied", { ruleId: rule.id })
setTimeout(() => setCliCopied(false), 2000)
toast.success("Install command copied to clipboard!")
}
Comment on lines +171 to +180
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handleCopyCLI function is a duplicate of the existing handleInstallCopy function, creating unnecessary code duplication.

View Details

Analysis

The code contains two identical functions that perform the same operation:

  1. handleInstallCopy (lines 134-143) - original function used by the CLI card
  2. handleCopyCLI (lines 171-180) - duplicate function used by the floating action bar

Both functions:

  • Generate the same install command
  • Copy to clipboard
  • Set the same state (setCliCopied(true))
  • Show the same toast message
  • Use the same timeout duration

The only difference is the tracking event name ("CLI Copied" vs "CLI Command Copied"). This duplication increases bundle size and creates maintenance overhead. Since the CLI card is now hidden on mobile (hidden md:block on line 260), the floating action bar could reuse the existing handleInstallCopy function instead of duplicating it. Either remove the duplicate function and use handleInstallCopy, or remove the original and keep handleCopyCLI if the different tracking event name is intentional.


// Auto-resize textarea on mobile
useEffect(() => {
const adjustTextareaHeight = () => {
if (textareaRef.current && window.innerWidth < 768) {
textareaRef.current.style.height = 'auto'
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`
}
}

if (rule?.content) {
setTimeout(adjustTextareaHeight, 100)
}

window.addEventListener('resize', adjustTextareaHeight)
return () => window.removeEventListener('resize', adjustTextareaHeight)
}, [rule?.content])

if (loading) {
return <PublicRuleSkeleton />
}
Expand Down Expand Up @@ -207,17 +246,18 @@ export default function PublicRulePage() {
}}
>
<Textarea
ref={textareaRef}
value={rule.content}
readOnly
className="min-h-[500px] resize-none border-0 bg-[#1B1D21] text-white leading-relaxed"
className="min-h-[500px] md:min-h-[500px] resize-none border-0 bg-[#1B1D21] text-white leading-relaxed"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Honor PR goal on mobile: remove 500px min-height for small screens.

Currently forces 500px on mobile, causing overflow.

-                className="min-h-[500px] md:min-h-[500px] resize-none border-0 bg-[#1B1D21] text-white leading-relaxed"
+                className="min-h-0 md:min-h-[500px] resize-none border-0 bg-[#1B1D21] text-white leading-relaxed"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
className="min-h-[500px] md:min-h-[500px] resize-none border-0 bg-[#1B1D21] text-white leading-relaxed"
className="min-h-0 md:min-h-[500px] resize-none border-0 bg-[#1B1D21] text-white leading-relaxed"
🤖 Prompt for AI Agents
In app/rule/[slug]/page.tsx around line 252, the textarea/class currently forces
min-h-[500px] on all screen sizes which causes overflow on mobile; change the
classes so the 500px minimum only applies at md and above (for example remove
the global min-h-[500px] and use md:min-h-[500px] or add min-h-0
md:min-h-[500px]) and keep the rest of the classes intact to honor the PR goal
of no 500px min-height on small screens.

style={{ fontSize: "14px" }}
/>
</Card>
</div>

{/* Installation Instructions */}
{/* Installation Instructions - Hidden on mobile */}
<Card
className="border-0 bg-[#1B1D21] p-4"
className="hidden md:block border-0 bg-[#1B1D21] p-4"
style={{
border: "1px solid rgba(255, 255, 255, 0.04)",
boxShadow: "rgba(0, 0, 0, 0.06) 0px 18px 25.8px 0px",
Expand Down Expand Up @@ -258,53 +298,84 @@ export default function PublicRulePage() {
</div>
</Card>

{/* Action Buttons */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 sm:gap-0 pt-[0]">
<div className="flex items-center gap-3 flex-wrap">
<Button
onClick={handleDownload}
variant="secondary"
size="sm"
>
<Download className="h-3 w-3" />
Download
</Button>

<Button
onClick={handleCopy}
variant="secondary"
size="sm"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
<title>duplicate</title>
<g fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" stroke="#70A7D7">
<path opacity="0.3" d="M13.75 5.25H7.25C6.145 5.25 5.25 6.145 5.25 7.25V13.75C5.25 14.855 6.145 15.75 7.25 15.75H13.75C14.855 15.75 15.75 14.855 15.75 13.75V7.25C15.75 6.145 14.855 5.25 13.75 5.25Z" fill="#70A7D7" data-stroke="none" stroke="none"></path>
<path d="M13.75 5.25H7.25C6.145 5.25 5.25 6.145 5.25 7.25V13.75C5.25 14.855 6.145 15.75 7.25 15.75H13.75C14.855 15.75 15.75 14.855 15.75 13.75V7.25C15.75 6.145 14.855 5.25 13.75 5.25Z"></path>
<path d="M12.4012 2.74998C12.0022 2.06148 11.2151 1.64837 10.38 1.77287L3.45602 2.80199C2.36402 2.96389 1.61003 3.98099 1.77203 5.07399L2.75002 11.6548"></path>
</g>
</svg>
{copied ? "Copied!" : "Copy Rule"}
</Button>
{/* Spacer for floating action bar */}
<div className="h-20"></div>
</div>
</main>

{/* Glassmorphic Floating Action Bar */}
<div className="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-40 max-w-[calc(100vw-2rem)]">
<div className="bg-neutral-300/20 dark:bg-neutral-400/20 text-neutral-600 dark:text-neutral-300 backdrop-blur-[1px] border border-neutral-400/20 rounded-xl p-2 sm:p-3 shadow-2xl overflow-x-auto">
<div className="flex items-center gap-2 sm:gap-3 min-w-max">
{/* Primary Actions */}
<Button
onClick={handleDownload}
variant="primary"
size="sm"
className="hover:bg-[#70A7D7] px-2 py-2 sm:px-3 sm:py-1 aspect-square sm:aspect-auto"
>
<Download className="h-3 w-3" />
<span className="hidden md:inline ml-1">Download</span>
</Button>
Comment on lines +311 to +319
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add aria-labels for icon-only buttons on mobile.

Improves screen reader accessibility.

             <Button
               onClick={handleDownload}
               variant="primary"
               size="sm"
               className="hover:bg-[#70A7D7] px-2 py-2 sm:px-3 sm:py-1 aspect-square sm:aspect-auto"
+              aria-label="Download rule"
             >
...
             <Button
               onClick={handleCopy}
               variant="primary"
               size="sm"
               className="hover:bg-[#90BAE0] px-2 py-2 sm:px-3 sm:py-1 aspect-square sm:aspect-auto"
+              aria-label="Copy rule"
             >
...
-            <Button
-              onClick={handleInstallCopy}
+            <Button
+              onClick={handleInstallCopy}
               variant="primary"
               size="sm"
               className="hover:bg-[#90BAE0] px-2 py-2 sm:px-3 sm:py-1 aspect-square sm:aspect-auto"
+              aria-label="Copy CLI install command"
             >
...
             <Button
               onClick={handleCopyViewURL}
               variant="primary"
               size="sm"
               className="hover:bg-[#90BAE0] px-2 py-2 sm:px-3 sm:py-1 aspect-square sm:aspect-auto"
+              aria-label="Copy view URL"
             >

Also applies to: 321-329, 331-339, 341-349

🤖 Prompt for AI Agents
In app/rule/[slug]/page.tsx around lines 311-319 (and similarly for 321-329,
331-339, 341-349), several Button components render as icon-only on mobile
without aria-labels; add an appropriate aria-label prop to each icon-only Button
(e.g., aria-label="Download", "Copy", "Share", etc.) ensuring labels are
descriptive and match the button action so screen readers can announce their
purpose, and keep existing visible text for larger viewports unchanged.


<Button
onClick={handleCopy}
variant="primary"
size="sm"
className="hover:bg-[#90BAE0] px-2 py-2 sm:px-3 sm:py-1 aspect-square sm:aspect-auto"
>
<Copy className="h-3 w-3" />
<span className="hidden md:inline ml-1">{copied ? "Copied!" : "Copy Rule"}</span>
</Button>

{/* Add to List button - only show if user is authenticated */}
{session?.user && (
<Button
onClick={handleCopyCLI}
variant="primary"
size="sm"
className="hover:bg-[#90BAE0] px-2 py-2 sm:px-3 sm:py-1 aspect-square sm:aspect-auto hidden md:flex"
>
<Terminal className="h-3 w-3" />
<span className="hidden md:inline ml-1">{cliCopied ? "Copied!" : "CLI"}</span>
</Button>

<Button
onClick={handleCopyViewURL}
variant="primary"
size="sm"
className="hover:bg-[#90BAE0] px-2 py-2 sm:px-3 sm:py-1 aspect-square sm:aspect-auto"
>
<Share2 className="h-3 w-3" />
<span className="hidden md:inline ml-1">{urlCopied ? "Copied!" : "URL"}</span>
</Button>

{/* Add to List button - only show if user is authenticated */}
{session?.user && (
<>
<div className="h-4 sm:h-6 w-px bg-neutral-400/30 mx-1"></div>
<AddToListButton
ruleId={rule.id}
ruleTitle={rule.title}
/>
)}

<div className="flex items-center gap-1 text-xs text-gray-500">
<Eye className="h-3 w-3" />
{rule.views.toLocaleString()} views
</div>
</>
)}

{/* Secondary Info */}
<div className="h-4 sm:h-6 w-px bg-neutral-400/30 mx-1"></div>

<div className="flex items-center gap-1 text-xs text-neutral-400">
<Eye className="h-3 w-3" />
<span className="hidden sm:inline">{rule.views.toLocaleString()} views</span>
<span className="sm:hidden">{rule.views.toLocaleString()}</span>
</div>
<div className="text-sm text-gray-500 whitespace-nowrap order-first sm:order-last">
{rule.content.length} chars • {tokenCount.toLocaleString()} tokens

<div className="text-xs text-neutral-400 pl-1 sm:pl-2 flex-shrink-0">
<div className="hidden sm:block">{rule.content.length} chars</div>
<div className="hidden sm:block">{tokenCount.toLocaleString()} tokens</div>
<div className="sm:hidden">{Math.round(rule.content.length/1000)}k</div>
</div>
</div>
</div>
</main>
</div>
</div>
)
}
}