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
90 changes: 90 additions & 0 deletions apps/web/src/components/web/hero-title-install-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"use client";

import { AnimatePresence, motion } from "framer-motion";
import { Check, ChevronRight, Copy } from "lucide-react";
import { useCallback, useState } from "react";

import { Button } from "../ui/button";

export default function HeroTitleInstallButton() {
const [copied, setCopied] = useState(false);
const [hovered, setHovered] = useState(false);

const handleCopy = useCallback(() => {
void navigator.clipboard.writeText("npx paykitjs init");
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}, []);
Comment on lines +13 to +17
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clear previous reset timers before creating a new one.

Line 16 starts a new timer on every click, so rapid re-clicks can flip copied back to false too early.

Suggested fix
-import { useCallback, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";

 export default function HeroTitleInstallButton() {
   const [copied, setCopied] = useState(false);
   const [hovered, setHovered] = useState(false);
+  const resetTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

   const handleCopy = useCallback(() => {
     void navigator.clipboard.writeText("npx paykitjs init");
     setCopied(true);
-    setTimeout(() => setCopied(false), 1500);
+    if (resetTimerRef.current) clearTimeout(resetTimerRef.current);
+    resetTimerRef.current = setTimeout(() => setCopied(false), 1500);
   }, []);
+
+  useEffect(() => {
+    return () => {
+      if (resetTimerRef.current) clearTimeout(resetTimerRef.current);
+    };
+  }, []);
📝 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 handleCopy = useCallback(() => {
void navigator.clipboard.writeText("npx paykitjs init");
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}, []);
import { useCallback, useEffect, useRef, useState } from "react";
export default function HeroTitleInstallButton() {
const [copied, setCopied] = useState(false);
const [hovered, setHovered] = useState(false);
const resetTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleCopy = useCallback(() => {
void navigator.clipboard.writeText("npx paykitjs init");
setCopied(true);
if (resetTimerRef.current) clearTimeout(resetTimerRef.current);
resetTimerRef.current = setTimeout(() => setCopied(false), 1500);
}, []);
useEffect(() => {
return () => {
if (resetTimerRef.current) clearTimeout(resetTimerRef.current);
};
}, []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/web/hero-title-install-button.tsx` around lines 13 -
17, handleCopy starts a new timeout on every click which can reset copied too
early; store the timeout id in a ref (e.g., copyTimeoutRef) and call
clearTimeout(copyTimeoutRef.current) before creating a new setTimeout, update
copyTimeoutRef.current with the new id, and ensure you clear the timeout on
unmount (useEffect cleanup) so setCopied(false) cannot run after the component
is gone.


return (
<Button
variant="ghost"
onClick={handleCopy}
size="lg"
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
className="group relative gap-1.5 rounded-none border-transparent pr-3.5 h-9.5 text-xs font-medium text-neutral-600 hover:bg-transparent sm:text-sm dark:text-neutral-400 dark:text-foreground/75 dark:hover:bg-transparent"
>
{/* Diagonal lines background */}
<span
className="absolute inset-0 opacity-[0.13] transition-opacity group-hover:opacity-[0.18]"
style={{
backgroundImage: `repeating-linear-gradient(
-45deg,
transparent,
transparent 4px,
currentColor 4px,
currentColor 5px
)`,
}}
/>
{/* Top border */}
<span className="bg-foreground/22 group-hover:bg-foreground/30 absolute top-0 -right-[6px] -left-[6px] h-px transition-colors" />
{/* Bottom border */}
<span className="bg-foreground/22 group-hover:bg-foreground/30 absolute -right-[6px] bottom-0 -left-[6px] h-px transition-colors" />
{/* Left border */}
<span className="bg-foreground/22 group-hover:bg-foreground/30 absolute -top-[6px] -bottom-[6px] left-0 w-px transition-colors" />
{/* Right border */}
<span className="bg-foreground/22 group-hover:bg-foreground/30 absolute -top-[6px] right-0 -bottom-[6px] w-px transition-colors" />
<span className="relative flex size-4.5 items-center justify-center">
<AnimatePresence mode="wait">
{copied ? (
<motion.span
key="check"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.15 }}
className="absolute flex items-center justify-center"
>
<Check className="text-foreground/50 size-3.5" />
</motion.span>
) : hovered ? (
<motion.span
key="copy"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.15 }}
className="absolute flex items-center justify-center"
>
<Copy className="text-foreground/50 size-3.5" />
</motion.span>
) : (
<motion.span
key="chevron"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.15 }}
className="absolute flex items-center justify-center"
>
<ChevronRight className="text-foreground/30 size-4.5" />
</motion.span>
)}
</AnimatePresence>
</span>
<code className="text-foreground/90 relative font-mono">npx paykitjs init</code>
</Button>
);
}
153 changes: 63 additions & 90 deletions apps/web/src/components/web/hero-title.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
"use client";

import { AnimatePresence, motion } from "framer-motion";
import { Check, ChevronRight, Copy, Sparkle } from "lucide-react";
import { Sparkle } from "lucide-react";
import Link from "next/link";
import { useCallback, useState } from "react";

import { PROMPT } from "@/lib/consts";

import { Button } from "../ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../ui/dialog";
import { ScrollArea } from "../ui/scroll-area";
import { useCopyButton } from "../ui/use-copy-button";
import HeroTitleInstallButton from "./hero-title-install-button";

export function HeroTitle() {
const [copied, setCopied] = useState(false);
const [hovered, setHovered] = useState(false);

const handleCopy = useCallback(() => {
void navigator.clipboard.writeText("npx paykitjs init");
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}, []);
const [copied, handleCopy] = useCopyButton(() => navigator.clipboard.writeText(PROMPT));

return (
<div className="relative flex w-full flex-col items-center text-center lg:items-start lg:text-left">
Expand All @@ -39,85 +44,53 @@ export function HeroTitle() {
inside your app.
</p>

<div className="mt-6 flex flex-wrap items-center justify-center gap-3 sm:mt-8 sm:gap-4 lg:mt-12 lg:justify-start">
<Button
render={<Link href="/docs" />}
nativeButton={false}
size="lg"
className="px-4 h-9.5"
variant="default"
>
Read Docs
</Button>
<Button
variant="ghost"
onClick={handleCopy}
size="lg"
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
className="group relative gap-1.5 rounded-none border-transparent pr-3.5 h-9.5 text-xs font-medium text-neutral-600 hover:bg-transparent sm:text-sm dark:text-neutral-400 dark:text-foreground/75 dark:hover:bg-transparent"
>
{/* Diagonal lines background */}
<span
className="absolute inset-0 opacity-[0.13] transition-opacity group-hover:opacity-[0.18]"
style={{
backgroundImage: `repeating-linear-gradient(
-45deg,
transparent,
transparent 4px,
currentColor 4px,
currentColor 5px
)`,
}}
/>
{/* Top border */}
<span className="bg-foreground/22 group-hover:bg-foreground/30 absolute top-0 -right-[6px] -left-[6px] h-px transition-colors" />
{/* Bottom border */}
<span className="bg-foreground/22 group-hover:bg-foreground/30 absolute -right-[6px] bottom-0 -left-[6px] h-px transition-colors" />
{/* Left border */}
<span className="bg-foreground/22 group-hover:bg-foreground/30 absolute -top-[6px] -bottom-[6px] left-0 w-px transition-colors" />
{/* Right border */}
<span className="bg-foreground/22 group-hover:bg-foreground/30 absolute -top-[6px] right-0 -bottom-[6px] w-px transition-colors" />
<span className="relative flex size-4.5 items-center justify-center">
<AnimatePresence mode="wait">
{copied ? (
<motion.span
key="check"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.15 }}
className="absolute flex items-center justify-center"
>
<Check className="text-foreground/50 size-3.5" />
</motion.span>
) : hovered ? (
<motion.span
key="copy"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.15 }}
className="absolute flex items-center justify-center"
>
<Copy className="text-foreground/50 size-3.5" />
</motion.span>
) : (
<motion.span
key="chevron"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.15 }}
className="absolute flex items-center justify-center"
>
<ChevronRight className="text-foreground/30 size-4.5" />
</motion.span>
)}
</AnimatePresence>
</span>
<code className="text-foreground/90 relative font-mono">npx paykitjs init</code>
</Button>
<div className="mt-6 flex flex-col items-center gap-2 sm:mt-8 lg:mt-12 lg:items-start">
<div className="flex flex-wrap items-start justify-center gap-3 sm:gap-4 lg:justify-start">
<div className="flex flex-col items-center gap-2 lg:items-start">
<Button
render={<Link href="/docs" />}
nativeButton={false}
size="lg"
className="px-4 h-9.5"
variant="default"
>
Read Docs
</Button>
<Dialog>
<DialogTrigger
render={
<Button
variant="link"
size="sm"
className="h-auto px-0 text-foreground/60 hover:text-foreground"
/>
}
>
or copy the prompt
</DialogTrigger>
<DialogContent className="sm:max-w-xl">
<DialogHeader>
<DialogTitle>Copy the prompt</DialogTitle>
<DialogDescription>Give this to your agent to get started.</DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-56 w-full rounded-md border border-foreground/10 bg-muted/40">
<div className="px-3 py-2">
<code className="font-mono text-xs text-foreground/90 whitespace-pre-wrap break-words sm:text-sm">
{PROMPT}
</code>
</div>
</ScrollArea>
<DialogFooter>
<Button variant="outline" onClick={handleCopy}>
{copied ? "Copied" : "Copy command"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>

<HeroTitleInstallButton />
</div>
</div>
</div>
</div>
Expand Down
12 changes: 12 additions & 0 deletions apps/web/src/lib/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,15 @@ export const homePageStructuredData = [
softwareApplicationSchema,
faqSchema,
];

export const PROMPT = [
"Set up authentication in my project using Paykit (paykitjs npm package)",
"",
"1. Install paykitjs. If I already have a database configured in this project, use that - don't set up a new one.",
"2. Check the runtime used in the project and use the same - don't simply go with npm.",
"3. Create `src/lib/paykit.ts` - call createPayKit() with:",
" - My existing database connection by passing the url directly `database: <db_url>` (or a new pg.Pool() if none exists)",
" - Add payment provider based on the auth credentials in my env. Example: `bun add @paykitjs/stripe`",
"4. Set up the route handlers according to the current project framework.",
"5. Create `src/lib/paykit-client.ts` - call `createPayKitClient<typeof paykit>()` where `paykit` is the object we created when initializing Paykit in `src/lib/paykit.ts`.",
].join("\n");