Add React frontend with Tailwind CSS
This commit is contained in:
parent
0593965305
commit
8d0b564738
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Hosting Platform - DNS & SSL Management</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"name": "hosting-platform-frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host 0.0.0.0 --port 3001",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"axios": "^1.6.2",
|
||||||
|
"react-router-dom": "^6.20.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.43",
|
||||||
|
"@types/react-dom": "^18.2.17",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
|
"eslint": "^8.55.0",
|
||||||
|
"eslint-plugin-react": "^7.33.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
|
"postcss": "^8.4.32",
|
||||||
|
"tailwindcss": "^3.3.6",
|
||||||
|
"vite": "^5.0.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
.app {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6b7280;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
border: 1px solid #6ee7b7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-error {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
border: 1px solid #fca5a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
border: 1px solid #93c5fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import DomainSetup from './pages/DomainSetup'
|
||||||
|
import DomainList from './pages/DomainList'
|
||||||
|
import './App.css'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [currentPage, setCurrentPage] = useState('setup')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="bg-white shadow-sm">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-lg flex items-center justify-center">
|
||||||
|
<span className="text-white text-xl font-bold">H</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">Hosting Platform</h1>
|
||||||
|
<p className="text-sm text-gray-500">DNS & SSL Management</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="flex space-x-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage('setup')}
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||||
|
currentPage === 'setup'
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: 'text-gray-600 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Add Domain
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage('list')}
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||||
|
currentPage === 'list'
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: 'text-gray-600 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
My Domains
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
{currentPage === 'setup' ? <DomainSetup /> : <DomainList />}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="bg-white border-t border-gray-200 mt-12">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<div className="text-center text-gray-500 text-sm">
|
||||||
|
<p>© 2024 Hosting Platform. Powered by Cloudflare.</p>
|
||||||
|
<p className="mt-1">Automated DNS & SSL Management</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App.jsx'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { dnsAPI } from '../services/api'
|
||||||
|
|
||||||
|
function DomainList() {
|
||||||
|
const [domains, setDomains] = useState([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDomains()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const fetchDomains = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await dnsAPI.getDomains()
|
||||||
|
setDomains(response.data)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.error || 'Failed to fetch domains')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusBadge = (status) => {
|
||||||
|
const colors = {
|
||||||
|
active: 'bg-green-100 text-green-800',
|
||||||
|
pending: 'bg-yellow-100 text-yellow-800',
|
||||||
|
error: 'bg-red-100 text-red-800',
|
||||||
|
}
|
||||||
|
return colors[status] || 'bg-gray-100 text-gray-800'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
|
||||||
|
<p className="mt-4 text-gray-600">Loading domains...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="alert alert-error">
|
||||||
|
<strong>Error:</strong> {error}
|
||||||
|
</div>
|
||||||
|
<button onClick={fetchDomains} className="btn btn-primary mt-4">
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domains.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="card text-center">
|
||||||
|
<div className="w-20 h-20 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<svg className="w-10 h-10 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-800 mb-2">No Domains Yet</h2>
|
||||||
|
<p className="text-gray-600 mb-6">
|
||||||
|
You haven't added any domains yet. Click "Add Domain" to get started.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-800">My Domains</h2>
|
||||||
|
<button onClick={fetchDomains} className="btn btn-secondary">
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{domains.map((domain) => (
|
||||||
|
<div key={domain.id} className="card">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center space-x-3 mb-2">
|
||||||
|
<h3 className="text-xl font-bold text-gray-800">
|
||||||
|
{domain.domain_name}
|
||||||
|
</h3>
|
||||||
|
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getStatusBadge(domain.status)}`}>
|
||||||
|
{domain.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-500">Load Balancer IP:</span>
|
||||||
|
<p className="font-medium text-gray-800">{domain.lb_ip || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-500">DNS Configured:</span>
|
||||||
|
<p className="font-medium text-gray-800">
|
||||||
|
{domain.dns_configured ? '✓ Yes' : '✗ No'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-500">SSL Configured:</span>
|
||||||
|
<p className="font-medium text-gray-800">
|
||||||
|
{domain.ssl_configured ? '✓ Yes' : '✗ No'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-500">Cloudflare Proxy:</span>
|
||||||
|
<p className="font-medium text-gray-800">
|
||||||
|
{domain.cf_proxy_enabled ? '✓ Enabled' : '✗ Disabled'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 text-xs text-gray-500">
|
||||||
|
Added: {new Date(domain.created_at).toLocaleDateString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ml-4">
|
||||||
|
<a
|
||||||
|
href={`https://${domain.domain_name}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="btn btn-primary"
|
||||||
|
>
|
||||||
|
Visit Site →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DomainList
|
||||||
|
|
||||||
|
|
@ -0,0 +1,346 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { dnsAPI } from '../services/api'
|
||||||
|
|
||||||
|
function DomainSetup() {
|
||||||
|
const [step, setStep] = useState(1)
|
||||||
|
const [domain, setDomain] = useState('')
|
||||||
|
const [cfToken, setCfToken] = useState('')
|
||||||
|
const [zoneInfo, setZoneInfo] = useState(null)
|
||||||
|
const [preview, setPreview] = useState(null)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState(null)
|
||||||
|
const [success, setSuccess] = useState(null)
|
||||||
|
|
||||||
|
const handleValidateToken = async (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await dnsAPI.validateToken(domain, cfToken)
|
||||||
|
|
||||||
|
if (response.data.status === 'success') {
|
||||||
|
setZoneInfo(response.data)
|
||||||
|
setStep(2)
|
||||||
|
} else {
|
||||||
|
setError(response.data.message)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.error || 'Failed to validate token')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePreviewChanges = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await dnsAPI.previewChanges(
|
||||||
|
domain,
|
||||||
|
zoneInfo.zone_id,
|
||||||
|
cfToken
|
||||||
|
)
|
||||||
|
|
||||||
|
if (response.data.status !== 'error') {
|
||||||
|
setPreview(response.data)
|
||||||
|
setStep(3)
|
||||||
|
} else {
|
||||||
|
setError(response.data.message)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.error || 'Failed to preview changes')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleApplyChanges = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await dnsAPI.applyChanges(
|
||||||
|
domain,
|
||||||
|
zoneInfo.zone_id,
|
||||||
|
cfToken,
|
||||||
|
preview,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
if (response.data.status === 'success') {
|
||||||
|
setSuccess('Domain successfully configured!')
|
||||||
|
setStep(4)
|
||||||
|
} else {
|
||||||
|
setError('Failed to apply changes')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.error || 'Failed to apply changes')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
setStep(1)
|
||||||
|
setDomain('')
|
||||||
|
setCfToken('')
|
||||||
|
setZoneInfo(null)
|
||||||
|
setPreview(null)
|
||||||
|
setError(null)
|
||||||
|
setSuccess(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
{/* Progress Steps */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
{[1, 2, 3, 4].map((s) => (
|
||||||
|
<div key={s} className="flex items-center">
|
||||||
|
<div
|
||||||
|
className={`w-10 h-10 rounded-full flex items-center justify-center font-bold ${
|
||||||
|
step >= s
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: 'bg-gray-200 text-gray-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{s}
|
||||||
|
</div>
|
||||||
|
{s < 4 && (
|
||||||
|
<div
|
||||||
|
className={`w-24 h-1 mx-2 ${
|
||||||
|
step > s ? 'bg-blue-500' : 'bg-gray-200'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between mt-2 text-sm text-gray-600">
|
||||||
|
<span>Domain Info</span>
|
||||||
|
<span>Verify</span>
|
||||||
|
<span>Preview</span>
|
||||||
|
<span>Complete</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Alert */}
|
||||||
|
{error && (
|
||||||
|
<div className="alert alert-error mb-4">
|
||||||
|
<strong>Error:</strong> {error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Success Alert */}
|
||||||
|
{success && (
|
||||||
|
<div className="alert alert-success mb-4">
|
||||||
|
<strong>Success!</strong> {success}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 1: Domain & Token */}
|
||||||
|
{step === 1 && (
|
||||||
|
<div className="card">
|
||||||
|
<h2 className="text-2xl font-bold mb-6 text-gray-800">
|
||||||
|
Step 1: Enter Domain & Cloudflare Token
|
||||||
|
</h2>
|
||||||
|
<form onSubmit={handleValidateToken} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Domain Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="input"
|
||||||
|
placeholder="example.com"
|
||||||
|
value={domain}
|
||||||
|
onChange={(e) => setDomain(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Cloudflare API Token
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="input"
|
||||||
|
placeholder="Your Cloudflare API Token"
|
||||||
|
value={cfToken}
|
||||||
|
onChange={(e) => setCfToken(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p className="mt-2 text-sm text-gray-500">
|
||||||
|
Get your API token from Cloudflare Dashboard → My Profile → API Tokens
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-primary w-full"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? 'Validating...' : 'Validate & Continue'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 2: Zone Info */}
|
||||||
|
{step === 2 && zoneInfo && (
|
||||||
|
<div className="card">
|
||||||
|
<h2 className="text-2xl font-bold mb-6 text-gray-800">
|
||||||
|
Step 2: Verify Zone Information
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-6">
|
||||||
|
<p className="text-green-800 font-medium">✓ Token validated successfully!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4 text-left">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Zone Name</label>
|
||||||
|
<p className="text-lg font-semibold text-gray-800">{zoneInfo.zone_name}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Zone ID</label>
|
||||||
|
<p className="text-gray-800 font-mono text-sm">{zoneInfo.zone_id}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Status</label>
|
||||||
|
<p className="text-gray-800">{zoneInfo.zone_status}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Nameservers</label>
|
||||||
|
<ul className="list-disc list-inside text-gray-800">
|
||||||
|
{zoneInfo.nameservers?.map((ns, i) => (
|
||||||
|
<li key={i}>{ns}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex space-x-4 mt-6">
|
||||||
|
<button
|
||||||
|
onClick={() => setStep(1)}
|
||||||
|
className="btn btn-secondary flex-1"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handlePreviewChanges}
|
||||||
|
className="btn btn-primary flex-1"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? 'Loading...' : 'Preview Changes'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 3: Preview Changes */}
|
||||||
|
{step === 3 && preview && (
|
||||||
|
<div className="card">
|
||||||
|
<h2 className="text-2xl font-bold mb-6 text-gray-800">
|
||||||
|
Step 3: Preview DNS Changes
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
||||||
|
<p className="text-blue-800">
|
||||||
|
<strong>New IP:</strong> {preview.new_ip}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4 text-left">
|
||||||
|
<h3 className="font-semibold text-gray-800">Changes to be applied:</h3>
|
||||||
|
|
||||||
|
{preview.changes?.map((change, i) => (
|
||||||
|
<div key={i} className="border border-gray-200 rounded-lg p-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="font-medium text-gray-800">
|
||||||
|
{change.record_type} Record: {change.name}
|
||||||
|
</span>
|
||||||
|
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||||
|
change.action === 'create' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
|
||||||
|
}`}>
|
||||||
|
{change.action.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{change.current && (
|
||||||
|
<div className="text-sm text-gray-600 mb-1">
|
||||||
|
Current: {change.current.value} (Proxied: {change.current.proxied ? 'Yes' : 'No'})
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="text-sm text-gray-800 font-medium">
|
||||||
|
New: {change.new.value} (Proxied: {change.new.proxied ? 'Yes' : 'No'})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex space-x-4 mt-6">
|
||||||
|
<button
|
||||||
|
onClick={() => setStep(2)}
|
||||||
|
className="btn btn-secondary flex-1"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleApplyChanges}
|
||||||
|
className="btn btn-primary flex-1"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? 'Applying...' : 'Apply Changes'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 4: Success */}
|
||||||
|
{step === 4 && (
|
||||||
|
<div className="card text-center">
|
||||||
|
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<svg className="w-10 h-10 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-3xl font-bold text-gray-800 mb-4">
|
||||||
|
Domain Configured Successfully!
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="text-gray-600 mb-6">
|
||||||
|
Your domain <strong>{domain}</strong> has been configured with DNS and SSL settings.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6 text-left">
|
||||||
|
<h3 className="font-semibold text-blue-900 mb-2">What's Next?</h3>
|
||||||
|
<ul className="list-disc list-inside text-blue-800 space-y-1">
|
||||||
|
<li>DNS changes may take a few minutes to propagate</li>
|
||||||
|
<li>SSL certificate will be automatically provisioned by Cloudflare</li>
|
||||||
|
<li>Your site will be accessible via HTTPS within 15 minutes</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={resetForm}
|
||||||
|
className="btn btn-primary"
|
||||||
|
>
|
||||||
|
Add Another Domain
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DomainSetup
|
||||||
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://176.96.129.77:5000'
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: API_BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const dnsAPI = {
|
||||||
|
// Health check
|
||||||
|
health: () => api.get('/health'),
|
||||||
|
|
||||||
|
// Validate Cloudflare token
|
||||||
|
validateToken: (domain, cfToken) =>
|
||||||
|
api.post('/api/dns/validate-token', {
|
||||||
|
domain,
|
||||||
|
cf_token: cfToken,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Preview DNS changes
|
||||||
|
previewChanges: (domain, zoneId, cfToken) =>
|
||||||
|
api.post('/api/dns/preview-changes', {
|
||||||
|
domain,
|
||||||
|
zone_id: zoneId,
|
||||||
|
cf_token: cfToken,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Apply DNS changes
|
||||||
|
applyChanges: (domain, zoneId, cfToken, preview, proxyEnabled = true) =>
|
||||||
|
api.post('/api/dns/apply-changes', {
|
||||||
|
domain,
|
||||||
|
zone_id: zoneId,
|
||||||
|
cf_token: cfToken,
|
||||||
|
preview,
|
||||||
|
proxy_enabled: proxyEnabled,
|
||||||
|
customer_id: 1, // TODO: Get from auth
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Get domains
|
||||||
|
getDomains: (customerId = 1) =>
|
||||||
|
api.get('/api/domains', {
|
||||||
|
params: { customer_id: customerId },
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Get domain by ID
|
||||||
|
getDomain: (domainId) => api.get(`/api/domains/${domainId}`),
|
||||||
|
}
|
||||||
|
|
||||||
|
export default api
|
||||||
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: 3001,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://176.96.129.77:5000',
|
||||||
|
changeOrigin: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
Loading…
Reference in New Issue