new changes

This commit is contained in:
Niranjan
2026-04-07 05:05:28 +05:30
parent 7c070224bd
commit a18bba15f2
29975 changed files with 3247495 additions and 2761 deletions

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react'
import Modal from 'react-bootstrap/Modal'
import {
apiRequest,
createSite,
@@ -16,7 +17,7 @@ import {
siteGitClone,
siteGitPull,
} from '../api/client'
import { Plus, Trash2, Download, Archive, RotateCcw, Pencil, Play, Square, ArrowRightLeft, GitBranch } from 'lucide-react'
import { PageHeader, AdminButton, AdminAlert, AdminTable, EmptyState } from '../components/admin'
interface Site {
id: number
@@ -40,7 +41,13 @@ export function SitePage() {
const [backups, setBackups] = useState<{ filename: string; size: number }[]>([])
const [backupLoading, setBackupLoading] = useState(false)
const [editSiteId, setEditSiteId] = useState<number | null>(null)
const [editForm, setEditForm] = useState<{ domains: string; path: string; ps: string; php_version: string; force_https: boolean } | null>(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<number | null>(null)
@@ -137,13 +144,15 @@ export function SitePage() {
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),
}))
.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))
}
@@ -197,7 +206,10 @@ export function SitePage() {
setRedirectAdding(true)
addSiteRedirect(redirectSiteId, redirectSource.trim(), redirectTarget.trim(), redirectCode)
.then(() => listSiteRedirects(redirectSiteId).then(setRedirects))
.then(() => { setRedirectSource(''); setRedirectTarget('') })
.then(() => {
setRedirectSource('')
setRedirectTarget('')
})
.catch((err) => setError(err.message))
.finally(() => setRedirectAdding(false))
}
@@ -242,374 +254,376 @@ export function SitePage() {
return (bytes / 1024 / 1024).toFixed(1) + ' MB'
}
if (loading) return <div className="text-gray-500">Loading...</div>
if (error) return <div className="p-4 rounded bg-red-100 text-red-700">{error}</div>
if (loading) {
return (
<>
<PageHeader title="Website" />
<div className="text-center py-5 text-muted">Loading</div>
</>
)
}
if (error && !sites.length) {
return (
<>
<PageHeader title="Website" />
<AdminAlert variant="danger">{error}</AdminAlert>
</>
)
}
return (
<div>
<div className="flex justify-between items-center mb-4">
<h1 className="text-2xl font-bold text-gray-800 dark:text-white">Website</h1>
<button
onClick={() => setShowCreate(true)}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium"
>
<Plus className="w-4 h-4" />
Add Site
</button>
</div>
<>
<PageHeader
title="Website"
actions={
<AdminButton onClick={() => setShowCreate(true)}>
<i className="ti ti-plus me-1" />
Add Site
</AdminButton>
}
/>
{showCreate && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-full max-w-md">
<h2 className="text-xl font-bold mb-4 text-gray-800 dark:text-white">Create Site</h2>
<form onSubmit={handleCreate} className="space-y-4">
{creatingError && (
<div className="p-2 rounded bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 text-sm">
{creatingError}
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Site Name
</label>
<input
name="name"
type="text"
placeholder="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
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Domain(s) <span className="text-gray-500">(comma or space separated)</span>
</label>
<input
name="domains"
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
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Path <span className="text-gray-500">(optional)</span>
</label>
<input
name="path"
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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
PHP Version
</label>
<select
name="php_version"
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"
>
<option value="74">7.4</option>
<option value="80">8.0</option>
<option value="81">8.1</option>
<option value="82">8.2</option>
</select>
</div>
<div className="flex items-center gap-2">
<input name="force_https" type="checkbox" id="create_force_https" className="rounded" />
<label htmlFor="create_force_https" className="text-sm text-gray-700 dark:text-gray-300">
Force HTTPS (redirect HTTP to HTTPS)
</label>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Note <span className="text-gray-500">(optional)</span>
</label>
<input
name="ps"
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"
/>
</div>
<div className="flex gap-2 justify-end pt-2">
<button
type="button"
onClick={() => setShowCreate(false)}
className="px-4 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg"
>
Cancel
</button>
<button
type="submit"
disabled={creating}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white rounded-lg font-medium"
>
{creating ? 'Creating...' : 'Create'}
</button>
</div>
</form>
</div>
</div>
)}
{error ? <AdminAlert variant="warning">{error}</AdminAlert> : null}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th className="px-4 py-2 text-left text-sm font-medium text-gray-700 dark:text-gray-300">Name</th>
<th className="px-4 py-2 text-left text-sm font-medium text-gray-700 dark:text-gray-300">Path</th>
<th className="px-4 py-2 text-left text-sm font-medium text-gray-700 dark:text-gray-300">Domains</th>
<th className="px-4 py-2 text-left text-sm font-medium text-gray-700 dark:text-gray-300">Type</th>
<th className="px-4 py-2 text-right text-sm font-medium text-gray-700 dark:text-gray-300">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{sites.length === 0 ? (
<Modal show={showCreate} onHide={() => setShowCreate(false)} centered>
<Modal.Header closeButton>
<Modal.Title>Create Site</Modal.Title>
</Modal.Header>
<form onSubmit={handleCreate}>
<Modal.Body>
{creatingError ? <AdminAlert variant="danger">{creatingError}</AdminAlert> : null}
<div className="mb-3">
<label className="form-label">Site Name</label>
<input name="name" type="text" placeholder="example.com" className="form-control" required />
</div>
<div className="mb-3">
<label className="form-label">Domain(s) (comma or space separated)</label>
<input
name="domains"
type="text"
placeholder="example.com www.example.com"
className="form-control"
required
/>
</div>
<div className="mb-3">
<label className="form-label">Path (optional)</label>
<input name="path" type="text" placeholder="/www/wwwroot/example.com" className="form-control" />
</div>
<div className="mb-3">
<label className="form-label">PHP Version</label>
<select name="php_version" className="form-select">
<option value="74">7.4</option>
<option value="80">8.0</option>
<option value="81">8.1</option>
<option value="82">8.2</option>
</select>
</div>
<div className="form-check mb-3">
<input name="force_https" type="checkbox" id="create_force_https" className="form-check-input" />
<label htmlFor="create_force_https" className="form-check-label">
Force HTTPS (redirect HTTP to HTTPS)
</label>
</div>
<div className="mb-0">
<label className="form-label">Note (optional)</label>
<input name="ps" type="text" placeholder="My website" className="form-control" />
</div>
</Modal.Body>
<Modal.Footer>
<button type="button" className="btn btn-light" onClick={() => setShowCreate(false)}>
Cancel
</button>
<button type="submit" disabled={creating} className="btn btn-primary">
{creating ? 'Creating…' : 'Create'}
</button>
</Modal.Footer>
</form>
</Modal>
<div className="card shadow-sm border-0">
<div className="card-body p-0">
<AdminTable>
<thead className="table-light">
<tr>
<td colSpan={5} className="px-4 py-8 text-center text-gray-500">
No sites yet. Click "Add Site" to create one.
</td>
<th>Name</th>
<th>Path</th>
<th>Domains</th>
<th>Type</th>
<th className="text-end">Actions</th>
</tr>
) : (
sites.map((s) => (
<tr key={s.id}>
<td className="px-4 py-2 text-gray-900 dark:text-white">{s.name}</td>
<td className="px-4 py-2 text-gray-600 dark:text-gray-400">{s.path}</td>
<td className="px-4 py-2 text-gray-600 dark:text-gray-400">{s.domain_count}</td>
<td className="px-4 py-2 text-gray-600 dark:text-gray-400">{s.project_type}</td>
<td className="px-4 py-2 text-right">
<span className="flex gap-1 justify-end">
</thead>
<tbody>
{sites.length === 0 ? (
<tr>
<td colSpan={5} className="p-0">
<EmptyState title="No sites yet" description='Click "Add Site" to create one.' />
</td>
</tr>
) : (
sites.map((s) => (
<tr key={s.id}>
<td className="align-middle">{s.name}</td>
<td className="align-middle text-muted">{s.path}</td>
<td className="align-middle">{s.domain_count}</td>
<td className="align-middle">{s.project_type}</td>
<td className="align-middle text-end text-nowrap">
{s.status === 1 ? (
<button
type="button"
onClick={() => handleSetStatus(s.id, false)}
disabled={statusLoading === s.id}
className="p-2 text-amber-600 hover:bg-amber-50 dark:hover:bg-amber-900/20 rounded disabled:opacity-50"
className="btn btn-sm btn-outline-warning me-1"
title="Stop"
>
<Square className="w-4 h-4" />
<i className="ti ti-player-stop" />
</button>
) : (
<button
type="button"
onClick={() => handleSetStatus(s.id, true)}
disabled={statusLoading === s.id}
className="p-2 text-green-600 hover:bg-green-50 dark:hover:bg-green-900/20 rounded disabled:opacity-50"
className="btn btn-sm btn-outline-success me-1"
title="Start"
>
<Play className="w-4 h-4" />
<i className="ti ti-player-play" />
</button>
)}
<button
onClick={() => { setGitSiteId(s.id); setGitAction('clone'); setGitUrl(''); setGitBranch('main') }}
className="p-2 text-emerald-600 hover:bg-emerald-50 dark:hover:bg-emerald-900/20 rounded"
type="button"
onClick={() => {
setGitSiteId(s.id)
setGitAction('clone')
setGitUrl('')
setGitBranch('main')
}}
className="btn btn-sm btn-outline-success me-1"
title="Git Deploy"
>
<GitBranch className="w-4 h-4" />
<i className="ti ti-git-branch" />
</button>
<button
type="button"
onClick={() => openRedirectModal(s.id)}
className="p-2 text-purple-600 hover:bg-purple-50 dark:hover:bg-purple-900/20 rounded"
className="btn btn-sm btn-outline-secondary me-1"
title="Redirects"
>
<ArrowRightLeft className="w-4 h-4" />
<i className="ti ti-arrows-right-left" />
</button>
<button
type="button"
onClick={() => openEditModal(s.id)}
className="p-2 text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded"
className="btn btn-sm btn-outline-primary me-1"
title="Edit"
>
<Pencil className="w-4 h-4" />
<i className="ti ti-pencil" />
</button>
<button
type="button"
onClick={() => openBackupModal(s.id)}
className="p-2 text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded"
className="btn btn-sm btn-outline-primary me-1"
title="Backup"
>
<Archive className="w-4 h-4" />
<i className="ti ti-archive" />
</button>
<button
type="button"
onClick={() => handleDelete(s.id, s.name)}
className="p-2 text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded"
className="btn btn-sm btn-outline-danger"
title="Delete"
>
<Trash2 className="w-4 h-4" />
<i className="ti ti-trash" />
</button>
</span>
</td>
</tr>
))
)}
</tbody>
</table>
</td>
</tr>
))
)}
</tbody>
</AdminTable>
</div>
</div>
{gitSiteId && gitAction && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-full max-w-md">
<h2 className="text-xl font-bold mb-4 text-gray-800 dark:text-white">Git Deploy</h2>
<div className="flex gap-2 mb-4">
<button
type="button"
onClick={() => setGitAction('clone')}
className={`px-3 py-1.5 rounded text-sm ${gitAction === 'clone' ? 'bg-emerald-600 text-white' : 'bg-gray-200 dark:bg-gray-700'}`}
>
Clone
</button>
<button
type="button"
onClick={() => setGitAction('pull')}
className={`px-3 py-1.5 rounded text-sm ${gitAction === 'pull' ? 'bg-emerald-600 text-white' : 'bg-gray-200 dark:bg-gray-700'}`}
>
Pull
</button>
</div>
{gitAction === 'clone' ? (
<form onSubmit={handleGitClone} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Repository URL</label>
<input
value={gitUrl}
onChange={(e) => 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
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Branch</label>
<input
value={gitBranch}
onChange={(e) => setGitBranch(e.target.value)}
placeholder="main"
className="w-full px-4 py-2 border rounded-lg bg-white dark:bg-gray-700"
/>
</div>
<p className="text-xs text-gray-500">Site path must be empty. This will clone the repo into the site directory.</p>
<div className="flex gap-2 justify-end">
<button type="button" onClick={() => { setGitSiteId(null); setGitAction(null) }} className="px-4 py-2 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700">Cancel</button>
<button type="submit" disabled={gitLoading} className="px-4 py-2 bg-emerald-600 text-white rounded-lg disabled:opacity-50">
{gitLoading ? 'Cloning...' : 'Clone'}
</button>
</div>
</form>
) : (
<div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">Pull latest changes from the remote repository.</p>
<div className="flex gap-2 justify-end">
<button type="button" onClick={() => { setGitSiteId(null); setGitAction(null) }} className="px-4 py-2 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700">Cancel</button>
<button onClick={handleGitPull} disabled={gitLoading} className="px-4 py-2 bg-emerald-600 text-white rounded-lg disabled:opacity-50">
{gitLoading ? 'Pulling...' : 'Pull'}
</button>
</div>
</div>
)}
<Modal show={!!gitSiteId && !!gitAction} onHide={() => { setGitSiteId(null); setGitAction(null) }} centered>
<Modal.Header closeButton>
<Modal.Title>Git Deploy</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className="btn-group mb-3" role="group">
<button
type="button"
className={`btn btn-sm ${gitAction === 'clone' ? 'btn-success' : 'btn-outline-secondary'}`}
onClick={() => setGitAction('clone')}
>
Clone
</button>
<button
type="button"
className={`btn btn-sm ${gitAction === 'pull' ? 'btn-success' : 'btn-outline-secondary'}`}
onClick={() => setGitAction('pull')}
>
Pull
</button>
</div>
</div>
)}
{redirectSiteId && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-full max-w-lg">
<h2 className="text-xl font-bold mb-4 text-gray-800 dark:text-white">Redirects</h2>
<form onSubmit={handleAddRedirect} className="space-y-2 mb-4">
<div className="flex gap-2 flex-wrap">
{gitAction === 'clone' ? (
<form onSubmit={handleGitClone}>
<div className="mb-3">
<label className="form-label">Repository URL</label>
<input
value={redirectSource}
onChange={(e) => 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"
value={gitUrl}
onChange={(e) => setGitUrl(e.target.value)}
placeholder="https://github.com/user/repo.git"
className="form-control"
required
/>
<span className="self-center text-gray-500"></span>
</div>
<div className="mb-3">
<label className="form-label">Branch</label>
<input
value={redirectTarget}
onChange={(e) => 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"
value={gitBranch}
onChange={(e) => setGitBranch(e.target.value)}
placeholder="main"
className="form-control"
/>
<select
value={redirectCode}
onChange={(e) => setRedirectCode(Number(e.target.value))}
className="px-3 py-2 border rounded-lg bg-white dark:bg-gray-700"
</div>
<p className="small text-muted">Site path must be empty for clone.</p>
<div className="d-flex justify-content-end gap-2">
<button
type="button"
className="btn btn-light"
onClick={() => {
setGitSiteId(null)
setGitAction(null)
}}
>
<option value={301}>301</option>
<option value={302}>302</option>
</select>
<button type="submit" disabled={redirectAdding} className="px-4 py-2 bg-blue-600 text-white rounded-lg disabled:opacity-50">
Add
Cancel
</button>
<button type="submit" disabled={gitLoading} className="btn btn-success">
{gitLoading ? 'Cloning…' : 'Clone'}
</button>
</div>
</form>
<div className="max-h-48 overflow-y-auto">
{redirects.length === 0 ? (
<p className="text-gray-500 text-sm">No redirects</p>
) : (
<ul className="space-y-2">
{redirects.map((r) => (
<li key={r.id} className="flex items-center justify-between gap-2 text-sm py-1 border-b dark:border-gray-700">
<span className="font-mono truncate">{r.source}</span>
<span className="text-gray-500"></span>
<span className="font-mono truncate flex-1">{r.target}</span>
<span className="text-gray-400">{r.code}</span>
<button onClick={() => handleDeleteRedirect(r.id)} className="p-1 text-red-600 hover:bg-red-50 rounded">
<Trash2 className="w-4 h-4" />
</button>
</li>
))}
</ul>
)}
) : (
<div>
<p className="text-muted small">Pull latest changes from the remote repository.</p>
<div className="d-flex justify-content-end gap-2">
<button
type="button"
className="btn btn-light"
onClick={() => {
setGitSiteId(null)
setGitAction(null)
}}
>
Cancel
</button>
<button type="button" onClick={handleGitPull} disabled={gitLoading} className="btn btn-success">
{gitLoading ? 'Pulling…' : 'Pull'}
</button>
</div>
</div>
<div className="mt-4 flex justify-end">
<button onClick={() => setRedirectSiteId(null)} className="px-4 py-2 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700">
Close
)}
</Modal.Body>
</Modal>
<Modal show={redirectSiteId != null} onHide={() => setRedirectSiteId(null)} size="lg" centered scrollable>
<Modal.Header closeButton>
<Modal.Title>Redirects</Modal.Title>
</Modal.Header>
<Modal.Body>
<form onSubmit={handleAddRedirect} className="row g-2 align-items-end mb-3">
<div className="col-md-3">
<label className="form-label small">Source</label>
<input
value={redirectSource}
onChange={(e) => setRedirectSource(e.target.value)}
placeholder="/old-path"
className="form-control form-control-sm"
/>
</div>
<div className="col-md-3">
<label className="form-label small">Target</label>
<input
value={redirectTarget}
onChange={(e) => setRedirectTarget(e.target.value)}
placeholder="/new-path"
className="form-control form-control-sm"
/>
</div>
<div className="col-md-2">
<label className="form-label small">Code</label>
<select
value={redirectCode}
onChange={(e) => setRedirectCode(Number(e.target.value))}
className="form-select form-select-sm"
>
<option value={301}>301</option>
<option value={302}>302</option>
</select>
</div>
<div className="col-md-2">
<button type="submit" disabled={redirectAdding} className="btn btn-primary btn-sm w-100">
Add
</button>
</div>
</div>
</div>
)}
</form>
{redirects.length === 0 ? (
<p className="text-muted small mb-0">No redirects</p>
) : (
<ul className="list-group list-group-flush">
{redirects.map((r) => (
<li key={r.id} className="list-group-item d-flex align-items-center gap-2 small">
<code className="text-truncate">{r.source}</code>
<span className="text-muted"></span>
<code className="text-truncate flex-grow-1">{r.target}</code>
<span className="badge bg-secondary">{r.code}</span>
<button type="button" className="btn btn-sm btn-link text-danger p-0" onClick={() => handleDeleteRedirect(r.id)}>
<i className="ti ti-trash" />
</button>
</li>
))}
</ul>
)}
</Modal.Body>
<Modal.Footer>
<button type="button" className="btn btn-light" onClick={() => setRedirectSiteId(null)}>
Close
</button>
</Modal.Footer>
</Modal>
{editSiteId && editForm && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-full max-w-md">
<h2 className="text-xl font-bold mb-4 text-gray-800 dark:text-white">Edit Site</h2>
<form onSubmit={handleEdit} className="space-y-4">
{editError && (
<div className="p-2 rounded bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 text-sm">
{editError}
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Domain(s) <span className="text-gray-500">(comma or space separated)</span>
</label>
<Modal show={editSiteId != null && editForm != null} onHide={() => { setEditSiteId(null); setEditForm(null) }} centered>
<Modal.Header closeButton>
<Modal.Title>Edit Site</Modal.Title>
</Modal.Header>
{editForm ? (
<form onSubmit={handleEdit}>
<Modal.Body>
{editError ? <AdminAlert variant="danger">{editError}</AdminAlert> : null}
<div className="mb-3">
<label className="form-label">Domain(s)</label>
<input
value={editForm.domains}
onChange={(e) => 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"
className="form-control"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Path <span className="text-gray-500">(optional)</span>
</label>
<div className="mb-3">
<label className="form-label">Path (optional)</label>
<input
value={editForm.path}
onChange={(e) => 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"
className="form-control"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">PHP Version</label>
<div className="mb-3">
<label className="form-label">PHP Version</label>
<select
value={editForm.php_version}
onChange={(e) => setEditForm({ ...editForm, php_version: e.target.value })}
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"
className="form-select"
>
<option value="74">7.4</option>
<option value="80">8.0</option>
@@ -617,108 +631,87 @@ export function SitePage() {
<option value="82">8.2</option>
</select>
</div>
<div className="flex items-center gap-2">
<div className="form-check mb-3">
<input
type="checkbox"
id="edit_force_https"
className="form-check-input"
checked={editForm.force_https}
onChange={(e) => setEditForm({ ...editForm, force_https: e.target.checked })}
className="rounded"
/>
<label htmlFor="edit_force_https" className="text-sm text-gray-700 dark:text-gray-300">
<label htmlFor="edit_force_https" className="form-check-label">
Force HTTPS
</label>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Note <span className="text-gray-500">(optional)</span>
</label>
<div className="mb-0">
<label className="form-label">Note (optional)</label>
<input
value={editForm.ps}
onChange={(e) => 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"
className="form-control"
/>
</div>
<div className="flex gap-2 justify-end pt-2">
<button
type="button"
onClick={() => { setEditSiteId(null); setEditForm(null) }}
className="px-4 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg"
>
Cancel
</button>
<button
type="submit"
disabled={editLoading}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white rounded-lg font-medium"
>
{editLoading ? 'Saving...' : 'Save'}
</button>
</div>
</form>
</div>
</div>
)}
</Modal.Body>
<Modal.Footer>
<button type="button" className="btn btn-light" onClick={() => { setEditSiteId(null); setEditForm(null) }}>
Cancel
</button>
<button type="submit" disabled={editLoading} className="btn btn-primary">
{editLoading ? 'Saving…' : 'Save'}
</button>
</Modal.Footer>
</form>
) : null}
</Modal>
{backupSiteId && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-full max-w-lg">
<h2 className="text-xl font-bold mb-4 text-gray-800 dark:text-white">Site Backup</h2>
<div className="mb-4">
<button
onClick={handleCreateBackup}
disabled={backupLoading}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg disabled:opacity-50"
>
<Archive className="w-4 h-4" />
{backupLoading ? 'Creating...' : 'Create Backup'}
</button>
</div>
<div className="mb-4">
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Existing backups</h3>
{backups.length === 0 ? (
<p className="text-gray-500 text-sm">No backups yet</p>
) : (
<ul className="space-y-2 max-h-48 overflow-y-auto">
{backups.map((b) => (
<li key={b.filename} className="flex items-center justify-between gap-2 text-sm">
<span className="truncate font-mono text-gray-700 dark:text-gray-300 flex-1">{b.filename}</span>
<span className="text-gray-500 text-xs flex-shrink-0">{formatSize(b.size)}</span>
<span className="flex gap-1 flex-shrink-0">
<button
onClick={() => handleDownloadBackup(b.filename)}
className="p-1.5 text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded"
title="Download"
>
<Download className="w-4 h-4" />
</button>
<button
onClick={() => handleRestore(b.filename)}
disabled={backupLoading}
className="p-1.5 text-amber-600 hover:bg-amber-50 dark:hover:bg-amber-900/20 rounded disabled:opacity-50"
title="Restore"
>
<RotateCcw className="w-4 h-4" />
</button>
</span>
</li>
))}
</ul>
)}
</div>
<div className="flex justify-end">
<button
onClick={() => setBackupSiteId(null)}
className="px-4 py-2 text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg"
>
Close
</button>
</div>
<Modal show={backupSiteId != null} onHide={() => setBackupSiteId(null)} size="lg" centered>
<Modal.Header closeButton>
<Modal.Title>Site Backup</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className="mb-3">
<AdminButton onClick={handleCreateBackup} disabled={backupLoading}>
<i className="ti ti-archive me-1" />
{backupLoading ? 'Creating…' : 'Create Backup'}
</AdminButton>
</div>
</div>
)}
</div>
<h6 className="text-muted small">Existing backups</h6>
{backups.length === 0 ? (
<p className="text-muted small">No backups yet</p>
) : (
<ul className="list-group list-group-flush">
{backups.map((b) => (
<li key={b.filename} className="list-group-item d-flex align-items-center justify-content-between gap-2">
<code className="small text-truncate flex-grow-1">{b.filename}</code>
<span className="text-muted small">{formatSize(b.size)}</span>
<button
type="button"
className="btn btn-sm btn-link"
onClick={() => handleDownloadBackup(b.filename)}
title="Download"
>
<i className="ti ti-download" />
</button>
<button
type="button"
className="btn btn-sm btn-link text-warning"
onClick={() => handleRestore(b.filename)}
disabled={backupLoading}
>
<i className="ti ti-restore" />
</button>
</li>
))}
</ul>
)}
</Modal.Body>
<Modal.Footer>
<button type="button" className="btn btn-light" onClick={() => setBackupSiteId(null)}>
Close
</button>
</Modal.Footer>
</Modal>
</>
)
}