From 8d0b56473836dff54fff3dd16c0c1096ebdb54a4 Mon Sep 17 00:00:00 2001 From: oguz ozturk Date: Sat, 10 Jan 2026 13:14:12 +0300 Subject: [PATCH] Add React frontend with Tailwind CSS --- frontend/index.html | 14 ++ frontend/package.json | 32 +++ frontend/postcss.config.js | 7 + frontend/src/App.css | 79 +++++++ frontend/src/App.jsx | 70 ++++++ frontend/src/index.css | 33 +++ frontend/src/main.jsx | 11 + frontend/src/pages/DomainList.jsx | 151 +++++++++++++ frontend/src/pages/DomainSetup.jsx | 346 +++++++++++++++++++++++++++++ frontend/src/services/api.js | 53 +++++ frontend/tailwind.config.js | 12 + frontend/vite.config.js | 17 ++ 12 files changed, 825 insertions(+) create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.jsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.jsx create mode 100644 frontend/src/pages/DomainList.jsx create mode 100644 frontend/src/pages/DomainSetup.jsx create mode 100644 frontend/src/services/api.js create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/vite.config.js diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..7f0c547 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,14 @@ + + + + + + + Hosting Platform - DNS & SSL Management + + +
+ + + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..c201e6b --- /dev/null +++ b/frontend/package.json @@ -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" + } +} + diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..b4a6220 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,7 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} + diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..0ad5111 --- /dev/null +++ b/frontend/src/App.css @@ -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; +} + diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..29f761c --- /dev/null +++ b/frontend/src/App.jsx @@ -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 ( +
+ {/* Header */} +
+
+
+
+
+ H +
+
+

Hosting Platform

+

DNS & SSL Management

+
+
+ + +
+
+
+ + {/* Main Content */} +
+ {currentPage === 'setup' ? : } +
+ + {/* Footer */} +
+
+
+

© 2024 Hosting Platform. Powered by Cloudflare.

+

Automated DNS & SSL Management

+
+
+
+
+ ) +} + +export default App + diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..10c0aec --- /dev/null +++ b/frontend/src/index.css @@ -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; +} + diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..299bc52 --- /dev/null +++ b/frontend/src/main.jsx @@ -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( + + + , +) + diff --git a/frontend/src/pages/DomainList.jsx b/frontend/src/pages/DomainList.jsx new file mode 100644 index 0000000..83cd3f1 --- /dev/null +++ b/frontend/src/pages/DomainList.jsx @@ -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 ( +
+
+
+

Loading domains...

+
+
+ ) + } + + if (error) { + return ( +
+
+ Error: {error} +
+ +
+ ) + } + + if (domains.length === 0) { + return ( +
+
+ + + +
+

No Domains Yet

+

+ You haven't added any domains yet. Click "Add Domain" to get started. +

+
+ ) + } + + return ( +
+
+

My Domains

+ +
+ +
+ {domains.map((domain) => ( +
+
+
+
+

+ {domain.domain_name} +

+ + {domain.status} + +
+ +
+
+ Load Balancer IP: +

{domain.lb_ip || 'N/A'}

+
+ +
+ DNS Configured: +

+ {domain.dns_configured ? '✓ Yes' : '✗ No'} +

+
+ +
+ SSL Configured: +

+ {domain.ssl_configured ? '✓ Yes' : '✗ No'} +

+
+ +
+ Cloudflare Proxy: +

+ {domain.cf_proxy_enabled ? '✓ Enabled' : '✗ Disabled'} +

+
+
+ +
+ Added: {new Date(domain.created_at).toLocaleDateString()} +
+
+ + +
+
+ ))} +
+
+ ) +} + +export default DomainList + diff --git a/frontend/src/pages/DomainSetup.jsx b/frontend/src/pages/DomainSetup.jsx new file mode 100644 index 0000000..a5e725c --- /dev/null +++ b/frontend/src/pages/DomainSetup.jsx @@ -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 ( +
+ {/* Progress Steps */} +
+
+ {[1, 2, 3, 4].map((s) => ( +
+
= s + ? 'bg-blue-500 text-white' + : 'bg-gray-200 text-gray-500' + }`} + > + {s} +
+ {s < 4 && ( +
s ? 'bg-blue-500' : 'bg-gray-200' + }`} + /> + )} +
+ ))} +
+
+ Domain Info + Verify + Preview + Complete +
+
+ + {/* Error Alert */} + {error && ( +
+ Error: {error} +
+ )} + + {/* Success Alert */} + {success && ( +
+ Success! {success} +
+ )} + + {/* Step 1: Domain & Token */} + {step === 1 && ( +
+

+ Step 1: Enter Domain & Cloudflare Token +

+
+
+ + setDomain(e.target.value)} + required + /> +
+ +
+ + setCfToken(e.target.value)} + required + /> +

+ Get your API token from Cloudflare Dashboard → My Profile → API Tokens +

+
+ + +
+
+ )} + + {/* Step 2: Zone Info */} + {step === 2 && zoneInfo && ( +
+

+ Step 2: Verify Zone Information +

+ +
+

✓ Token validated successfully!

+
+ +
+
+ +

{zoneInfo.zone_name}

+
+ +
+ +

{zoneInfo.zone_id}

+
+ +
+ +

{zoneInfo.zone_status}

+
+ +
+ +
    + {zoneInfo.nameservers?.map((ns, i) => ( +
  • {ns}
  • + ))} +
+
+
+ +
+ + +
+
+ )} + + {/* Step 3: Preview Changes */} + {step === 3 && preview && ( +
+

+ Step 3: Preview DNS Changes +

+ +
+

+ New IP: {preview.new_ip} +

+
+ +
+

Changes to be applied:

+ + {preview.changes?.map((change, i) => ( +
+
+ + {change.record_type} Record: {change.name} + + + {change.action.toUpperCase()} + +
+ + {change.current && ( +
+ Current: {change.current.value} (Proxied: {change.current.proxied ? 'Yes' : 'No'}) +
+ )} + +
+ New: {change.new.value} (Proxied: {change.new.proxied ? 'Yes' : 'No'}) +
+
+ ))} +
+ +
+ + +
+
+ )} + + {/* Step 4: Success */} + {step === 4 && ( +
+
+ + + +
+ +

+ Domain Configured Successfully! +

+ +

+ Your domain {domain} has been configured with DNS and SSL settings. +

+ +
+

What's Next?

+
    +
  • DNS changes may take a few minutes to propagate
  • +
  • SSL certificate will be automatically provisioned by Cloudflare
  • +
  • Your site will be accessible via HTTPS within 15 minutes
  • +
+
+ + +
+ )} +
+ ) +} + +export default DomainSetup + diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js new file mode 100644 index 0000000..59c773f --- /dev/null +++ b/frontend/src/services/api.js @@ -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 + diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..d37737f --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,12 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} + diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..1572bfa --- /dev/null +++ b/frontend/vite.config.js @@ -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, + } + } + } +}) +