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