Add step-by-step domain wizard with CF account selection and NS setup

This commit is contained in:
oguz ozturk 2026-01-11 15:17:37 +03:00
parent 801db4616b
commit 045f46bce6
2 changed files with 554 additions and 13 deletions

View File

@ -0,0 +1,547 @@
/**
* Add Domain Wizard - Step-by-step domain addition process
*/
import { useState, useEffect } from 'react';
import {
XMarkIcon,
CheckCircleIcon,
ArrowRightIcon,
ArrowLeftIcon,
GlobeAltIcon,
CloudIcon,
DocumentTextIcon,
ServerIcon,
ShieldCheckIcon,
} from '@heroicons/react/24/outline';
import api from '../services/api';
import CFTokenGuide from './CFTokenGuide';
import NSInstructions from './NSInstructions';
const AddDomainWizard = ({ onClose, onSuccess, customer }) => {
const [currentStep, setCurrentStep] = useState(1);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// Form data
const [domainName, setDomainName] = useState('');
const [cfAccountType, setCfAccountType] = useState(''); // 'company' or 'own'
const [selectedCompanyAccount, setSelectedCompanyAccount] = useState(null);
const [ownCfToken, setOwnCfToken] = useState('');
const [ownCfEmail, setOwnCfEmail] = useState('');
// Company CF accounts
const [companyAccounts, setCompanyAccounts] = useState([]);
// Domain setup data
const [domainId, setDomainId] = useState(null);
const [dnsPreview, setDnsPreview] = useState(null);
const [nsInstructions, setNsInstructions] = useState(null);
const [nsStatus, setNsStatus] = useState(null);
// UI helpers
const [showTokenGuide, setShowTokenGuide] = useState(false);
const steps = [
{ number: 1, title: 'Domain Name', icon: GlobeAltIcon },
{ number: 2, title: 'Cloudflare Account', icon: CloudIcon },
{ number: 3, title: cfAccountType === 'own' ? 'API Token' : 'DNS Preview', icon: DocumentTextIcon },
{ number: 4, title: 'Nameserver Setup', icon: ServerIcon },
{ number: 5, title: 'Verification', icon: ShieldCheckIcon },
];
// Fetch company CF accounts
useEffect(() => {
if (currentStep === 2) {
fetchCompanyAccounts();
}
}, [currentStep]);
const fetchCompanyAccounts = async () => {
try {
const response = await api.get('/api/customer/cloudflare-accounts');
setCompanyAccounts(response.data.accounts || []);
} catch (err) {
console.error('Failed to fetch CF accounts:', err);
}
};
// Validate domain name
const validateDomain = (domain) => {
const domainRegex = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/i;
return domainRegex.test(domain);
};
// Step 1: Submit domain name
const handleStep1Next = async () => {
if (!domainName.trim()) {
setError('Please enter a domain name');
return;
}
if (!validateDomain(domainName)) {
setError('Please enter a valid domain name (e.g., example.com)');
return;
}
setError(null);
setCurrentStep(2);
};
// Step 2: Select CF account type
const handleStep2Next = () => {
if (!cfAccountType) {
setError('Please select a Cloudflare account option');
return;
}
if (cfAccountType === 'company' && !selectedCompanyAccount) {
setError('Please select a company Cloudflare account');
return;
}
setError(null);
setCurrentStep(3);
};
// Step 3: Handle based on account type
const handleStep3Next = async () => {
setLoading(true);
setError(null);
try {
if (cfAccountType === 'own') {
// Validate own CF token
if (!ownCfToken.trim() || !ownCfEmail.trim()) {
setError('Please enter both Cloudflare email and API token');
setLoading(false);
return;
}
// Create domain with own CF account
const response = await api.post('/api/customer/domains', {
domain_name: domainName,
cf_account_type: 'own',
cf_email: ownCfEmail,
cf_api_token: ownCfToken,
});
setDomainId(response.data.domain.id);
setNsInstructions(response.data.ns_instructions);
setCurrentStep(4);
} else {
// Create domain with company CF account
const response = await api.post('/api/customer/domains', {
domain_name: domainName,
cf_account_type: 'company',
cf_account_id: selectedCompanyAccount.id,
});
setDomainId(response.data.domain.id);
setDnsPreview(response.data.dns_preview);
setNsInstructions(response.data.ns_instructions);
setCurrentStep(4);
}
} catch (err) {
setError(err.response?.data?.error || 'Failed to create domain');
} finally {
setLoading(false);
}
};
// Step 4: Check nameserver status
const checkNsStatus = async () => {
setLoading(true);
try {
const response = await api.get(`/api/customer/domains/${domainId}/ns-status`);
setNsStatus(response.data);
if (response.data.is_cloudflare) {
setCurrentStep(5);
}
} catch (err) {
console.error('Failed to check NS status:', err);
} finally {
setLoading(false);
}
};
return (
<>
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="sticky top-0 bg-white border-b px-6 py-4 flex items-center justify-between">
<h2 className="text-2xl font-bold text-gray-900">Add New Domain</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 transition-colors"
>
<XMarkIcon className="w-6 h-6" />
</button>
</div>
{/* Progress Steps */}
<div className="px-6 py-4 bg-gray-50">
<div className="flex items-center justify-between">
{steps.map((step, index) => {
const Icon = step.icon;
const isActive = currentStep === step.number;
const isCompleted = currentStep > step.number;
return (
<div key={step.number} className="flex items-center flex-1">
<div className="flex flex-col items-center flex-1">
<div
className={`w-10 h-10 rounded-full flex items-center justify-center transition-all ${
isCompleted
? 'bg-green-500 text-white'
: isActive
? 'bg-primary-500 text-white'
: 'bg-gray-200 text-gray-500'
}`}
>
{isCompleted ? (
<CheckCircleIcon className="w-6 h-6" />
) : (
<Icon className="w-5 h-5" />
)}
</div>
<span
className={`text-xs mt-2 font-medium ${
isActive ? 'text-primary-600' : 'text-gray-500'
}`}
>
{step.title}
</span>
</div>
{index < steps.length - 1 && (
<div
className={`h-0.5 flex-1 mx-2 ${
isCompleted ? 'bg-green-500' : 'bg-gray-200'
}`}
/>
)}
</div>
);
})}
</div>
</div>
{/* Error Message */}
{error && (
<div className="mx-6 mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-800 text-sm">{error}</p>
</div>
)}
{/* Step Content */}
<div className="p-6">
{/* Step 1: Domain Name */}
{currentStep === 1 && (
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold mb-2">Enter Your Domain Name</h3>
<p className="text-gray-600 text-sm mb-4">
Enter the domain name you want to add to your hosting platform.
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Domain Name *
</label>
<input
type="text"
value={domainName}
onChange={(e) => setDomainName(e.target.value.toLowerCase().trim())}
placeholder="example.com"
className="input-field w-full"
autoFocus
/>
<p className="text-xs text-gray-500 mt-1">
Enter without http:// or https://
</p>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="font-medium text-blue-900 mb-2">📋 Requirements:</h4>
<ul className="text-sm text-blue-800 space-y-1">
<li> You must own this domain</li>
<li> You must have access to domain registrar settings</li>
<li> Domain limit: {customer?.max_domains} domains</li>
</ul>
</div>
</div>
)}
{/* Step 2: Cloudflare Account Selection */}
{currentStep === 2 && (
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold mb-2">Select Cloudflare Account</h3>
<p className="text-gray-600 text-sm mb-4">
Choose how you want to manage your domain's DNS.
</p>
</div>
<div className="space-y-3">
{/* Company Account Option */}
<div
onClick={() => setCfAccountType('company')}
className={`border-2 rounded-lg p-4 cursor-pointer transition-all ${
cfAccountType === 'company'
? 'border-primary-500 bg-primary-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<div className="flex items-start">
<input
type="radio"
checked={cfAccountType === 'company'}
onChange={() => setCfAccountType('company')}
className="mt-1 mr-3"
/>
<div className="flex-1">
<h4 className="font-semibold text-gray-900">Use Company Cloudflare Account</h4>
<p className="text-sm text-gray-600 mt-1">
We'll manage your DNS using our Cloudflare account. Easier setup, no API token needed.
</p>
{cfAccountType === 'company' && companyAccounts.length > 0 && (
<div className="mt-3">
<label className="block text-sm font-medium text-gray-700 mb-2">
Select Account:
</label>
<select
value={selectedCompanyAccount?.id || ''}
onChange={(e) => {
const account = companyAccounts.find(a => a.id === parseInt(e.target.value));
setSelectedCompanyAccount(account);
}}
className="input-field w-full"
>
<option value="">Choose an account...</option>
{companyAccounts.map((account) => (
<option key={account.id} value={account.id}>
{account.name} ({account.email})
</option>
))}
</select>
</div>
)}
</div>
</div>
</div>
{/* Own Account Option */}
<div
onClick={() => setCfAccountType('own')}
className={`border-2 rounded-lg p-4 cursor-pointer transition-all ${
cfAccountType === 'own'
? 'border-primary-500 bg-primary-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<div className="flex items-start">
<input
type="radio"
checked={cfAccountType === 'own'}
onChange={() => setCfAccountType('own')}
className="mt-1 mr-3"
/>
<div className="flex-1">
<h4 className="font-semibold text-gray-900">Use My Own Cloudflare Account</h4>
<p className="text-sm text-gray-600 mt-1">
Use your own Cloudflare account. You'll need to provide an API token.
</p>
</div>
</div>
</div>
</div>
</div>
)}
{/* Step 3: Own CF Token OR DNS Preview */}
{currentStep === 3 && cfAccountType === 'own' && (
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold mb-2">Enter Cloudflare API Token</h3>
<p className="text-gray-600 text-sm mb-4">
Provide your Cloudflare API token to manage DNS records.
</p>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<p className="text-sm text-blue-800 mb-2">
<strong>Don't have an API token?</strong>
</p>
<button
onClick={() => setShowTokenGuide(true)}
className="text-blue-600 hover:text-blue-800 font-medium text-sm underline"
>
📖 View API Token Creation Guide
</button>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Cloudflare Email *
</label>
<input
type="email"
value={ownCfEmail}
onChange={(e) => setOwnCfEmail(e.target.value)}
placeholder="your-email@example.com"
className="input-field w-full"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
API Token *
</label>
<input
type="password"
value={ownCfToken}
onChange={(e) => setOwnCfToken(e.target.value)}
placeholder="Your Cloudflare API Token"
className="input-field w-full font-mono text-sm"
/>
<p className="text-xs text-gray-500 mt-1">
Your token will be encrypted and stored securely
</p>
</div>
</div>
)}
{currentStep === 3 && cfAccountType === 'company' && (
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold mb-2">DNS Records Preview</h3>
<p className="text-gray-600 text-sm mb-4">
These DNS records will be created for your domain.
</p>
</div>
{dnsPreview && (
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-300">
<th className="text-left py-2 px-2">Type</th>
<th className="text-left py-2 px-2">Name</th>
<th className="text-left py-2 px-2">Value</th>
</tr>
</thead>
<tbody>
{dnsPreview.records?.map((record, idx) => (
<tr key={idx} className="border-b border-gray-200">
<td className="py-2 px-2 font-mono">{record.type}</td>
<td className="py-2 px-2 font-mono">{record.name}</td>
<td className="py-2 px-2 font-mono text-xs">{record.value}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<p className="text-sm text-green-800">
DNS records will be automatically configured when you complete the setup.
</p>
</div>
</div>
)}
{/* Step 4: Nameserver Setup */}
{currentStep === 4 && (
<div className="space-y-4">
<NSInstructions
domain={domainName}
nsInstructions={nsInstructions}
nsStatus={nsStatus}
onCheck={checkNsStatus}
onContinue={() => setCurrentStep(5)}
loading={loading}
/>
</div>
)}
{/* Step 5: Verification & Completion */}
{currentStep === 5 && (
<div className="space-y-4 text-center py-8">
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto">
<CheckCircleIcon className="w-12 h-12 text-green-600" />
</div>
<div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">
Domain Successfully Added!
</h3>
<p className="text-gray-600">
Your domain <strong>{domainName}</strong> has been configured and is ready to use.
</p>
</div>
<div className="bg-green-50 border border-green-200 rounded-lg p-4 text-left max-w-md mx-auto">
<h4 className="font-semibold text-green-900 mb-2"> What's Next?</h4>
<ul className="text-sm text-green-800 space-y-1">
<li> DNS records have been configured</li>
<li> SSL certificate will be issued automatically</li>
<li> Your domain will be active within a few minutes</li>
</ul>
</div>
<button
onClick={() => {
onSuccess();
onClose();
}}
className="btn-primary mx-auto"
>
Go to Domains
</button>
</div>
)}
</div>
{/* Footer Actions */}
<div className="sticky bottom-0 bg-gray-50 border-t px-6 py-4 flex items-center justify-between">
<button
onClick={() => {
if (currentStep > 1) setCurrentStep(currentStep - 1);
else onClose();
}}
className="btn-secondary inline-flex items-center"
>
<ArrowLeftIcon className="w-4 h-4 mr-2" />
{currentStep === 1 ? 'Cancel' : 'Back'}
</button>
{currentStep < 5 && (
<button
onClick={() => {
if (currentStep === 1) handleStep1Next();
else if (currentStep === 2) handleStep2Next();
else if (currentStep === 3) handleStep3Next();
}}
disabled={loading}
className="btn-primary inline-flex items-center disabled:opacity-50"
>
{loading ? 'Processing...' : 'Next'}
<ArrowRightIcon className="w-4 h-4 ml-2" />
</button>
)}
</div>
</div>
</div>
{/* Token Guide Modal */}
{showTokenGuide && <CFTokenGuide onClose={() => setShowTokenGuide(false)} />}
</>
);
};
export default AddDomainWizard;

View File

@ -18,6 +18,7 @@ import {
ClockIcon,
} from '@heroicons/react/24/outline';
import api from '../services/api';
import AddDomainWizard from '../components/AddDomainWizard';
// Domains Content Component
const DomainsContent = ({ customer }) => {
@ -131,20 +132,13 @@ const DomainsContent = ({ customer }) => {
</div>
)}
{/* Add Domain Modal - Placeholder */}
{/* Add Domain Wizard */}
{showAddModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-2xl w-full mx-4">
<h3 className="text-xl font-bold mb-4">Add New Domain</h3>
<p className="text-gray-600 mb-4">Domain wizard coming soon...</p>
<button
onClick={() => setShowAddModal(false)}
className="btn-secondary"
>
Close
</button>
</div>
</div>
<AddDomainWizard
onClose={() => setShowAddModal(false)}
onSuccess={fetchDomains}
customer={customer}
/>
)}
</div>
);