Skip to content
Merged
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
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@radix-ui/react-checkbox": "1.1.3",
"@radix-ui/react-collapsible": "1.1.2",
"@radix-ui/react-context-menu": "2.2.4",
"@radix-ui/react-dialog": "1.1.4",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "2.1.4",
"@radix-ui/react-hover-card": "1.1.4",
"@radix-ui/react-label": "2.1.1",
Expand Down
9 changes: 5 additions & 4 deletions src/components/call-to-action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { ArrowRight, BookOpen, Copy, Check } from "lucide-react"
import { WaitlistModal } from "@/components/waitlist-modal"
import { useState } from "react"

export function CallToAction() {
Expand All @@ -25,12 +26,12 @@ export function CallToAction() {
</p>

<div className="flex flex-col sm:flex-row justify-center gap-4">
<Button asChild size="lg" className="bg-emerald-500 hover:bg-emerald-600 h-12 px-8">
<Link href="#pricing">
<WaitlistModal source="footer_cta">
<Button size="lg" className="bg-emerald-500 hover:bg-emerald-600 h-12 px-8">
Get Started Free
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</Button>
</WaitlistModal>
<Button
asChild
size="lg"
Expand Down
26 changes: 15 additions & 11 deletions src/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
import { Github, Menu, Terminal, Star } from "lucide-react"
import { WaitlistModal } from "@/components/waitlist-modal"

export function Header() {
// Controlled state for mobile sheet - ensures it closes on navigation
Expand Down Expand Up @@ -60,10 +61,12 @@ export function Header() {
</Badge>
</Link>

{/* Primary CTA - Links to pricing instead of non-existent sign-up */}
<Button asChild size="sm" className="bg-emerald-500 hover:bg-emerald-600 text-white">
<Link href="#pricing">Get Started</Link>
</Button>
{/* Primary CTA */}
<WaitlistModal source="nav">
<Button size="sm" className="bg-emerald-500 hover:bg-emerald-600 text-white">
Get Started
</Button>
</WaitlistModal>

{/* Mobile Menu */}
<Sheet open={open} onOpenChange={setOpen}>
Expand Down Expand Up @@ -96,13 +99,14 @@ export function Header() {
<Github className="w-4 h-4" />
View on GitHub
</Link>
<Button
asChild
className="bg-emerald-500 hover:bg-emerald-600 text-white"
onClick={handleLinkClick}
>
<Link href="#pricing">Get Started</Link>
</Button>
<WaitlistModal source="nav_mobile">
<Button
className="bg-emerald-500 hover:bg-emerald-600 text-white"
onClick={handleLinkClick}
>
Get Started
</Button>
</WaitlistModal>
</div>
</nav>
</SheetContent>
Expand Down
9 changes: 5 additions & 4 deletions src/components/hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { ArrowRight, Github, Terminal } from "lucide-react"
import { WaitlistModal } from "@/components/waitlist-modal"

export function Hero() {
// Typewriter animation state
Expand Down Expand Up @@ -91,12 +92,12 @@ export function Hero() {

{/* CTA Buttons */}
<div className="flex flex-col sm:flex-row gap-4 justify-center mb-12">
<Button asChild className="bg-emerald-500 hover:bg-emerald-600 text-white h-12 px-8">
<Link href="/sign-up">
<WaitlistModal source="hero">
<Button className="bg-emerald-500 hover:bg-emerald-600 text-white h-12 px-8">
Get Started Free
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</Button>
</WaitlistModal>
<Button
asChild
variant="outline"
Expand Down
31 changes: 18 additions & 13 deletions src/components/pricing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Check, X, Sparkles } from "lucide-react"
import { WaitlistModal } from "@/components/waitlist-modal"
import { PLANS, ENTERPRISE_FEATURES, type PlanName, type BillingPeriod } from "@/lib/pricing"

// Transform PLANS object to array for rendering, excluding enterprise (shown separately)
Expand Down Expand Up @@ -117,17 +118,19 @@ export function Pricing() {
<p className="text-muted-foreground text-sm mt-2 mb-6">{plan.description}</p>

{/* CTA Button */}
<Button
disabled={billingPeriod === "lifetime" && !plan.hasLifetime}
className={`w-full mb-6 ${
plan.highlighted ? "bg-emerald-500 hover:bg-emerald-600 text-white" : ""
}`}
variant={plan.highlighted ? "default" : "outline"}
>
{billingPeriod === "lifetime" && !plan.hasLifetime
? "No Lifetime Option"
: plan.cta}
</Button>
<WaitlistModal source={`pricing_${plan.key}`}>
<Button
disabled={billingPeriod === "lifetime" && !plan.hasLifetime}
className={`w-full mb-6 ${
plan.highlighted ? "bg-emerald-500 hover:bg-emerald-600 text-white" : ""
}`}
variant={plan.highlighted ? "default" : "outline"}
>
{billingPeriod === "lifetime" && !plan.hasLifetime
? "No Lifetime Option"
: "Join Waitlist"}
</Button>
</WaitlistModal>

{/* Features List */}
<ul className="space-y-3 flex-1">
Expand Down Expand Up @@ -183,8 +186,10 @@ export function Pricing() {
))}
</div>
<div className="lg:w-auto">
<Button className="bg-purple-500 hover:bg-purple-600 text-white">
Contact Sales
<Button asChild className="bg-purple-500 hover:bg-purple-600 text-white">
<a href="mailto:david@replimap.com?subject=RepliMap Enterprise Inquiry">
Contact Sales
</a>
</Button>
</div>
</div>
Expand Down
143 changes: 143 additions & 0 deletions src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
"use client"

import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"

import { cn } from "@/lib/utils"

function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}

function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}

function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}

function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}

function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}

function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}

function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}

function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}

function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}

function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}

export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}
58 changes: 58 additions & 0 deletions src/components/waitlist-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"use client"

import { useState } from "react"
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"

const TALLY_FORM_ID = "2EaYae"
const BASE_TALLY_URL = `https://tally.so/r/${TALLY_FORM_ID}?transparentBackground=1&hideTitle=1`

interface WaitlistModalProps {
children: React.ReactNode
source?: string // Track where the user clicked (for analytics)
}

export function WaitlistModal({ children, source = "generic" }: WaitlistModalProps) {
const [open, setOpen] = useState(false)
const [isLoading, setIsLoading] = useState(true)

// Dynamic URL with source tracking
const formUrl = `${BASE_TALLY_URL}&source=${encodeURIComponent(source)}`

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent
className="sm:max-w-[550px] p-0 overflow-hidden bg-background border-border"
showCloseButton={true}
>
<DialogHeader className="sr-only">
<DialogTitle>Join the Waitlist</DialogTitle>
</DialogHeader>
{/* Dark background prevents white flash while iframe loads */}
<div className="w-full h-[750px] relative bg-background">
{/* Loading spinner */}
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-6 h-6 border-2 border-emerald-500 border-t-transparent rounded-full animate-spin" />
</div>
)}
<iframe
src={formUrl}
width="100%"
height="100%"
frameBorder="0"
title="Join Waitlist"
className="absolute inset-0 bg-transparent"
onLoad={() => setIsLoading(false)}
/>
</div>
</DialogContent>
</Dialog>
)
}