import { useEffect, useState } from 'react' import { apiRequest, createSite, getSite, updateSite, setSiteStatus, deleteSite, createSiteBackup, listSiteBackups, restoreSiteBackup, downloadSiteBackup, listSiteRedirects, addSiteRedirect, deleteSiteRedirect, siteGitClone, siteGitPull, } from '../api/client' import { Plus, Trash2, Download, Archive, RotateCcw, Pencil, Play, Square, Redirect, GitBranch } from 'lucide-react' interface Site { id: number name: string path: string status: number ps: string project_type: string domain_count: number addtime: string | null } export function SitePage() { const [sites, setSites] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState('') const [showCreate, setShowCreate] = useState(false) const [creating, setCreating] = useState(false) const [creatingError, setCreatingError] = useState('') const [backupSiteId, setBackupSiteId] = useState(null) const [backups, setBackups] = useState<{ filename: string; size: number }[]>([]) const [backupLoading, setBackupLoading] = useState(false) const [editSiteId, setEditSiteId] = useState(null) const [editForm, setEditForm] = useState<{ domains: string; path: string; ps: string; php_version: string; force_https: boolean } | null>(null) const [editLoading, setEditLoading] = useState(false) const [editError, setEditError] = useState('') const [statusLoading, setStatusLoading] = useState(null) const [redirectSiteId, setRedirectSiteId] = useState(null) const [redirects, setRedirects] = useState<{ id: number; source: string; target: string; code: number }[]>([]) const [redirectSource, setRedirectSource] = useState('') const [redirectTarget, setRedirectTarget] = useState('') const [redirectCode, setRedirectCode] = useState(301) const [redirectAdding, setRedirectAdding] = useState(false) const [gitSiteId, setGitSiteId] = useState(null) const [gitUrl, setGitUrl] = useState('') const [gitBranch, setGitBranch] = useState('main') const [gitAction, setGitAction] = useState<'clone' | 'pull' | null>(null) const [gitLoading, setGitLoading] = useState(false) const loadSites = () => { setLoading(true) apiRequest('/site/list') .then(setSites) .catch((err) => setError(err.message)) .finally(() => setLoading(false)) } useEffect(() => { loadSites() }, []) const handleCreate = (e: React.FormEvent) => { e.preventDefault() const form = e.currentTarget const name = (form.elements.namedItem('name') as HTMLInputElement).value.trim() const domainsStr = (form.elements.namedItem('domains') as HTMLInputElement).value.trim() const path = (form.elements.namedItem('path') as HTMLInputElement).value.trim() const ps = (form.elements.namedItem('ps') as HTMLInputElement).value.trim() if (!name || !domainsStr) { setCreatingError('Name and domain(s) are required') return } const domains = domainsStr.split(/[\s,]+/).filter(Boolean) const php_version = (form.elements.namedItem('php_version') as HTMLSelectElement)?.value || '74' const force_https = (form.elements.namedItem('force_https') as HTMLInputElement)?.checked || false setCreating(true) setCreatingError('') createSite({ name, domains, path: path || undefined, ps: ps || undefined, php_version, force_https }) .then(() => { setShowCreate(false) form.reset() loadSites() }) .catch((err) => setCreatingError(err.message)) .finally(() => setCreating(false)) } const handleDelete = (id: number, name: string) => { if (!confirm(`Delete site "${name}"? This cannot be undone.`)) return deleteSite(id) .then(loadSites) .catch((err) => setError(err.message)) } const openBackupModal = (siteId: number) => { setBackupSiteId(siteId) setBackups([]) listSiteBackups(siteId) .then((data) => setBackups(data.backups)) .catch((err) => setError(err.message)) } const handleCreateBackup = () => { if (!backupSiteId) return setBackupLoading(true) createSiteBackup(backupSiteId) .then(() => listSiteBackups(backupSiteId).then((d) => setBackups(d.backups))) .catch((err) => setError(err.message)) .finally(() => setBackupLoading(false)) } const handleRestore = (filename: string) => { if (!backupSiteId || !confirm(`Restore from ${filename}? This will overwrite existing files.`)) return setBackupLoading(true) restoreSiteBackup(backupSiteId, filename) .then(() => setBackupSiteId(null)) .catch((err) => setError(err.message)) .finally(() => setBackupLoading(false)) } const handleDownloadBackup = (filename: string) => { if (!backupSiteId) return downloadSiteBackup(backupSiteId, filename).catch((err) => setError(err.message)) } const openEditModal = (siteId: number) => { setEditSiteId(siteId) setEditError('') getSite(siteId) .then((s) => setEditForm({ domains: (s.domains || []).join(', '), path: s.path || '', ps: s.ps || '', php_version: s.php_version || '74', force_https: !!(s.force_https && s.force_https !== 0), })) .catch((err) => setEditError(err.message)) } const handleEdit = (e: React.FormEvent) => { e.preventDefault() if (!editSiteId || !editForm) return const domains = editForm.domains.split(/[\s,]+/).filter(Boolean) if (domains.length === 0) { setEditError('At least one domain is required') return } setEditLoading(true) setEditError('') updateSite(editSiteId, { domains, path: editForm.path || undefined, ps: editForm.ps || undefined, php_version: editForm.php_version, force_https: editForm.force_https, }) .then(() => { setEditSiteId(null) setEditForm(null) loadSites() }) .catch((err) => setEditError(err.message)) .finally(() => setEditLoading(false)) } const handleSetStatus = (siteId: number, enable: boolean) => { setStatusLoading(siteId) setSiteStatus(siteId, enable) .then(loadSites) .catch((err) => setError(err.message)) .finally(() => setStatusLoading(null)) } const openRedirectModal = (siteId: number) => { setRedirectSiteId(siteId) setRedirectSource('') setRedirectTarget('') setRedirectCode(301) listSiteRedirects(siteId) .then(setRedirects) .catch((err) => setError(err.message)) } const handleAddRedirect = (e: React.FormEvent) => { e.preventDefault() if (!redirectSiteId || !redirectSource.trim() || !redirectTarget.trim()) return setRedirectAdding(true) addSiteRedirect(redirectSiteId, redirectSource.trim(), redirectTarget.trim(), redirectCode) .then(() => listSiteRedirects(redirectSiteId).then(setRedirects)) .then(() => { setRedirectSource(''); setRedirectTarget('') }) .catch((err) => setError(err.message)) .finally(() => setRedirectAdding(false)) } const handleDeleteRedirect = (redirectId: number) => { if (!redirectSiteId) return deleteSiteRedirect(redirectSiteId, redirectId) .then(() => listSiteRedirects(redirectSiteId).then(setRedirects)) .catch((err) => setError(err.message)) } const formatSize = (bytes: number) => { if (bytes < 1024) return bytes + ' B' if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB' return (bytes / 1024 / 1024).toFixed(1) + ' MB' } if (loading) return
Loading...
if (error) return
{error}
return (

Website

{showCreate && (

Create Site

{creatingError && (
{creatingError}
)}
)}
{sites.length === 0 ? ( ) : ( sites.map((s) => ( )) )}
Name Path Domains Type Actions
No sites yet. Click "Add Site" to create one.
{s.name} {s.path} {s.domain_count} {s.project_type} {s.status === 1 ? ( ) : ( )}
{gitSiteId && gitAction && (

Git Deploy

{gitAction === 'clone' ? (
setGitUrl(e.target.value)} placeholder="https://github.com/user/repo.git" className="w-full px-4 py-2 border rounded-lg bg-white dark:bg-gray-700" required />
setGitBranch(e.target.value)} placeholder="main" className="w-full px-4 py-2 border rounded-lg bg-white dark:bg-gray-700" />

Site path must be empty. This will clone the repo into the site directory.

) : (

Pull latest changes from the remote repository.

)}
)} {redirectSiteId && (

Redirects

setRedirectSource(e.target.value)} placeholder="/old-path" className="flex-1 min-w-[100px] px-3 py-2 border rounded-lg bg-white dark:bg-gray-700" /> setRedirectTarget(e.target.value)} placeholder="/new-path or https://..." className="flex-1 min-w-[100px] px-3 py-2 border rounded-lg bg-white dark:bg-gray-700" />
{redirects.length === 0 ? (

No redirects

) : (
    {redirects.map((r) => (
  • {r.source} {r.target} {r.code}
  • ))}
)}
)} {editSiteId && editForm && (

Edit Site

{editError && (
{editError}
)}
setEditForm({ ...editForm, domains: e.target.value })} type="text" placeholder="example.com www.example.com" className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white" required />
setEditForm({ ...editForm, path: e.target.value })} type="text" placeholder="/www/wwwroot/example.com" className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white" />
setEditForm({ ...editForm, force_https: e.target.checked })} className="rounded" />
setEditForm({ ...editForm, ps: e.target.value })} type="text" placeholder="My website" className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white" />
)} {backupSiteId && (

Site Backup

Existing backups

{backups.length === 0 ? (

No backups yet

) : (
    {backups.map((b) => (
  • {b.filename} {formatSize(b.size)}
  • ))}
)}
)}
) }