+
diff --git a/src/app/pro/components/advancedTools.tsx b/src/app/pro/components/advancedTools.tsx
new file mode 100644
index 00000000..a82e217d
--- /dev/null
+++ b/src/app/pro/components/advancedTools.tsx
@@ -0,0 +1,231 @@
+'use client'
+
+import React, { useEffect, useId, useState } from 'react'
+import Image from 'next/image'
+import register from 'public/icons/registration-primary.svg'
+import Search from 'public/icons/search-primary.svg'
+import Grid from 'public/icons/grid-primary.svg'
+import Watchlist from 'public/icons/watchlist-primary.svg'
+import Message from 'public/icons/chat.svg'
+import Notification from 'public/icons/bell-primary.svg'
+import View from 'public/icons/view-primary.svg'
+import grailsAI from 'public/icons/grails-ai.svg'
+import { AnimatePresence, motion } from "motion/react";
+import { useClickAway } from '@/hooks/useClickAway'
+import { Cross } from 'ethereum-identity-kit'
+import GrailsPoap2025 from 'public/art/grails-poap-2025.webp'
+import PrimaryButton from '@/components/ui/buttons/primary'
+
+const tools = [
+ {
+ title: 'Bulk Offers',
+ description: 'Create offers for multiple names at the same time.',
+ longDescription: 'This tool allows you to create offers for multiple name at once. You can easily set a base price, and then tweak offer amounts for individual names. ',
+ icon:
,
+ },
+ {
+ title: 'Google Metrics Filters',
+ description: 'Filter names by various google Google metrics like searches, CPC, and more.',
+ icon:
,
+ },
+ {
+ title: 'Custom Dashboard',
+ description: 'Create your custom dashboard. Add widgets to your dashboard to track your favorite metrics.',
+ icon:
,
+ },
+ {
+ title: 'Multiple Watchlists',
+ description: 'Create multiple watchlists to track your favorite names.',
+ icon:
,
+ },
+ {
+ title: 'Private Chat Group',
+ description:
+ 'Get access to the exclusive Grails Pro chat group, where you can chat with biggest domainers in the game.',
+ icon:
,
+ },
+ {
+ title: 'Feature Notifications',
+ description: 'Be the first to know about new features and updates to Grails.',
+ icon:
,
+ },
+ {
+ title: 'See view details',
+ description: 'See exactly who and when viewed your profile or name pages.',
+ icon:
,
+ },
+ {
+ title: 'AI Search',
+ description: 'Get AI recommended search terms based on your search history and market conditions.',
+ longDescription: 'Individual page on Grails, which uses our internal AI to recommend search terms based on your search history and market conditions. You can provide a list of all the names that you are interested in and the AI will recommend search terms based on your history and market conditions.',
+ icon:
,
+ },
+]
+
+const AdvancedTools = () => {
+ const [active, setActive] = useState<(typeof tools)[number] | boolean | null>(
+ null
+ );
+ const ref = useClickAway
(() => setActive(null));
+ const id = useId();
+
+ useEffect(() => {
+ function onKeyDown(event: KeyboardEvent) {
+ if (event.key === "Escape") {
+ setActive(null);
+ }
+ }
+
+ if (active && typeof active === "object") {
+ document.body.style.overflow = "hidden";
+ } else {
+ document.body.style.overflow = "auto";
+ }
+
+ window.addEventListener("keydown", onKeyDown);
+ return () => window.removeEventListener("keydown", onKeyDown);
+ }, [active]);
+
+ return (
+ <>
+
+ {active && typeof active === "object" && (
+
+ )}
+
+
+ {active && typeof active === "object" ? (
+
+
setActive(null)}
+ >
+
+
+
+
+
+
+
+
+
+
+
+ {active.title}
+
+
+ {active.description}
+
+
+
+
+
+ {active.longDescription || active.description}
+
+
+
+
+ Upgrade to Pro
+
+
+
+
+
+ ) : null}
+
+
+
+ {/*
+
Professional Domainer Tools
+
+ Save gas and time. For serious domainers.
+
+
*/}
+
+ {tools.map((tool) => (
+
setActive(tool)}
+ >
+
+ {tool.icon}
+
+ {tool.title}
+
+
+
+ {tool.description}
+
+
+ ))}
+
+
+ {/*
*/}
+
+ >
+ )
+}
+
+export default AdvancedTools
diff --git a/src/app/pro/components/proPageContent.tsx b/src/app/pro/components/proPageContent.tsx
new file mode 100644
index 00000000..2b6fe5eb
--- /dev/null
+++ b/src/app/pro/components/proPageContent.tsx
@@ -0,0 +1,318 @@
+'use client'
+
+import { useAppDispatch } from '@/state/hooks'
+import { openUpgradeModalWithTier } from '@/state/reducers/modals/upgradeModal'
+import { cn } from '@/utils/tailwind'
+
+// ── Tier definitions ──────────────────────────────────────────────
+const ANNUAL_DISCOUNT = 0.15
+
+const formatPrice = (price: number) => {
+ return '$' + price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
+}
+
+const tiers = [
+ {
+ tierId: 1,
+ name: 'Plus',
+ monthlyPrice: 19.99,
+ lifetime: null,
+ color: 'text-white',
+ borderColor: 'border-white/40',
+ headerBg: 'bg-white/5',
+ buttonStyle: 'border-white text-white hover:bg-white/10',
+ buttonText: 'Get Plus',
+ },
+ {
+ tierId: 2,
+ name: 'Pro',
+ monthlyPrice: 49.99,
+ lifetime: null,
+ color: 'text-primary',
+ borderColor: 'border-primary/40',
+ headerBg: 'bg-primary/5',
+ buttonStyle: 'border-primary text-primary hover:bg-primary/10',
+ buttonText: 'Get Pro',
+ },
+ {
+ tierId: 3,
+ name: 'Gold',
+ monthlyPrice: 99.99,
+ lifetime: null,
+ color: 'text-amber-500',
+ borderColor: 'border-amber-500/40',
+ headerBg: 'bg-amber-500/5',
+ buttonStyle: 'border-amber-500 text-amber-500 hover:bg-amber-500/10',
+ buttonText: 'Get Gold',
+ },
+ {
+ tierId: 4,
+ name: 'Patron',
+ monthlyPrice: 10000,
+ lifetime: null,
+ color: 'text-purple-400',
+ borderColor: 'border-purple-400/40',
+ headerBg: 'bg-purple-400/5',
+ buttonStyle: 'border-purple-400 text-purple-400 hover:bg-purple-400/10',
+ buttonText: 'Become Patron',
+ },
+ // {
+ // tierId: 5,
+ // name: 'Legend',
+ // monthlyPrice: null,
+ // lifetime: '$1,000,000.00',
+ // color: 'text-red-400',
+ // borderColor: 'border-red-400/40',
+ // headerBg: 'bg-red-400/5',
+ // buttonStyle: 'border-red-400 text-red-400 hover:bg-red-400/10',
+ // buttonText: 'Contact Us',
+ // },
+]
+
+// ── Feature matrix ────────────────────────────────────────────────
+// Each feature has a label and a boolean per tier [Plus, Pro, Gold, Patron, Legend]
+const features: { label: string; tiers: boolean[] }[] = [
+ { label: 'Badge on profile and avatar/header', tiers: [true, true, true, true] },
+ { label: 'Google Metrics Filter/Sort', tiers: [true, true, true, true] },
+ { label: 'Bulk Offers', tiers: [true, true, true, true] },
+ { label: 'n of many Bulk Offers', tiers: [true, true, true, true] },
+ { label: 'Telegram notifications', tiers: [true, true, true, true] },
+ { label: 'First notification of new features and categories', tiers: [true, true, true, true] },
+ { label: 'Your listings get in Featured Listings', tiers: [true, true, true, true] },
+ { label: 'Customizable Dashboard', tiers: [false, true, true, true] },
+ { label: 'Multiple watchlists', tiers: [false, true, true, true] },
+ { label: 'Who viewed your Profile', tiers: [false, true, true, true] },
+ { label: 'Who viewed your Name pages', tiers: [false, true, true, true] },
+ { label: 'AI Recommendation page', tiers: [false, true, true, true] },
+ { label: 'Saved Search/Filter/Sort', tiers: [false, true, true, true] },
+ { label: 'Priority Support', tiers: [false, true, true, true] },
+ { label: 'Get name on Sponsorship page (optional)', tiers: [false, false, true, true] },
+ { label: 'Private Chat group', tiers: [false, false, true, true] },
+ { label: 'Monthly video chat with team', tiers: [false, false, false, true] },
+]
+
+// ── Component ─────────────────────────────────────────────────────
+const ProPageContent = () => {
+ const dispatch = useAppDispatch()
+
+ return (
+ <>
+ {/* Desktop */}
+
+
+ {/* Tier header row */}
+
+
+ |
+ {tiers.map((tier) => (
+
+ {tier.name}
+ |
+ ))}
+
+
+ {/* Pricing rows */}
+
+ | Monthly |
+ {tiers.map((tier) => (
+
+ {tier.monthlyPrice != null ? formatPrice(tier.monthlyPrice) : —}
+ |
+ ))}
+
+
+ |
+
+ Annual
+
+ -15%
+
+
+ |
+ {tiers.map((tier) => {
+ if (tier.monthlyPrice == null) {
+ return (
+
+ —
+ |
+ )
+ }
+ const fullYear = tier.monthlyPrice * 12
+ const discountedYear = fullYear * (1 - ANNUAL_DISCOUNT)
+ return (
+
+ {formatPrice(fullYear)}
+ {formatPrice(discountedYear)}
+ |
+ )
+ })}
+
+
+ | Lifetime |
+ {tiers.map((tier) => (
+
+ {tier.lifetime ?? —}
+ |
+ ))}
+
+
+
+ {features.map((feature, i) => (
+
+ | {feature.label} |
+ {feature.tiers.map((included, j) => {
+ const checkBackgroundColor = tiers[j].color.replace('text-', 'bg-')
+
+ return (
+
+ {included ? (
+
+ ✓
+
+ ) : (
+ —
+ )}
+ |
+ )
+ })}
+
+ ))}
+
+
+
+ |
+ {tiers.map((tier) => (
+
+
+ |
+ ))}
+
+
+
+
+
+ {/* Mobile */}
+
+ {tiers.map((tier) => (
+
+
{tier.name}
+
+ {/* Pricing */}
+
+ {tier.monthlyPrice != null && (
+
+ Monthly:{' '}
+ {formatPrice(tier.monthlyPrice)}
+
+ )}
+ {tier.monthlyPrice != null && (
+
+ Annual:{' '}
+ {formatPrice(tier.monthlyPrice * 12)}
+
+ {formatPrice(tier.monthlyPrice * 12 * (1 - ANNUAL_DISCOUNT))}
+
+
+ -15%
+
+
+ )}
+ {tier.lifetime && (
+
+ Lifetime: {tier.lifetime}
+
+ )}
+
+
+
+ {features.map((feature) => {
+ const tierIndex = tiers.findIndex((t) => t.name === tier.name)
+ const included = feature.tiers[tierIndex]
+ return (
+ -
+
+ {included ? '✓' : '—'}
+
+ {feature.label}
+
+ )
+ })}
+
+
+ ))}
+
+ >
+ )
+}
+
+export default ProPageContent
diff --git a/src/app/pro/page.tsx b/src/app/pro/page.tsx
new file mode 100644
index 00000000..452710ed
--- /dev/null
+++ b/src/app/pro/page.tsx
@@ -0,0 +1,47 @@
+import { Metadata } from 'next'
+import { Suspense } from 'react'
+import Footer from '@/components/footer'
+import ProPageContent from './components/proPageContent'
+import AdvancedTools from './components/advancedTools'
+
+export const metadata: Metadata = {
+ title: 'Grails Pro',
+ description: 'Unlock premium features with Grails Pro subscriptions',
+ openGraph: {
+ title: 'Grails Pro',
+ description: 'Unlock premium features with Grails Pro subscriptions',
+ siteName: 'Grails',
+ url: 'https://grails.app/pro',
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: 'Grails Pro',
+ description: 'Unlock premium features with Grails Pro subscriptions',
+ },
+}
+
+const ProPage = () => {
+ return (
+
+
+ {/* Hero */}
+
+
+ Do more, with Grails Pro
+
+
+ Supercharge your ENS experience with premium tools, analytics, and exclusive access.
+
+
+
+
+
+
+
+ )
+}
+
+export default ProPage
diff --git a/src/components/home/pricingTiers.tsx b/src/components/home/pricingTiers.tsx
new file mode 100644
index 00000000..ca7ab306
--- /dev/null
+++ b/src/components/home/pricingTiers.tsx
@@ -0,0 +1,169 @@
+'use client'
+
+import { useState } from 'react'
+import Link from 'next/link'
+import { useAppDispatch } from '@/state/hooks'
+import { openUpgradeModalWithTier } from '@/state/reducers/modals/upgradeModal'
+import { cn } from '@/utils/tailwind'
+
+const tiers = [
+ {
+ tierId: 1,
+ name: 'Plus',
+ monthlyPrice: 19.99,
+ buttonText: 'Enroll',
+ borderColor: 'border-white',
+ nameColor: 'text-white',
+ priceColor: 'text-white',
+ buttonStyle: 'border-white text-white hover:bg-white/10',
+ features: [
+ 'Bulk Offers',
+ 'Name views info',
+ 'AI recommended search',
+ 'Customizable dashboard',
+ 'Multiple watchlists',
+ 'Profile views info',
+ 'Filter/Sort by google metrics',
+ ],
+ },
+ {
+ tierId: 2,
+ name: 'PRO',
+ monthlyPrice: 49.99,
+ buttonText: 'Become a PRO user',
+ borderColor: 'border-primary',
+ nameColor: 'text-primary',
+ priceColor: 'text-primary',
+ buttonStyle: 'border-primary text-primary hover:bg-primary/10',
+ features: ['Everything in Plus', 'Exclusive new feature notifications', 'Customizable dashboard'],
+ },
+ {
+ tierId: 3,
+ name: 'Gold',
+ monthlyPrice: 99.99,
+ buttonText: 'Get Gold',
+ borderColor: 'border-amber-500',
+ nameColor: 'text-amber-500',
+ priceColor: 'text-amber-500',
+ buttonStyle: 'border-amber-500 text-amber-500 hover:bg-amber-500/10',
+ features: ['Everything in Plus', 'Everything in PRO', 'Exclusive Grails Whales Telegram'],
+ },
+]
+
+type BillingPeriod = 'monthly' | 'yearly'
+
+const PricingTiers = () => {
+ const dispatch = useAppDispatch()
+ const [billing, setBilling] = useState('monthly')
+
+ const isYearly = billing === 'yearly'
+
+ const ANNUAL_DISCOUNT = 0.15
+
+ const getDisplayPrice = (monthlyPrice: number) => {
+ if (isYearly) {
+ // 15% off yearly: (monthly * 12) * 0.85 / 12
+ return ((monthlyPrice * 12 * (1 - ANNUAL_DISCOUNT)) / 12).toFixed(2)
+ }
+ return monthlyPrice.toFixed(2)
+ }
+
+ const getYearlyTotal = (monthlyPrice: number) => {
+ return (monthlyPrice * 12 * (1 - ANNUAL_DISCOUNT)).toFixed(2)
+ }
+
+ const getOriginalYearlyTotal = (monthlyPrice: number) => {
+ return (monthlyPrice * 12).toFixed(2)
+ }
+
+ return (
+
+
+ Do more, with Grails Pro
+
+
+ {/* Billing toggle */}
+
+
+
+
+
+
+
+
+
+ {tiers.map((tier) => (
+
+
{tier.name}
+
+
+ {isYearly &&
${tier.monthlyPrice.toFixed(2)}/mo
}
+
+ ${getDisplayPrice(tier.monthlyPrice)}{' '}
+ per Month
+
+ {isYearly && (
+
+ ${getOriginalYearlyTotal(tier.monthlyPrice)}{' '}
+ ${getYearlyTotal(tier.monthlyPrice)}/year
+
+ )}
+
+
+
+
+ {tier.features.map((feature) => (
+ -
+ {feature}
+
+ ))}
+
+
+ ))}
+
+
+
+ See all plans & compare features →
+
+
+ )
+}
+
+export default PricingTiers
diff --git a/src/components/modal/offer/bulkOfferModal.tsx b/src/components/modal/offer/bulkOfferModal.tsx
new file mode 100644
index 00000000..c77e6235
--- /dev/null
+++ b/src/components/modal/offer/bulkOfferModal.tsx
@@ -0,0 +1,332 @@
+'use client'
+
+import { useMemo, useState } from 'react'
+import { MarketplaceDomainType } from '@/types/domains'
+import Dropdown, { DropdownOption } from '@/components/ui/dropdown'
+import WrappedEtherIcon from 'public/tokens/weth-circle.svg'
+import UsdcIcon from 'public/tokens/usdc.svg'
+import Input from '@/components/ui/input'
+import PrimaryButton from '@/components/ui/buttons/primary'
+import SecondaryButton from '@/components/ui/buttons/secondary'
+import DatePicker from '@/components/ui/datepicker'
+import Image from 'next/image'
+import Calendar from 'public/icons/calendar.svg'
+import { DAY_IN_SECONDS } from '@/constants/time'
+import { Check } from 'ethereum-identity-kit'
+import { useSeaportContext } from '@/context/seaport'
+import { mainnet } from 'viem/chains'
+import { useAccount, useBalance } from 'wagmi'
+import { parseUnits } from 'viem'
+import { WETH_ADDRESS, USDC_ADDRESS, TOKEN_DECIMALS } from '@/constants/web3/tokens'
+import { beautifyName } from '@/lib/ens'
+import Label from '@/components/ui/label'
+import { cn } from '@/utils/tailwind'
+import ArrowDownIcon from 'public/icons/arrow-down.svg'
+
+type BulkOfferStatus = 'review' | 'signing' | 'submitting' | 'success' | 'error'
+
+interface BulkOfferModalProps {
+ onClose: () => void
+ domains: MarketplaceDomainType[]
+}
+
+const BulkOfferModal: React.FC = ({ onClose, domains }) => {
+ const { createBulkOffer, isLoading } = useSeaportContext()
+ const { address } = useAccount()
+
+ const [status, setStatus] = useState('review')
+ const [errorMessage, setErrorMessage] = useState(null)
+ const [showDatePicker, setShowDatePicker] = useState(false)
+ const [showIndividualPrices, setShowIndividualPrices] = useState(false)
+ const [currency, setCurrency] = useState<'WETH' | 'USDC'>('WETH')
+ const [defaultPrice, setDefaultPrice] = useState('')
+ const [individualPrices, setIndividualPrices] = useState
+ {/* Subscription Indicator */}
+ {hasActiveSubscription ? (
+