feat: Redesign Yield activation wizard with clear 3-step flow
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
- Step 1: Select domain from verified portfolio - Step 2: Simple DNS instructions with A-record to 46.235.147.194 - Step 3: Success confirmation Much cleaner UI with: - Visual step indicators - Copy IP button - Clear explanations - Preview for non-Tycoon users
This commit is contained in:
@ -253,3 +253,4 @@ def check_coredns_status() -> dict:
|
|||||||
status["domain_count"] = len(_parse_domains_from_zone(content))
|
status["domain_count"] = len(_parse_domains_from_zone(content))
|
||||||
|
|
||||||
return status
|
return status
|
||||||
|
|
||||||
|
|||||||
@ -178,3 +178,4 @@ echo "Then reload: systemctl reload coredns"
|
|||||||
echo ""
|
echo ""
|
||||||
echo "Test with: dig @localhost akaya.ch"
|
echo "Test with: dig @localhost akaya.ch"
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
|
|
||||||
|
|||||||
@ -36,9 +36,11 @@ function StatusBadge({ status }: { status: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// ACTIVATE MODAL - Only verified portfolio domains
|
// ACTIVATE MODAL - Simple 3-step wizard
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
const YIELD_SERVER_IP = '46.235.147.194'
|
||||||
|
|
||||||
function ActivateModal({
|
function ActivateModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
@ -60,58 +62,29 @@ function ActivateModal({
|
|||||||
const [loadingDomains, setLoadingDomains] = useState(true)
|
const [loadingDomains, setLoadingDomains] = useState(true)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [step, setStep] = useState<1 | 2>(1)
|
const [step, setStep] = useState<1 | 2 | 3>(1)
|
||||||
const [activation, setActivation] = useState<null | {
|
const [activatedDomainId, setActivatedDomainId] = useState<number | null>(null)
|
||||||
domain_id: number
|
const [landing, setLanding] = useState<null | {
|
||||||
domain: string
|
headline: string
|
||||||
status: string
|
seo_intro: string
|
||||||
dns_instructions: {
|
cta_label: string
|
||||||
domain: string
|
|
||||||
nameservers: string[]
|
|
||||||
cname_host: string
|
|
||||||
cname_target: string
|
|
||||||
verification_url: string
|
|
||||||
}
|
|
||||||
landing?: {
|
|
||||||
template: string
|
|
||||||
headline: string
|
|
||||||
seo_intro: string
|
|
||||||
cta_label: string
|
|
||||||
model?: string | null
|
|
||||||
generated_at?: string | null
|
|
||||||
} | null
|
|
||||||
}>(null)
|
}>(null)
|
||||||
const [dnsChecking, setDnsChecking] = useState(false)
|
const [dnsChecking, setDnsChecking] = useState(false)
|
||||||
const [dnsResult, setDnsResult] = useState<null | {
|
const [dnsVerified, setDnsVerified] = useState(false)
|
||||||
verified: boolean
|
const [copied, setCopied] = useState(false)
|
||||||
expected_ns: string[]
|
|
||||||
actual_ns: string[]
|
|
||||||
cname_ok: boolean
|
|
||||||
error: string | null
|
|
||||||
}>(null)
|
|
||||||
const [copied, setCopied] = useState<string | null>(null)
|
|
||||||
|
|
||||||
const [previewLoading, setPreviewLoading] = useState(false)
|
const [previewLoading, setPreviewLoading] = useState(false)
|
||||||
const [previewError, setPreviewError] = useState<string | null>(null)
|
const [previewError, setPreviewError] = useState<string | null>(null)
|
||||||
const [preview, setPreview] = useState<null | {
|
const [preview, setPreview] = useState<null | {
|
||||||
template: string
|
|
||||||
headline: string
|
headline: string
|
||||||
seo_intro: string
|
seo_intro: string
|
||||||
cta_label: string
|
cta_label: string
|
||||||
niche: string
|
|
||||||
color_scheme: string
|
|
||||||
model: string
|
|
||||||
generated_at: string
|
|
||||||
cached: boolean
|
|
||||||
}>(null)
|
}>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) return
|
if (!isOpen) return
|
||||||
if (prefillDomain) {
|
if (prefillDomain) {
|
||||||
setSelectedDomain(prefillDomain)
|
setSelectedDomain(prefillDomain)
|
||||||
setStep(1)
|
|
||||||
setActivation(null)
|
|
||||||
setDnsResult(null)
|
|
||||||
}
|
}
|
||||||
const fetchVerifiedDomains = async () => {
|
const fetchVerifiedDomains = async () => {
|
||||||
setLoadingDomains(true)
|
setLoadingDomains(true)
|
||||||
@ -125,26 +98,28 @@ function ActivateModal({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fetchVerifiedDomains()
|
fetchVerifiedDomains()
|
||||||
}, [isOpen])
|
}, [isOpen, prefillDomain])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) return
|
if (!isOpen) return
|
||||||
setStep(1)
|
setStep(1)
|
||||||
setActivation(null)
|
setActivatedDomainId(null)
|
||||||
setDnsResult(null)
|
setLanding(null)
|
||||||
setDnsChecking(false)
|
setDnsChecking(false)
|
||||||
|
setDnsVerified(false)
|
||||||
setError(null)
|
setError(null)
|
||||||
setSelectedDomain('')
|
setSelectedDomain(prefillDomain || '')
|
||||||
setPreview(null)
|
setPreview(null)
|
||||||
setPreviewError(null)
|
setPreviewError(null)
|
||||||
setPreviewLoading(false)
|
setPreviewLoading(false)
|
||||||
}, [isOpen])
|
setCopied(false)
|
||||||
|
}, [isOpen, prefillDomain])
|
||||||
|
|
||||||
const copyToClipboard = async (value: string, key: string) => {
|
const copyIP = async () => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(value)
|
await navigator.clipboard.writeText(YIELD_SERVER_IP)
|
||||||
setCopied(key)
|
setCopied(true)
|
||||||
setTimeout(() => setCopied(null), 1200)
|
setTimeout(() => setCopied(false), 2000)
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
@ -156,39 +131,33 @@ function ActivateModal({
|
|||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
const res = await api.activateYieldDomain(selectedDomain, true)
|
const res = await api.activateYieldDomain(selectedDomain, true)
|
||||||
setActivation({
|
setActivatedDomainId(res.domain_id)
|
||||||
domain_id: res.domain_id,
|
if (res.landing) {
|
||||||
domain: res.domain,
|
setLanding({
|
||||||
status: res.status,
|
headline: res.landing.headline,
|
||||||
dns_instructions: res.dns_instructions,
|
seo_intro: res.landing.seo_intro,
|
||||||
landing: res.landing || null,
|
cta_label: res.landing.cta_label,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
setStep(2)
|
setStep(2)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message || 'Failed')
|
setError(err.message || 'Failed to activate')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePreview = async (refresh: boolean = false) => {
|
const handlePreview = async () => {
|
||||||
if (!selectedDomain) return
|
if (!selectedDomain || !canPreview) return
|
||||||
if (!canPreview) return
|
|
||||||
setPreviewLoading(true)
|
setPreviewLoading(true)
|
||||||
setPreviewError(null)
|
setPreviewError(null)
|
||||||
setPreview(null)
|
setPreview(null)
|
||||||
try {
|
try {
|
||||||
const res = await api.getYieldLandingPreview(selectedDomain, refresh)
|
const res = await api.getYieldLandingPreview(selectedDomain, false)
|
||||||
setPreview({
|
setPreview({
|
||||||
template: res.result.template,
|
|
||||||
headline: res.result.headline,
|
headline: res.result.headline,
|
||||||
seo_intro: res.result.seo_intro,
|
seo_intro: res.result.seo_intro,
|
||||||
cta_label: res.result.cta_label,
|
cta_label: res.result.cta_label,
|
||||||
niche: res.result.niche,
|
|
||||||
color_scheme: res.result.color_scheme,
|
|
||||||
model: res.model,
|
|
||||||
generated_at: res.generated_at,
|
|
||||||
cached: res.cached,
|
|
||||||
})
|
})
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setPreviewError(err.message || 'Preview failed')
|
setPreviewError(err.message || 'Preview failed')
|
||||||
@ -197,242 +166,322 @@ function ActivateModal({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkDNS = useCallback(async (domainId: number) => {
|
const checkDNS = async () => {
|
||||||
|
if (!activatedDomainId) return
|
||||||
setDnsChecking(true)
|
setDnsChecking(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
const res = await api.verifyYieldDomainDNS(domainId)
|
const res = await api.verifyYieldDomainDNS(activatedDomainId)
|
||||||
setDnsResult({
|
|
||||||
verified: res.verified,
|
|
||||||
expected_ns: res.expected_ns,
|
|
||||||
actual_ns: res.actual_ns,
|
|
||||||
cname_ok: res.cname_ok,
|
|
||||||
error: res.error,
|
|
||||||
})
|
|
||||||
if (res.verified) {
|
if (res.verified) {
|
||||||
|
setDnsVerified(true)
|
||||||
|
setStep(3)
|
||||||
onSuccess()
|
onSuccess()
|
||||||
|
} else {
|
||||||
|
setError('DNS not yet propagated. This can take up to 15 minutes. Try again shortly.')
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message || 'DNS check failed')
|
setError(err.message || 'DNS check failed')
|
||||||
} finally {
|
} finally {
|
||||||
setDnsChecking(false)
|
setDnsChecking(false)
|
||||||
}
|
}
|
||||||
}, [onSuccess])
|
}
|
||||||
|
|
||||||
if (!isOpen) return null
|
if (!isOpen) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-[110] bg-black/80 flex items-center justify-center p-4" onClick={onClose}>
|
<div className="fixed inset-0 z-[110] bg-black/80 flex items-center justify-center p-4" onClick={onClose}>
|
||||||
<div className="w-full max-w-md bg-[#0A0A0A] border border-white/[0.08]" onClick={(e) => e.stopPropagation()}>
|
<div className="w-full max-w-lg bg-[#0A0A0A] border border-white/[0.08]" onClick={(e) => e.stopPropagation()}>
|
||||||
<div className="flex items-center justify-between p-4 border-b border-white/[0.08]">
|
{/* Header */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center justify-between p-5 border-b border-white/[0.08]">
|
||||||
<Sparkles className="w-4 h-4 text-accent" />
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-xs font-mono text-accent uppercase tracking-wider">
|
<div className="w-10 h-10 bg-accent/10 border border-accent/20 flex items-center justify-center">
|
||||||
{isTycoon ? 'Activate Yield' : 'Yield Preview'}
|
<Sparkles className="w-5 h-5 text-accent" />
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<button onClick={onClose} className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/40"><X className="w-4 h-4" /></button>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 space-y-4">
|
|
||||||
{step === 1 && loadingDomains ? (
|
|
||||||
<div className="flex items-center justify-center py-6">
|
|
||||||
<Loader2 className="w-5 h-5 text-accent animate-spin" />
|
|
||||||
</div>
|
</div>
|
||||||
) : step === 1 && verifiedDomains.length === 0 ? (
|
<div>
|
||||||
<div className="text-center py-6">
|
<h2 className="text-base font-bold text-white">
|
||||||
<AlertCircle className="w-8 h-8 text-amber-400 mx-auto mb-3" />
|
{isTycoon ? 'Activate Yield' : 'Preview Yield Landing'}
|
||||||
<h3 className="text-sm font-bold text-white mb-2">No Verified Domains</h3>
|
</h2>
|
||||||
<p className="text-xs text-white/50 mb-4">
|
<p className="text-[10px] font-mono text-white/40 uppercase tracking-wider">
|
||||||
You need to add domains to your portfolio and verify DNS ownership before activating Yield.
|
Step {step} of {isTycoon ? 3 : 1}
|
||||||
</p>
|
</p>
|
||||||
<a href="/terminal/portfolio" className="inline-flex items-center gap-2 px-4 py-2 bg-accent text-black text-xs font-bold uppercase">
|
|
||||||
Go to Portfolio
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
) : step === 1 ? (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<label className="block text-[9px] font-mono text-white/40 uppercase mb-1.5">Select Domain (DNS Verified)</label>
|
|
||||||
<select
|
|
||||||
value={selectedDomain}
|
|
||||||
onChange={(e) => setSelectedDomain(e.target.value)}
|
|
||||||
className="w-full px-3 py-2.5 bg-white/5 border border-white/10 text-white text-sm font-mono outline-none focus:border-accent/50"
|
|
||||||
>
|
|
||||||
<option value="">— Select a domain —</option>
|
|
||||||
{verifiedDomains.map(d => (
|
|
||||||
<option key={d.id} value={d.domain}>{d.domain}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className={clsx(
|
|
||||||
"p-3 border text-xs font-mono",
|
|
||||||
isTycoon ? "bg-accent/5 border-accent/20 text-accent/80" : "bg-white/[0.02] border-white/[0.08] text-white/50"
|
|
||||||
)}>
|
|
||||||
{isTycoon ? (
|
|
||||||
<p>Only DNS-verified domains from your portfolio can be activated for Yield.</p>
|
|
||||||
) : (
|
|
||||||
<p>
|
|
||||||
Yield is <span className="text-white/80 font-bold">Tycoon-only</span>. On Trader you can preview the landing page that will be generated.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{error && <div className="p-2 bg-rose-500/10 border border-rose-500/20 text-rose-400 text-xs">{error}</div>}
|
<button onClick={onClose} className="w-9 h-9 flex items-center justify-center border border-white/10 text-white/40 hover:text-white hover:border-white/20 transition-colors">
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{!isTycoon && (
|
{/* Content */}
|
||||||
|
<div className="p-5">
|
||||||
|
{/* STEP 1: Select Domain */}
|
||||||
|
{step === 1 && (
|
||||||
|
<div className="space-y-5">
|
||||||
|
{loadingDomains ? (
|
||||||
|
<div className="flex items-center justify-center py-10">
|
||||||
|
<Loader2 className="w-6 h-6 text-accent animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : verifiedDomains.length === 0 ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<AlertCircle className="w-12 h-12 text-amber-400/50 mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-bold text-white mb-2">No Verified Domains</h3>
|
||||||
|
<p className="text-sm text-white/50 mb-6 max-w-xs mx-auto">
|
||||||
|
First, add domains to your portfolio and verify DNS ownership.
|
||||||
|
</p>
|
||||||
|
<a href="/terminal/portfolio" className="inline-flex items-center gap-2 px-5 py-3 bg-accent text-black text-xs font-bold uppercase tracking-wider">
|
||||||
|
Go to Portfolio
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="flex gap-2">
|
<div>
|
||||||
<button
|
<label className="block text-xs font-medium text-white mb-2">
|
||||||
onClick={() => handlePreview(false)}
|
Which domain do you want to monetize?
|
||||||
disabled={previewLoading || !selectedDomain}
|
</label>
|
||||||
className="flex-1 py-2.5 bg-white/10 text-white text-xs font-bold uppercase flex items-center justify-center gap-2 disabled:opacity-50"
|
<select
|
||||||
|
value={selectedDomain}
|
||||||
|
onChange={(e) => { setSelectedDomain(e.target.value); setPreview(null); setPreviewError(null) }}
|
||||||
|
className="w-full px-4 py-3 bg-white/5 border border-white/10 text-white text-sm font-mono outline-none focus:border-accent/50 transition-colors"
|
||||||
>
|
>
|
||||||
{previewLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Sparkles className="w-4 h-4" />}
|
<option value="">Select a domain...</option>
|
||||||
Generate Preview
|
{verifiedDomains.map(d => (
|
||||||
</button>
|
<option key={d.id} value={d.domain}>{d.domain}</option>
|
||||||
<Link
|
))}
|
||||||
href="/pricing"
|
</select>
|
||||||
className="px-3 py-2.5 bg-accent text-black text-xs font-bold uppercase flex items-center justify-center"
|
<p className="text-[10px] text-white/30 mt-2">
|
||||||
>
|
Only DNS-verified domains from your portfolio are shown.
|
||||||
Upgrade
|
</p>
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{previewError && (
|
{/* Info Box */}
|
||||||
<div className="p-3 bg-rose-500/10 border border-rose-500/20 text-rose-300 text-xs">
|
<div className={clsx(
|
||||||
{previewError}
|
"p-4 border",
|
||||||
|
isTycoon ? "bg-accent/5 border-accent/20" : "bg-white/[0.02] border-white/[0.08]"
|
||||||
|
)}>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<div className="shrink-0 w-8 h-8 bg-white/5 flex items-center justify-center">
|
||||||
|
<Zap className={clsx("w-4 h-4", isTycoon ? "text-accent" : "text-white/40")} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-bold text-white mb-1">
|
||||||
|
{isTycoon ? 'How Yield Works' : 'Yield is Tycoon-Only'}
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-white/50 leading-relaxed">
|
||||||
|
{isTycoon
|
||||||
|
? 'We generate an AI-powered landing page for your domain. When visitors click, you earn revenue through our affiliate network.'
|
||||||
|
: 'You can preview the AI-generated landing page, but activation requires a Tycoon subscription.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="p-3 bg-rose-500/10 border border-rose-500/20 text-rose-400 text-xs font-mono">
|
||||||
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{preview && (
|
{/* Preview for non-Tycoon */}
|
||||||
<div className="p-3 bg-[#050505] border border-white/[0.08] space-y-2">
|
{!isTycoon && (
|
||||||
<div className="flex items-center justify-between">
|
<>
|
||||||
<div className="text-[9px] font-mono text-white/40 uppercase tracking-wider">Landing Page Preview</div>
|
<button
|
||||||
<button
|
onClick={handlePreview}
|
||||||
onClick={() => handlePreview(true)}
|
disabled={previewLoading || !selectedDomain}
|
||||||
className="text-[10px] font-mono text-white/40 hover:text-white"
|
className="w-full py-3 bg-white/10 text-white text-sm font-bold uppercase flex items-center justify-center gap-2 disabled:opacity-50 hover:bg-white/15 transition-colors"
|
||||||
disabled={previewLoading}
|
>
|
||||||
>
|
{previewLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Eye className="w-4 h-4" />}
|
||||||
Refresh
|
Preview Landing Page
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
<div className="text-sm font-bold text-white">{preview.headline}</div>
|
{previewError && (
|
||||||
<div className="text-xs text-white/50">{preview.seo_intro}</div>
|
<div className="p-3 bg-rose-500/10 border border-rose-500/20 text-rose-300 text-xs">
|
||||||
<div className="flex items-center justify-between text-[10px] font-mono text-white/40 pt-2 border-t border-white/10">
|
{previewError}
|
||||||
<span>CTA: <span className="text-white/70">{preview.cta_label}</span></span>
|
</div>
|
||||||
<span>{preview.cached ? 'Cached' : 'Fresh'} • {preview.model}</span>
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
{preview && (
|
||||||
|
<div className="p-4 bg-[#050505] border border-white/[0.08] space-y-3">
|
||||||
|
<div className="text-[9px] font-mono text-accent uppercase tracking-wider">Generated Landing Page</div>
|
||||||
|
<h3 className="text-base font-bold text-white">{preview.headline}</h3>
|
||||||
|
<p className="text-sm text-white/50 leading-relaxed">{preview.seo_intro}</p>
|
||||||
|
<div className="pt-3 border-t border-white/10">
|
||||||
|
<span className="inline-flex items-center gap-2 px-4 py-2 bg-accent/20 text-accent text-xs font-bold">
|
||||||
|
{preview.cta_label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/pricing"
|
||||||
|
className="w-full py-3 bg-accent text-black text-sm font-bold uppercase flex items-center justify-center gap-2 hover:bg-white transition-colors"
|
||||||
|
>
|
||||||
|
<Crown className="w-4 h-4" />
|
||||||
|
Upgrade to Tycoon to Activate
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Activate for Tycoon */}
|
||||||
|
{isTycoon && (
|
||||||
|
<button
|
||||||
|
onClick={handleActivate}
|
||||||
|
disabled={loading || !selectedDomain}
|
||||||
|
className="w-full py-3.5 bg-accent text-black text-sm font-bold uppercase flex items-center justify-center gap-2 disabled:opacity-50 hover:bg-white transition-colors"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Sparkles className="w-4 h-4" />}
|
||||||
|
Generate Landing & Continue
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{isTycoon && (
|
{/* STEP 2: DNS Setup Instructions */}
|
||||||
<button onClick={handleActivate} disabled={loading || !selectedDomain}
|
{step === 2 && (
|
||||||
className="w-full py-2.5 bg-accent text-black text-xs font-bold uppercase flex items-center justify-center gap-2 disabled:opacity-50">
|
<div className="space-y-5">
|
||||||
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Sparkles className="w-4 h-4" />}
|
{/* Success Message */}
|
||||||
Activate Yield
|
<div className="p-4 bg-accent/5 border border-accent/20">
|
||||||
</button>
|
<div className="flex gap-3">
|
||||||
)}
|
<CheckCircle2 className="w-5 h-5 text-accent shrink-0 mt-0.5" />
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="p-3 bg-white/[0.02] border border-white/[0.08]">
|
|
||||||
<div className="flex items-start justify-between gap-3">
|
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[9px] font-mono text-white/40 uppercase tracking-wider">Domain</div>
|
<h4 className="text-sm font-bold text-white mb-1">Landing Page Generated!</h4>
|
||||||
<div className="text-sm font-bold text-white font-mono">{activation?.domain}</div>
|
<p className="text-xs text-white/50">
|
||||||
|
Now point your domain to our server to start earning.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<StatusBadge status={activation?.status || 'pending'} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activation?.landing?.headline && (
|
{/* Landing Preview */}
|
||||||
<div className="p-3 bg-[#050505] border border-white/[0.08] space-y-2">
|
{landing && (
|
||||||
<div className="text-[9px] font-mono text-white/40 uppercase tracking-wider">Landing Page (Generated)</div>
|
<div className="p-4 bg-[#050505] border border-white/[0.08] space-y-2">
|
||||||
<div className="text-sm font-bold text-white">{activation.landing.headline}</div>
|
<div className="text-[9px] font-mono text-white/40 uppercase tracking-wider">Your Landing Page</div>
|
||||||
<div className="text-xs text-white/50">{activation.landing.seo_intro}</div>
|
<h3 className="text-sm font-bold text-white">{landing.headline}</h3>
|
||||||
<div className="text-[10px] font-mono text-white/40">
|
<p className="text-xs text-white/50">{landing.seo_intro}</p>
|
||||||
CTA: <span className="text-white/70">{activation.landing.cta_label}</span>
|
|
||||||
{activation.landing.model ? <span className="text-white/20"> • {activation.landing.model}</span> : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
{/* DNS Instructions */}
|
||||||
<div className="text-[10px] font-mono text-white/40 uppercase tracking-wider">Option A (Recommended): Nameservers</div>
|
<div className="space-y-3">
|
||||||
<div className="bg-[#020202] border border-white/[0.08]">
|
<h3 className="text-sm font-bold text-white flex items-center gap-2">
|
||||||
{(activation?.dns_instructions.nameservers || []).map((ns, idx) => (
|
<span className="w-6 h-6 bg-accent text-black text-xs font-bold flex items-center justify-center">1</span>
|
||||||
<div key={ns} className={clsx("flex items-center justify-between px-3 py-2", idx > 0 && "border-t border-white/[0.06]")}>
|
Set up DNS at your registrar
|
||||||
<span className="text-xs font-mono text-white/80">{ns}</span>
|
</h3>
|
||||||
<button onClick={() => copyToClipboard(ns, `ns-${idx}`)} className="p-1.5 border border-white/10 text-white/40 hover:text-white">
|
|
||||||
{copied === `ns-${idx}` ? <Check className="w-4 h-4 text-accent" /> : <Copy className="w-4 h-4" />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="p-4 bg-[#020202] border border-white/[0.08] space-y-4">
|
||||||
<div className="text-[10px] font-mono text-white/40 uppercase tracking-wider">Option B: CNAME / ALIAS</div>
|
<p className="text-sm text-white/70">
|
||||||
<div className="bg-[#020202] border border-white/[0.08] p-3 space-y-2">
|
Go to your domain registrar (where you bought <span className="text-white font-bold">{selectedDomain}</span>) and add this DNS record:
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="text-xs font-mono text-white/70">
|
|
||||||
<span className="text-white/40">Host:</span> {activation?.dns_instructions.cname_host} <span className="text-white/40">→ Target:</span> {activation?.dns_instructions.cname_target}
|
|
||||||
</div>
|
|
||||||
<button onClick={() => copyToClipboard(activation?.dns_instructions.cname_target || '', `cname-target`)} className="p-1.5 border border-white/10 text-white/40 hover:text-white">
|
|
||||||
{copied === `cname-target` ? <Check className="w-4 h-4 text-accent" /> : <Copy className="w-4 h-4" />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p className="text-[10px] font-mono text-white/35">
|
|
||||||
Some DNS providers use ALIAS/ANAME for apex. We accept both CNAME and ALIAS-style flattening.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{dnsResult && (
|
<div className="grid grid-cols-3 gap-2 text-center">
|
||||||
<div className={clsx("p-3 border text-xs font-mono", dnsResult.verified ? "bg-accent/5 border-accent/20 text-accent/80" : "bg-amber-400/5 border-amber-400/20 text-amber-400/80")}>
|
<div className="p-3 bg-white/5 border border-white/10">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="text-[9px] font-mono text-white/40 uppercase mb-1">Type</div>
|
||||||
<span>{dnsResult.verified ? 'Connected. Domain is active.' : 'Not connected yet. Waiting for DNS propagation.'}</span>
|
<div className="text-lg font-bold text-white font-mono">A</div>
|
||||||
{dnsResult.verified ? <CheckCircle2 className="w-4 h-4 text-accent" /> : <Clock className="w-4 h-4 text-amber-400" />}
|
|
||||||
</div>
|
|
||||||
{dnsResult.error && <div className="mt-2 text-rose-400/80">Error: {dnsResult.error}</div>}
|
|
||||||
{!dnsResult.verified && (
|
|
||||||
<div className="mt-2 text-white/40">
|
|
||||||
<div>Expected NS: {dnsResult.expected_ns?.join(', ') || '—'}</div>
|
|
||||||
<div>Actual NS: {dnsResult.actual_ns?.join(', ') || '—'}</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="p-3 bg-white/5 border border-white/10">
|
||||||
|
<div className="text-[9px] font-mono text-white/40 uppercase mb-1">Name</div>
|
||||||
|
<div className="text-lg font-bold text-white font-mono">@</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-accent/10 border border-accent/20">
|
||||||
|
<div className="text-[9px] font-mono text-accent/60 uppercase mb-1">Value</div>
|
||||||
|
<div className="text-sm font-bold text-accent font-mono">{YIELD_SERVER_IP}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={copyIP}
|
||||||
|
className="w-full py-2.5 border border-white/10 text-white/70 text-xs font-mono flex items-center justify-center gap-2 hover:bg-white/5 transition-colors"
|
||||||
|
>
|
||||||
|
{copied ? <Check className="w-4 h-4 text-accent" /> : <Copy className="w-4 h-4" />}
|
||||||
|
{copied ? 'Copied!' : `Copy IP: ${YIELD_SERVER_IP}`}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="text-[10px] text-white/30 leading-relaxed">
|
||||||
|
<strong className="text-white/50">Tip:</strong> The "@" symbol means the root domain. Some registrars use "empty" or the domain name itself instead.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{error && <div className="p-2 bg-rose-500/10 border border-rose-500/20 text-rose-400 text-xs">{error}</div>}
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => { setStep(1); setActivation(null); setDnsResult(null) }}
|
|
||||||
className="flex-1 py-2.5 border border-white/10 text-white/60 text-xs font-bold uppercase"
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => activation?.domain_id && checkDNS(activation.domain_id)}
|
|
||||||
disabled={dnsChecking || !activation?.domain_id}
|
|
||||||
className="flex-[1.4] py-2.5 bg-accent text-black text-xs font-bold uppercase flex items-center justify-center gap-2 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{dnsChecking ? <Loader2 className="w-4 h-4 animate-spin" /> : <RefreshCw className="w-4 h-4" />}
|
|
||||||
Verify DNS
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{dnsResult?.verified && (
|
{/* Verify Button */}
|
||||||
<button
|
<div className="space-y-3">
|
||||||
onClick={() => { onClose(); onSuccess() }}
|
<h3 className="text-sm font-bold text-white flex items-center gap-2">
|
||||||
className="w-full py-2.5 bg-white text-black text-xs font-bold uppercase flex items-center justify-center gap-2"
|
<span className="w-6 h-6 bg-white/10 text-white/60 text-xs font-bold flex items-center justify-center">2</span>
|
||||||
>
|
Verify connection
|
||||||
View Yield Dashboard <ChevronRight className="w-4 h-4" />
|
</h3>
|
||||||
</button>
|
|
||||||
)}
|
<p className="text-xs text-white/50">
|
||||||
</>
|
After saving your DNS settings, click verify. DNS changes can take 5-15 minutes to propagate.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="p-3 bg-amber-500/10 border border-amber-500/20 text-amber-400 text-xs flex items-start gap-2">
|
||||||
|
<Clock className="w-4 h-4 shrink-0 mt-0.5" />
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setStep(1)}
|
||||||
|
className="flex-1 py-3 border border-white/10 text-white/60 text-xs font-bold uppercase hover:bg-white/5 transition-colors"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={checkDNS}
|
||||||
|
disabled={dnsChecking}
|
||||||
|
className="flex-[2] py-3 bg-accent text-black text-sm font-bold uppercase flex items-center justify-center gap-2 disabled:opacity-50 hover:bg-white transition-colors"
|
||||||
|
>
|
||||||
|
{dnsChecking ? <Loader2 className="w-4 h-4 animate-spin" /> : <RefreshCw className="w-4 h-4" />}
|
||||||
|
Verify DNS
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* STEP 3: Success */}
|
||||||
|
{step === 3 && (
|
||||||
|
<div className="text-center py-6 space-y-5">
|
||||||
|
<div className="w-16 h-16 bg-accent/10 border border-accent/20 flex items-center justify-center mx-auto">
|
||||||
|
<CheckCircle2 className="w-8 h-8 text-accent" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-bold text-white mb-2">Yield Activated!</h3>
|
||||||
|
<p className="text-sm text-white/50">
|
||||||
|
<span className="text-white font-bold">{selectedDomain}</span> is now earning passive income.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-[#050505] border border-white/[0.08] text-left space-y-2">
|
||||||
|
<div className="text-[9px] font-mono text-white/40 uppercase tracking-wider">What happens now?</div>
|
||||||
|
<ul className="text-xs text-white/60 space-y-1.5">
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="text-accent">•</span>
|
||||||
|
Visitors to {selectedDomain} see your AI-generated landing page
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="text-accent">•</span>
|
||||||
|
When they click the CTA, you earn revenue
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="text-accent">•</span>
|
||||||
|
Track clicks and earnings in your Yield dashboard
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="w-full py-3.5 bg-accent text-black text-sm font-bold uppercase flex items-center justify-center gap-2 hover:bg-white transition-colors"
|
||||||
|
>
|
||||||
|
View Dashboard
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user