🚀 Implement tier-based scan intervals & improve Settings page
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
Backend (Scheduler): - Add check_domains_by_frequency() for tier-based scanning - Scout: Daily checks (at configured hour) - Trader: Hourly checks (every :00) - Tycoon: 10-minute real-time checks - Smart tier filtering to avoid duplicate checks Frontend (Pricing): - All feature text now white (text-foreground) Frontend (Settings/Billing): - Show current plan with visual stats (domains, interval, portfolio) - Display check frequency in human-readable format - Full plan comparison table - Green checkmarks for active features - Upgrade CTA for free users
This commit is contained in:
@ -11,6 +11,7 @@ from app.config import get_settings
|
|||||||
from app.database import AsyncSessionLocal
|
from app.database import AsyncSessionLocal
|
||||||
from app.models.domain import Domain, DomainCheck
|
from app.models.domain import Domain, DomainCheck
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
|
from app.models.subscription import Subscription, SubscriptionTier, TIER_CONFIG
|
||||||
from app.services.domain_checker import domain_checker
|
from app.services.domain_checker import domain_checker
|
||||||
from app.services.email_service import email_service
|
from app.services.email_service import email_service
|
||||||
from app.services.price_tracker import price_tracker
|
from app.services.price_tracker import price_tracker
|
||||||
@ -44,14 +45,38 @@ async def scrape_tld_prices():
|
|||||||
logger.exception(f"TLD price scrape failed: {e}")
|
logger.exception(f"TLD price scrape failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
async def check_all_domains():
|
async def check_domains_by_frequency(frequency: str):
|
||||||
"""Check availability of all monitored domains."""
|
"""Check availability of domains based on their subscription frequency.
|
||||||
logger.info("Starting daily domain check...")
|
|
||||||
|
Args:
|
||||||
|
frequency: One of 'daily', 'hourly', 'realtime' (10-min)
|
||||||
|
"""
|
||||||
|
logger.info(f"Starting {frequency} domain check...")
|
||||||
start_time = datetime.utcnow()
|
start_time = datetime.utcnow()
|
||||||
|
|
||||||
async with AsyncSessionLocal() as db:
|
async with AsyncSessionLocal() as db:
|
||||||
# Get all domains
|
# Get users with matching check frequency
|
||||||
result = await db.execute(select(Domain))
|
tiers_for_frequency = []
|
||||||
|
for tier, config in TIER_CONFIG.items():
|
||||||
|
if config['check_frequency'] == frequency:
|
||||||
|
tiers_for_frequency.append(tier)
|
||||||
|
# Realtime includes hourly and daily too (more frequent = superset)
|
||||||
|
elif frequency == 'realtime':
|
||||||
|
tiers_for_frequency.append(tier)
|
||||||
|
elif frequency == 'hourly' and config['check_frequency'] in ['hourly', 'realtime']:
|
||||||
|
tiers_for_frequency.append(tier)
|
||||||
|
|
||||||
|
# Get domains from users with matching subscription tier
|
||||||
|
from sqlalchemy.orm import joinedload
|
||||||
|
result = await db.execute(
|
||||||
|
select(Domain)
|
||||||
|
.join(User, Domain.user_id == User.id)
|
||||||
|
.outerjoin(Subscription, Subscription.user_id == User.id)
|
||||||
|
.where(
|
||||||
|
(Subscription.tier.in_(tiers_for_frequency)) |
|
||||||
|
(Subscription.id.is_(None) & (frequency == 'daily')) # Scout users (no subscription) = daily
|
||||||
|
)
|
||||||
|
)
|
||||||
domains = result.scalars().all()
|
domains = result.scalars().all()
|
||||||
|
|
||||||
logger.info(f"Checking {len(domains)} domains...")
|
logger.info(f"Checking {len(domains)} domains...")
|
||||||
@ -112,14 +137,47 @@ async def check_all_domains():
|
|||||||
await send_domain_availability_alerts(db, newly_available)
|
await send_domain_availability_alerts(db, newly_available)
|
||||||
|
|
||||||
|
|
||||||
|
async def check_all_domains():
|
||||||
|
"""Legacy function - checks all domains (daily)."""
|
||||||
|
await check_domains_by_frequency('daily')
|
||||||
|
|
||||||
|
|
||||||
|
async def check_hourly_domains():
|
||||||
|
"""Check domains for Trader users (hourly)."""
|
||||||
|
await check_domains_by_frequency('hourly')
|
||||||
|
|
||||||
|
|
||||||
|
async def check_realtime_domains():
|
||||||
|
"""Check domains for Tycoon users (every 10 minutes)."""
|
||||||
|
await check_domains_by_frequency('realtime')
|
||||||
|
|
||||||
|
|
||||||
def setup_scheduler():
|
def setup_scheduler():
|
||||||
"""Configure and start the scheduler."""
|
"""Configure and start the scheduler."""
|
||||||
# Daily domain check at configured hour
|
# Daily domain check for Scout users at configured hour
|
||||||
scheduler.add_job(
|
scheduler.add_job(
|
||||||
check_all_domains,
|
check_all_domains,
|
||||||
CronTrigger(hour=settings.check_hour, minute=settings.check_minute),
|
CronTrigger(hour=settings.check_hour, minute=settings.check_minute),
|
||||||
id="daily_domain_check",
|
id="daily_domain_check",
|
||||||
name="Daily Domain Availability Check",
|
name="Daily Domain Check (Scout)",
|
||||||
|
replace_existing=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Hourly domain check for Trader users
|
||||||
|
scheduler.add_job(
|
||||||
|
check_hourly_domains,
|
||||||
|
CronTrigger(minute=0), # Every hour at :00
|
||||||
|
id="hourly_domain_check",
|
||||||
|
name="Hourly Domain Check (Trader)",
|
||||||
|
replace_existing=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 10-minute domain check for Tycoon users
|
||||||
|
scheduler.add_job(
|
||||||
|
check_realtime_domains,
|
||||||
|
CronTrigger(minute='*/10'), # Every 10 minutes
|
||||||
|
id="realtime_domain_check",
|
||||||
|
name="10-Minute Domain Check (Tycoon)",
|
||||||
replace_existing=True,
|
replace_existing=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -152,7 +210,9 @@ def setup_scheduler():
|
|||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Scheduler configured:"
|
f"Scheduler configured:"
|
||||||
f"\n - Domain check at {settings.check_hour:02d}:{settings.check_minute:02d}"
|
f"\n - Scout domain check at {settings.check_hour:02d}:{settings.check_minute:02d} (daily)"
|
||||||
|
f"\n - Trader domain check every hour at :00"
|
||||||
|
f"\n - Tycoon domain check every 10 minutes"
|
||||||
f"\n - TLD price scrape at 03:00 UTC"
|
f"\n - TLD price scrape at 03:00 UTC"
|
||||||
f"\n - Price change alerts at 04:00 UTC"
|
f"\n - Price change alerts at 04:00 UTC"
|
||||||
f"\n - Auction scrape every hour at :30"
|
f"\n - Auction scrape every hour at :30"
|
||||||
|
|||||||
@ -231,10 +231,7 @@ export default function PricingPage() {
|
|||||||
{tier.features.map((feature) => (
|
{tier.features.map((feature) => (
|
||||||
<li key={feature.text} className="flex items-start gap-3">
|
<li key={feature.text} className="flex items-start gap-3">
|
||||||
<Check className="w-4 h-4 mt-0.5 shrink-0 text-accent" strokeWidth={2.5} />
|
<Check className="w-4 h-4 mt-0.5 shrink-0 text-accent" strokeWidth={2.5} />
|
||||||
<span className={clsx(
|
<span className="text-body-sm text-foreground">
|
||||||
"text-body-sm",
|
|
||||||
feature.highlight ? "text-foreground" : "text-foreground-muted"
|
|
||||||
)}>
|
|
||||||
{feature.text}
|
{feature.text}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import { useStore } from '@/lib/store'
|
|||||||
import { api, PriceAlert } from '@/lib/api'
|
import { api, PriceAlert } from '@/lib/api'
|
||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
Mail,
|
|
||||||
Bell,
|
Bell,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Shield,
|
Shield,
|
||||||
@ -20,8 +19,8 @@ import {
|
|||||||
ExternalLink,
|
ExternalLink,
|
||||||
Crown,
|
Crown,
|
||||||
Zap,
|
Zap,
|
||||||
Settings,
|
|
||||||
Key,
|
Key,
|
||||||
|
TrendingUp,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
@ -466,17 +465,28 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
{/* Billing Tab */}
|
{/* Billing Tab */}
|
||||||
{activeTab === 'billing' && (
|
{activeTab === 'billing' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Current Plan */}
|
||||||
<div className="p-6 sm:p-8 bg-background-secondary/30 border border-border rounded-2xl">
|
<div className="p-6 sm:p-8 bg-background-secondary/30 border border-border rounded-2xl">
|
||||||
<h2 className="text-body-lg font-medium text-foreground mb-6">Subscription & Billing</h2>
|
<h2 className="text-body-lg font-medium text-foreground mb-6">Your Current Plan</h2>
|
||||||
|
|
||||||
<div className="p-5 bg-accent/5 border border-accent/20 rounded-xl mb-6">
|
<div className="p-5 bg-accent/5 border border-accent/20 rounded-xl mb-6">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{tierName === 'Tycoon' ? (
|
||||||
|
<Crown className="w-6 h-6 text-accent" />
|
||||||
|
) : tierName === 'Trader' ? (
|
||||||
|
<TrendingUp className="w-6 h-6 text-accent" />
|
||||||
|
) : (
|
||||||
|
<Zap className="w-6 h-6 text-accent" />
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<p className="text-body font-medium text-foreground">{tierName} Plan</p>
|
<p className="text-xl font-semibold text-foreground">{tierName}</p>
|
||||||
<p className="text-body-sm text-foreground-muted">
|
<p className="text-body-sm text-foreground-muted">
|
||||||
{subscription?.check_frequency || 'Daily'} checks · {subscription?.domain_limit || 5} domains
|
{tierName === 'Scout' ? 'Free forever' : tierName === 'Trader' ? '$19/month' : '$49/month'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<span className={clsx(
|
<span className={clsx(
|
||||||
"px-3 py-1.5 text-ui-xs font-medium rounded-full",
|
"px-3 py-1.5 text-ui-xs font-medium rounded-full",
|
||||||
isProOrHigher ? "bg-accent/10 text-accent" : "bg-foreground/5 text-foreground-muted"
|
isProOrHigher ? "bg-accent/10 text-accent" : "bg-foreground/5 text-foreground-muted"
|
||||||
@ -485,6 +495,27 @@ export default function SettingsPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Plan Stats */}
|
||||||
|
<div className="grid grid-cols-3 gap-4 p-4 bg-background/50 rounded-xl mb-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-2xl font-semibold text-foreground">{subscription?.domain_limit || 5}</p>
|
||||||
|
<p className="text-xs text-foreground-muted">Domains</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center border-x border-border/50">
|
||||||
|
<p className="text-2xl font-semibold text-foreground">
|
||||||
|
{subscription?.check_frequency === 'realtime' ? '10m' :
|
||||||
|
subscription?.check_frequency === 'hourly' ? '1h' : '24h'}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-foreground-muted">Check Interval</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-2xl font-semibold text-foreground">
|
||||||
|
{subscription?.portfolio_limit === -1 ? '∞' : subscription?.portfolio_limit || 0}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-foreground-muted">Portfolio</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{isProOrHigher ? (
|
{isProOrHigher ? (
|
||||||
<button
|
<button
|
||||||
onClick={handleOpenBillingPortal}
|
onClick={handleOpenBillingPortal}
|
||||||
@ -506,58 +537,142 @@ export default function SettingsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
{/* Plan Features */}
|
||||||
<h3 className="text-body-sm font-medium text-foreground">Plan Features</h3>
|
<h3 className="text-body-sm font-medium text-foreground mb-3">Your Plan Includes</h3>
|
||||||
<ul className="space-y-2">
|
<ul className="grid grid-cols-2 gap-2">
|
||||||
{subscription?.features && Object.entries(subscription.features)
|
<li className="flex items-center gap-2 text-body-sm">
|
||||||
.filter(([key]) => !['sms_alerts', 'api_access', 'webhooks', 'bulk_tools', 'seo_metrics'].includes(key))
|
|
||||||
.map(([key, value]) => {
|
|
||||||
const featureNames: Record<string, string> = {
|
|
||||||
email_alerts: 'Email Alerts',
|
|
||||||
priority_alerts: 'Priority Alerts',
|
|
||||||
full_whois: 'Full WHOIS Data',
|
|
||||||
expiration_tracking: 'Expiry Tracking',
|
|
||||||
domain_valuation: 'Domain Valuation',
|
|
||||||
market_insights: 'Market Insights',
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<li key={key} className="flex items-center gap-2 text-body-sm">
|
|
||||||
{value ? (
|
|
||||||
<Check className="w-4 h-4 text-accent" />
|
<Check className="w-4 h-4 text-accent" />
|
||||||
) : (
|
<span className="text-foreground">{subscription?.domain_limit || 5} Watchlist Domains</span>
|
||||||
<span className="w-4 h-4 text-foreground-subtle">—</span>
|
|
||||||
)}
|
|
||||||
<span className={value ? 'text-foreground' : 'text-foreground-muted'}>
|
|
||||||
{featureNames[key] || key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
|
||||||
</span>
|
|
||||||
</li>
|
</li>
|
||||||
)
|
|
||||||
})}
|
|
||||||
{/* Show additional plan info */}
|
|
||||||
<li className="flex items-center gap-2 text-body-sm">
|
<li className="flex items-center gap-2 text-body-sm">
|
||||||
<Check className="w-4 h-4 text-accent" />
|
<Check className="w-4 h-4 text-accent" />
|
||||||
<span className="text-foreground">
|
<span className="text-foreground">
|
||||||
{subscription?.domain_limit} Watchlist Domains
|
{subscription?.check_frequency === 'realtime' ? '10-minute' :
|
||||||
|
subscription?.check_frequency === 'hourly' ? 'Hourly' : 'Daily'} Scans
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
<li className="flex items-center gap-2 text-body-sm">
|
||||||
|
<Check className="w-4 h-4 text-accent" />
|
||||||
|
<span className="text-foreground">Email Alerts</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2 text-body-sm">
|
||||||
|
<Check className="w-4 h-4 text-accent" />
|
||||||
|
<span className="text-foreground">TLD Price Data</span>
|
||||||
|
</li>
|
||||||
|
{subscription?.features?.domain_valuation && (
|
||||||
|
<li className="flex items-center gap-2 text-body-sm">
|
||||||
|
<Check className="w-4 h-4 text-accent" />
|
||||||
|
<span className="text-foreground">Domain Valuation</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
{(subscription?.portfolio_limit ?? 0) !== 0 && (
|
{(subscription?.portfolio_limit ?? 0) !== 0 && (
|
||||||
<li className="flex items-center gap-2 text-body-sm">
|
<li className="flex items-center gap-2 text-body-sm">
|
||||||
<Check className="w-4 h-4 text-accent" />
|
<Check className="w-4 h-4 text-accent" />
|
||||||
<span className="text-foreground">
|
<span className="text-foreground">
|
||||||
{subscription?.portfolio_limit === -1 ? 'Unlimited' : subscription?.portfolio_limit} Portfolio Domains
|
{subscription?.portfolio_limit === -1 ? 'Unlimited' : subscription?.portfolio_limit} Portfolio
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
|
{subscription?.features?.expiration_tracking && (
|
||||||
|
<li className="flex items-center gap-2 text-body-sm">
|
||||||
|
<Check className="w-4 h-4 text-accent" />
|
||||||
|
<span className="text-foreground">Expiry Tracking</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
{(subscription?.history_days ?? 0) !== 0 && (
|
{(subscription?.history_days ?? 0) !== 0 && (
|
||||||
<li className="flex items-center gap-2 text-body-sm">
|
<li className="flex items-center gap-2 text-body-sm">
|
||||||
<Check className="w-4 h-4 text-accent" />
|
<Check className="w-4 h-4 text-accent" />
|
||||||
<span className="text-foreground">
|
<span className="text-foreground">
|
||||||
{subscription?.history_days === -1 ? 'Full' : `${subscription?.history_days}-day`} Price History
|
{subscription?.history_days === -1 ? 'Full' : `${subscription?.history_days}-day`} History
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Compare All Plans */}
|
||||||
|
<div className="p-6 sm:p-8 bg-background-secondary/30 border border-border rounded-2xl">
|
||||||
|
<h2 className="text-body-lg font-medium text-foreground mb-6">Compare All Plans</h2>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto -mx-2">
|
||||||
|
<table className="w-full min-w-[500px]">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-border">
|
||||||
|
<th className="text-left py-3 px-3 text-body-sm font-medium text-foreground-muted">Feature</th>
|
||||||
|
<th className={clsx(
|
||||||
|
"text-center py-3 px-3 text-body-sm font-medium",
|
||||||
|
tierName === 'Scout' ? "text-accent" : "text-foreground-muted"
|
||||||
|
)}>Scout</th>
|
||||||
|
<th className={clsx(
|
||||||
|
"text-center py-3 px-3 text-body-sm font-medium",
|
||||||
|
tierName === 'Trader' ? "text-accent" : "text-foreground-muted"
|
||||||
|
)}>Trader</th>
|
||||||
|
<th className={clsx(
|
||||||
|
"text-center py-3 px-3 text-body-sm font-medium",
|
||||||
|
tierName === 'Tycoon' ? "text-accent" : "text-foreground-muted"
|
||||||
|
)}>Tycoon</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr className="border-b border-border/50">
|
||||||
|
<td className="py-3 px-3 text-body-sm text-foreground">Price</td>
|
||||||
|
<td className="py-3 px-3 text-center text-body-sm text-foreground">Free</td>
|
||||||
|
<td className="py-3 px-3 text-center text-body-sm text-foreground">$19/mo</td>
|
||||||
|
<td className="py-3 px-3 text-center text-body-sm text-foreground">$49/mo</td>
|
||||||
|
</tr>
|
||||||
|
<tr className="border-b border-border/50">
|
||||||
|
<td className="py-3 px-3 text-body-sm text-foreground">Watchlist Domains</td>
|
||||||
|
<td className="py-3 px-3 text-center text-body-sm text-foreground">5</td>
|
||||||
|
<td className="py-3 px-3 text-center text-body-sm text-foreground">50</td>
|
||||||
|
<td className="py-3 px-3 text-center text-body-sm text-foreground">500</td>
|
||||||
|
</tr>
|
||||||
|
<tr className="border-b border-border/50">
|
||||||
|
<td className="py-3 px-3 text-body-sm text-foreground">Scan Frequency</td>
|
||||||
|
<td className="py-3 px-3 text-center text-body-sm text-foreground">Daily</td>
|
||||||
|
<td className="py-3 px-3 text-center text-body-sm text-foreground">Hourly</td>
|
||||||
|
<td className="py-3 px-3 text-center text-body-sm text-accent font-medium">10 min</td>
|
||||||
|
</tr>
|
||||||
|
<tr className="border-b border-border/50">
|
||||||
|
<td className="py-3 px-3 text-body-sm text-foreground">Portfolio</td>
|
||||||
|
<td className="py-3 px-3 text-center text-body-sm text-foreground-muted">—</td>
|
||||||
|
<td className="py-3 px-3 text-center text-body-sm text-foreground">25</td>
|
||||||
|
<td className="py-3 px-3 text-center text-body-sm text-foreground">Unlimited</td>
|
||||||
|
</tr>
|
||||||
|
<tr className="border-b border-border/50">
|
||||||
|
<td className="py-3 px-3 text-body-sm text-foreground">Domain Valuation</td>
|
||||||
|
<td className="py-3 px-3 text-center text-body-sm text-foreground-muted">—</td>
|
||||||
|
<td className="py-3 px-3 text-center"><Check className="w-5 h-5 text-accent mx-auto" /></td>
|
||||||
|
<td className="py-3 px-3 text-center"><Check className="w-5 h-5 text-accent mx-auto" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr className="border-b border-border/50">
|
||||||
|
<td className="py-3 px-3 text-body-sm text-foreground">Price History</td>
|
||||||
|
<td className="py-3 px-3 text-center text-body-sm text-foreground-muted">—</td>
|
||||||
|
<td className="py-3 px-3 text-center text-body-sm text-foreground">90 days</td>
|
||||||
|
<td className="py-3 px-3 text-center text-body-sm text-foreground">Unlimited</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="py-3 px-3 text-body-sm text-foreground">Expiry Tracking</td>
|
||||||
|
<td className="py-3 px-3 text-center text-body-sm text-foreground-muted">—</td>
|
||||||
|
<td className="py-3 px-3 text-center"><Check className="w-5 h-5 text-accent mx-auto" /></td>
|
||||||
|
<td className="py-3 px-3 text-center"><Check className="w-5 h-5 text-accent mx-auto" /></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isProOrHigher && (
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<Link
|
||||||
|
href="/pricing"
|
||||||
|
className="inline-flex items-center gap-2 px-6 py-3 bg-accent text-background text-ui font-medium rounded-xl
|
||||||
|
hover:bg-accent-hover transition-all shadow-lg shadow-accent/20"
|
||||||
|
>
|
||||||
|
<Zap className="w-4 h-4" />
|
||||||
|
Upgrade Now
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user