| Current Path : /home/seto/indexator.pm/templates/projects/ |
| Current File : /home/seto/indexator.pm/templates/projects/create.php |
<?php
$title = 'New Project — Indexator';
ob_start();
?>
<div class="row justify-content-center">
<div class="col-lg-7">
<h1 class="h3 mb-4">New Project</h1>
<!-- Top-level mode tabs -->
<ul class="nav nav-tabs mb-4" id="modeTabs" role="tablist">
<li class="nav-item">
<button class="nav-link active" id="mode-single" data-bs-toggle="tab"
data-bs-target="#panel-single" type="button">Single project</button>
</li>
<li class="nav-item">
<button class="nav-link" id="mode-bulk" data-bs-toggle="tab"
data-bs-target="#panel-bulk" type="button">Bulk import</button>
</li>
</ul>
<div class="tab-content">
<!-- ── Single project ──────────────────────────────────────────── -->
<div class="tab-pane fade show active" id="panel-single">
<form method="POST" action="/projects" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="<?= htmlspecialchars($csrf) ?>">
<div class="mb-3">
<label for="name" class="form-label fw-semibold">Project name</label>
<input type="text" id="name" name="name"
class="form-control <?= isset($errors['name']) ? 'is-invalid' : '' ?>"
value="<?= htmlspecialchars($old['name'] ?? '') ?>" required maxlength="255">
<?php if (isset($errors['name'])): ?>
<div class="invalid-feedback"><?= htmlspecialchars($errors['name']) ?></div>
<?php endif; ?>
</div>
<div class="mb-3">
<label for="domain" class="form-label fw-semibold">Domain</label>
<input type="text" id="domain" name="domain"
class="form-control <?= isset($errors['domain']) ? 'is-invalid' : '' ?>"
value="<?= htmlspecialchars($old['domain'] ?? '') ?>"
placeholder="example.com" required maxlength="255">
<?php if (isset($errors['domain'])): ?>
<div class="invalid-feedback"><?= htmlspecialchars($errors['domain']) ?></div>
<?php endif; ?>
</div>
<!-- Import mode -->
<div class="mb-3">
<label class="form-label fw-semibold">Import source</label>
<ul class="nav nav-pills mb-3" id="importTabs" role="tablist">
<li class="nav-item">
<button class="nav-link active" id="tab-manual" data-bs-toggle="pill"
data-bs-target="#panel-manual" type="button">Manual list</button>
</li>
<li class="nav-item">
<button class="nav-link" id="tab-file" data-bs-toggle="pill"
data-bs-target="#panel-file" type="button">File (TXT/CSV)</button>
</li>
<li class="nav-item">
<button class="nav-link" id="tab-sitemap" data-bs-toggle="pill"
data-bs-target="#panel-sitemap" type="button">Sitemap URL</button>
</li>
</ul>
<input type="hidden" name="source_type" id="source_type"
value="<?= htmlspecialchars($old['source_type'] ?? 'manual') ?>">
<div class="tab-content border rounded p-3">
<div class="tab-pane fade show active" id="panel-manual">
<label for="urls_manual" class="form-label text-muted small">One URL per line</label>
<textarea id="urls_manual" name="urls_manual"
class="form-control font-monospace" rows="10"
placeholder="https://example.com/page-1 https://example.com/page-2"><?= htmlspecialchars($old['urls_manual'] ?? '') ?></textarea>
</div>
<div class="tab-pane fade" id="panel-file">
<label for="urls_file" class="form-label text-muted small">TXT or CSV file (one URL per line / first column)</label>
<input type="file" id="urls_file" name="urls_file" class="form-control"
accept=".txt,.csv,text/plain,text/csv">
</div>
<div class="tab-pane fade" id="panel-sitemap">
<label for="sitemap_url" class="form-label text-muted small">XML sitemap URL</label>
<input type="url" id="sitemap_url" name="sitemap_url" class="form-control"
value="<?= htmlspecialchars($old['sitemap_url'] ?? '') ?>"
placeholder="https://example.com/sitemap.xml">
<?php if (isset($errors['sitemap'])): ?>
<div class="text-danger small mt-1"><?= htmlspecialchars($errors['sitemap']) ?></div>
<?php endif; ?>
</div>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-dark">Create Project</button>
<a href="/projects" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
<!-- ── Bulk import ─────────────────────────────────────────────── -->
<div class="tab-pane fade" id="panel-bulk">
<div id="bulk-form">
<div class="mb-3">
<label for="bulk-domains" class="form-label fw-semibold">Domains</label>
<textarea id="bulk-domains" class="form-control font-monospace" rows="12"
placeholder="example.com another-site.com third-site.net"></textarea>
<div class="form-text">One domain per line. http/https prefix is stripped automatically.</div>
</div>
<div class="mb-4">
<label for="bulk-sitemap-path" class="form-label fw-semibold">Sitemap path</label>
<input type="text" id="bulk-sitemap-path" class="form-control"
value="/sitemap.xml" placeholder="/sitemap.xml" maxlength="255">
<div class="form-text">Applied to all domains: <code>https://domain.com<span id="sitemap-path-preview">/sitemap.xml</span></code></div>
</div>
<div class="d-flex gap-2">
<button type="button" id="bulk-start-btn" class="btn btn-dark">Start Import</button>
<a href="/projects" class="btn btn-outline-secondary">Cancel</a>
</div>
</div>
<!-- Progress area (hidden until import starts) -->
<div id="bulk-progress" class="mt-4" style="display:none;">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="fw-semibold">Import progress</span>
<span id="bulk-counter" class="text-muted small"></span>
</div>
<div class="progress mb-3" style="height:8px;">
<div id="bulk-progress-bar" class="progress-bar progress-bar-striped progress-bar-animated bg-dark"
style="width:0%"></div>
</div>
<!-- Live log -->
<div id="bulk-log-wrap" class="border rounded bg-dark text-white font-monospace p-3"
style="max-height:320px; overflow-y:auto; font-size:.8rem;">
</div>
<!-- Summary (shown on done) -->
<div id="bulk-summary" class="mt-3" style="display:none;"></div>
</div>
</div>
</div><!-- /tab-content -->
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Sync single-project import tabs
const tabMap = { 'tab-manual': 'manual', 'tab-file': 'file', 'tab-sitemap': 'sitemap' };
document.querySelectorAll('[data-bs-toggle="pill"]').forEach(function (btn) {
btn.addEventListener('shown.bs.tab', function () {
document.getElementById('source_type').value = tabMap[btn.id] || 'manual';
});
});
const sourceType = '<?= htmlspecialchars($old['source_type'] ?? 'manual') ?>';
const tabId = Object.entries(tabMap).find(([, v]) => v === sourceType)?.[0];
if (tabId && tabId !== 'tab-manual') {
const tabEl = document.getElementById(tabId);
if (tabEl) new bootstrap.Tab(tabEl).show();
}
// Sitemap path preview
const pathInput = document.getElementById('bulk-sitemap-path');
const pathPreview = document.getElementById('sitemap-path-preview');
if (pathInput && pathPreview) {
pathInput.addEventListener('input', function () {
pathPreview.textContent = pathInput.value || '/sitemap.xml';
});
}
// ── Bulk import ────────────────────────────────────────────────────────
const startBtn = document.getElementById('bulk-start-btn');
const bulkForm = document.getElementById('bulk-form');
const progressArea = document.getElementById('bulk-progress');
const progressBar = document.getElementById('bulk-progress-bar');
const counterEl = document.getElementById('bulk-counter');
const logWrap = document.getElementById('bulk-log-wrap');
const summaryEl = document.getElementById('bulk-summary');
if (!startBtn) return;
startBtn.addEventListener('click', function () {
const domains = document.getElementById('bulk-domains').value.trim();
const sitemapPath = pathInput.value.trim() || '/sitemap.xml';
if (!domains) {
alert('Please enter at least one domain.');
return;
}
const csrf = document.querySelector('meta[name="csrf-token"]').content;
startBtn.disabled = true;
startBtn.textContent = 'Starting…';
fetch('/api/bulk-import/start', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: '_csrf=' + encodeURIComponent(csrf)
+ '&domains=' + encodeURIComponent(domains)
+ '&sitemap_path=' + encodeURIComponent(sitemapPath),
})
.then(function (res) { return res.json(); })
.then(function (data) {
if (data.error) { alert(data.error); startBtn.disabled = false; startBtn.textContent = 'Start Import'; return; }
bulkForm.style.display = 'none';
progressArea.style.display = '';
pollBulkJob(data.job_id, data.total);
})
.catch(function () {
alert('Failed to start import.');
startBtn.disabled = false;
startBtn.textContent = 'Start Import';
});
});
function pollBulkJob(jobId, total) {
let lastRowCount = 0;
let done = false;
const timer = setInterval(function () {
if (done) { clearInterval(timer); return; }
fetch('/api/bulk-import/' + jobId + '/status')
.then(function (res) { return res.json(); })
.then(function (data) {
const rows = data.rows || [];
// Append new log lines
for (let i = lastRowCount; i < rows.length; i++) {
const r = rows[i];
const div = document.createElement('div');
div.className = r.type === 'OK' ? 'text-success' : r.type === 'ERROR' ? 'text-danger' : 'text-secondary';
div.textContent = (r.type === 'OK' ? '✓' : r.type === 'ERROR' ? '✗' : '—')
+ ' ' + r.domain + ' ' + r.detail;
logWrap.appendChild(div);
}
lastRowCount = rows.length;
// Auto-scroll
logWrap.scrollTop = logWrap.scrollHeight;
// Progress bar
if (total > 0) {
const pct = Math.round(rows.length / total * 100);
progressBar.style.width = pct + '%';
counterEl.textContent = rows.length + ' / ' + total;
}
if (data.status === 'done' || data.status === 'error') {
done = true;
clearInterval(timer);
progressBar.classList.remove('progress-bar-animated');
if (data.summary) {
const s = data.summary;
summaryEl.style.display = '';
summaryEl.innerHTML =
'<div class="alert alert-success mb-2">'
+ '<strong>Import complete.</strong> '
+ 'Created: <strong>' + s.created + '</strong> '
+ 'Skipped: <strong>' + s.skipped + '</strong> '
+ 'Errors: <strong>' + s.errors + '</strong>'
+ '</div>'
+ '<a href="/projects" class="btn btn-dark">View Projects</a>';
}
}
})
.catch(function () { /* ignore transient errors */ });
}, 1500);
}
});
</script>
<?php
$content = ob_get_clean();
require __DIR__ . '/../layout.php';