Toolvana: production-ready media tools platform (CodeIgniter 4)
- 134-tool registry with programmatic SEO (unique titles/H1/descriptions, JSON-LD graphs, sitemap index, canonical 301 enforcement via required filter) - DB-backed job queue (SKIP LOCKED) with drivers: Ffmpeg, Images (GD), Pdf (qpdf/gs/poppler), Youtube (thumbnails), Qr (server-side PNG) - Security: SSRF guard, MIME validation, rate limits, API-key auth, bcrypt admin login, security headers - Admin panel: dashboard, tools/categories/guides CRUD, SEO audit, analytics, job inspector with retry, system health, feature flags - Docker deployment (nginx + web/api FPM pools + scalable workers), PHPUnit suite (19 tests / 1139 assertions), PWA manifest + service worker
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
<?= view('partials/admin_nav') ?>
|
||||
<h1 style="font-size:1.7rem;margin-bottom:1rem">Analytics (anonymous events only)</h1>
|
||||
<div class="grid cols-4" style="margin-bottom:1.4rem">
|
||||
<?php foreach (['tool_view' => 'Tool views', 'tool_start' => 'Jobs started', 'tool_success' => 'Successes', 'download' => 'Downloads'] as $key => $label): ?>
|
||||
<div class="card"><p style="font-size:.78rem;text-transform:uppercase;color:var(--ink-500)"><?= esc($label) ?></p>
|
||||
<h2 style="font-size:1.7rem;font-weight:800;margin:.15rem 0"><?= number_format($counts[$key] ?? 0) ?> <span class="hint">/24h</span></h2></div>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">Top tools (7 days)</h2>
|
||||
<table style="width:100%;font-size:.87rem;border-collapse:collapse;background:#fff;border-radius:var(--radius-sm);box-shadow:var(--shadow-sm)">
|
||||
<tr style="text-align:left;color:var(--ink-500)"><th style="padding:.65rem">Tool</th><th>Views</th><th>Starts</th><th>Success rate</th></tr>
|
||||
<?php foreach ($topTools as $row): ?>
|
||||
<tr style="border-top:1px solid #f2f2f5">
|
||||
<td style="padding:.5rem .65rem"><a href="/<?= esc($row['tool_slug']) ?>"><?= esc(str_replace('-', ' ', $row['tool_slug'])) ?></a></td>
|
||||
<td><?= number_format((float)$row['views']) ?></td>
|
||||
<td><?= number_format((float)$row['starts']) ?></td>
|
||||
<td><?= $row['success_rate'] === null ? '–' : $row['success_rate'] . '%' ?></td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
|
||||
<div class="grid cols-2" style="margin-top:1.4rem">
|
||||
<div class="card">
|
||||
<h3>Output formats (7d)</h3>
|
||||
<?php if ($formats === []): ?><p class="hint">No downloads yet.</p><?php else: ?>
|
||||
<ul style="padding-left:1.1rem"><?php foreach ($formats as $f): ?><li><strong><?= esc(strtoupper($f['format'])) ?></strong> — <?= number_format((float)$f['total']) ?></li><?php endforeach ?></ul>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Recent searches</h3>
|
||||
<?php if ($searches === []): ?><p class="hint">None yet.</p><?php else: ?>
|
||||
<ul style="padding-left:1.1rem"><?php foreach ($searches as $s): $m = json_decode((string)($s['meta'] ?? '{}'), true); ?><li><?= esc($m['term'] ?? '?') ?></li><?php endforeach ?></ul>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<p>Use the inline editor on the categories index page.</p>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?= view('partials/admin_nav') ?>
|
||||
<h1 style="font-size:1.7rem;margin-bottom:1rem">Categories</h1>
|
||||
<div class="grid cols-2">
|
||||
<?php foreach ($categories as $c): ?>
|
||||
<form method="post" action="/admin/categories/<?= (int)$c['id'] ?>" class="card" style="display:grid;gap:.5rem">
|
||||
<?= csrf_field() ?>
|
||||
<strong>/<?= esc($c['slug']) ?></strong>
|
||||
<input type="hidden" name="sort_order" value="<?= (int)$c['sort_order'] ?>">
|
||||
<input type="text" name="name" value="<?= esc($c['name']) ?>" style="border:1px solid #ddd;border-radius:8px;padding:.4rem .6rem">
|
||||
<input type="text" name="tagline" value="<?= esc($c['tagline']) ?>" placeholder="Tagline" style="border:1px solid #ddd;border-radius:8px;padding:.4rem .6rem">
|
||||
<input type="text" name="seo_title" value="<?= esc($c['seo_title']) ?>" placeholder="SEO title" style="border:1px solid #ddd;border-radius:8px;padding:.4rem .6rem">
|
||||
<button type="submit" class="btn-primary" style="font-size:.85rem;padding:.5rem 1rem">Save</button>
|
||||
</form>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
@@ -0,0 +1,43 @@
|
||||
<?= view('partials/admin_nav') ?>
|
||||
<h1 style="font-size:1.7rem;letter-spacing:-.02em;margin-bottom:1.2rem">Platform overview</h1>
|
||||
<div class="grid cols-4" style="margin-bottom:1.6rem">
|
||||
<?php
|
||||
$stats = [
|
||||
['Tools', $toolCount, '/admin/tools'],
|
||||
['Guides', $guideCount, '/admin/guides'],
|
||||
['Queued jobs', $queued, '/admin/jobs?status=queued'],
|
||||
['Processing', $processing, '/admin/jobs?status=processing'],
|
||||
];
|
||||
foreach ($stats as [$label, $value, $href]): ?>
|
||||
<a class="card" href="<?= esc($href) ?>">
|
||||
<p style="font-size:.8rem;text-transform:uppercase;letter-spacing:.06em"><?= esc($label) ?></p>
|
||||
<h2 style="font-size:2rem;font-weight:800;margin:.2rem 0 0"><?= (int) $value ?></h2>
|
||||
</a>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
<div class="grid cols-2">
|
||||
<div class="card">
|
||||
<h3>Today</h3>
|
||||
<p><?= number_format($viewsToday) ?> tool views · <?= number_format($startsToday) ?> jobs started · <?= (int) $failedToday ?> failed</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Recent searches</h3>
|
||||
<?php if ($searches === []): ?><p class="hint">No searches yet.</p><?php else: ?>
|
||||
<ul style="margin:.3rem 0;padding-left:1.2rem">
|
||||
<?php foreach ($searches as $s): $meta = json_decode((string) ($s['meta'] ?? '{}'), true); ?>
|
||||
<li><?= esc($meta['term'] ?? '?') ?></li>
|
||||
<?php endforeach ?>
|
||||
</ul><?php endif ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="margin-top:1rem">
|
||||
<h3>Latest jobs</h3>
|
||||
<table style="width:100%;font-size:.85rem;border-collapse:collapse">
|
||||
<tr style="text-align:left;color:var(--ink-500)"><th>Job</th><th>Operation</th><th>Status</th><th>Created</th></tr>
|
||||
<?php foreach ($recentJobs as $j): ?>
|
||||
<tr><td><code><?= esc(substr($j['id'],0,8)) ?></code></td><td><?= esc($j['operation']) ?></td>
|
||||
<td><span class="badge" style="<?= $j['status']==='completed'?'background:#ecfdf5;color:#047857':($j['status']==='failed'?'background:#fef2f2;color:#b91c1c':'') ?>"><?= esc($j['status']) ?></span></td>
|
||||
<td><?= esc($j['created_at']) ?></td></tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?= view('partials/admin_nav') ?>
|
||||
<h1 style="font-size:1.7rem;margin-bottom:1rem"><?= $guide ? 'Edit guide' : 'New guide' ?></h1>
|
||||
<form method="post" action="/admin/guides<?= $guide ? '/' . (int)$guide['id'] : '' ?>" class="tool-panel" style="max-width:820px">
|
||||
<?= csrf_field() ?>
|
||||
<div class="opt-grid" style="padding-top:0">
|
||||
<div><label>Title *</label><input type="text" name="title" required value="<?= esc($guide['title'] ?? '') ?>"></div>
|
||||
<div><label>Slug *</label><input type="text" name="slug" required pattern="[a-z0-9-]+" value="<?= esc($guide['slug'] ?? '') ?>"></div>
|
||||
<div><label>SEO title</label><input type="text" name="seo_title" value="<?= esc($guide['seo_title'] ?? '') ?>"></div>
|
||||
<div><label>Status</label>
|
||||
<select name="status"><?php foreach (['published','draft'] as $st): ?><option value="<?= $st ?>" <?= ($guide['status'] ?? 'draft')===$st?'selected':'' ?>><?= $st ?></option><?php endforeach ?></select></div>
|
||||
</div>
|
||||
<div style="margin-top:1rem"><label>Excerpt</label>
|
||||
<textarea name="excerpt" class="tool-area" style="min-height:55px"><?= esc($guide['excerpt'] ?? '') ?></textarea></div>
|
||||
<div class="opt-grid" style="margin-top:.6rem">
|
||||
<div style="grid-column:span 2"><label>Tool slugs to interlink (comma separated)</label>
|
||||
<input type="text" name="tool_slugs" value="<?= esc(implode(', ', json_decode((string)($guide['tool_slugs'] ?? '[]'), true) ?: [])) ?>"></div>
|
||||
<div><label>Meta description</label>
|
||||
<input type="text" name="seo_description" value="<?= esc($guide['seo_description'] ?? '') ?>"></div>
|
||||
</div>
|
||||
<div style="margin-top:1rem"><label>Body (Markdown)</label>
|
||||
<textarea name="body_md" class="tool-area" style="min-height:420px;font-family:ui-monospace,monospace;font-size:.86rem"><?= esc($guide['body_md'] ?? '') ?></textarea></div>
|
||||
<button class="btn-primary" type="submit" style="margin-top:1rem"><?= $guide ? 'Save' : 'Create' ?></button>
|
||||
</form>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?= view('partials/admin_nav') ?>
|
||||
<div style="display:flex;align-items:center;gap:1rem">
|
||||
<h1 style="font-size:1.7rem">Guides</h1>
|
||||
<a class="btn-primary" href="/admin/guides/new" style="text-decoration:none;font-size:.9rem;padding:.55rem 1.2rem;margin-left:auto">+ New guide</a>
|
||||
</div>
|
||||
<table style="width:100%;font-size:.86rem;border-collapse:collapse;background:#fff;border-radius:var(--radius-sm);box-shadow:var(--shadow-sm);margin-top:1rem">
|
||||
<tr style="text-align:left;color:var(--ink-500)"><th style="padding:.7rem">Title</th><th>Status</th><th>Views</th><th>Published</th><th></th></tr>
|
||||
<?php foreach ($guides as $g): ?>
|
||||
<tr style="border-top:1px solid #f2f2f5">
|
||||
<td style="padding:.6rem .7rem"><?= esc($g['title']) ?></td>
|
||||
<td><span class="badge" <?= $g['status']!=='published' ? 'style="background:#fffbeb;color:#b45309"' : '' ?>><?= esc($g['status']) ?></span></td>
|
||||
<td><?= (int)$g['views'] ?></td>
|
||||
<td><?= esc(substr($g['published_at'],0,10)) ?></td>
|
||||
<td><a href="/admin/guides/<?= (int)$g['id'] ?>/edit">Edit</a></td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?= view('partials/admin_nav') ?>
|
||||
<h1 style="font-size:1.7rem;margin-bottom:1rem">Processing jobs</h1>
|
||||
<div class="chips" style="margin-bottom:1rem">
|
||||
<a class="chip" href="/admin/jobs">all (<?= $counts['completed'] + $counts['failed'] + $counts['queued'] + $counts['processing'] ?>)</a>
|
||||
<a class="chip" href="/admin/jobs?status=queued">queued (<?= $counts['queued'] ?>)</a>
|
||||
<a class="chip" href="/admin/jobs?status=processing">processing (<?= $counts['processing'] ?>)</a>
|
||||
<a class="chip" href="/admin/jobs?status=completed">completed (<?= $counts['completed'] ?>)</a>
|
||||
<a class="chip" href="/admin/jobs?status=failed">failed (<?= $counts['failed'] ?>)</a>
|
||||
</div>
|
||||
<table style="width:100%;font-size:.84rem;border-collapse:collapse;background:#fff;border-radius:var(--radius-sm);box-shadow:var(--shadow-sm)">
|
||||
<tr style="text-align:left;color:var(--ink-500)"><th style="padding:.65rem">Job</th><th>Op</th><th>In</th><th>Status / stage</th><th>Error</th><th></th></tr>
|
||||
<?php foreach ($jobs as $j): ?>
|
||||
<tr style="border-top:1px solid #f2f2f5">
|
||||
<td style="padding:.5rem .65rem"><code><?= esc(substr($j['id'],0,8)) ?></code><br><span class="hint"><?= esc($j['created_at']) ?></span></td>
|
||||
<td><?= esc($j['operation']) ?></td>
|
||||
<td><?= esc($j['input_name'] ?? '—') ?></td>
|
||||
<td><span class="badge" style="<?= $j['status']==='completed'?'background:#ecfdf5;color:#047857':($j['status']==='failed'?'background:#fef2f2;color:#b91c1c':'') ?>"><?= esc($j['status']) ?></span> <?= esc($j['stage']) ?> · <?= (int)$j['progress'] ?>%</td>
|
||||
<td style="max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" class="hint"><?= esc(mb_substr($j['error'] ?? '', 0, 60)) ?></td>
|
||||
<td><?php if (in_array($j['status'], ['failed', 'expired'], true)): ?><form method="post" action="/admin/jobs/<?= esc($j['id']) ?>/retry"><?= csrf_field() ?><button class="chip" type="submit" style="cursor:pointer;border:1px solid var(--brand-300,#c4b5fd)">Retry</button></form><?php endif ?></td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="wrap" style="max-width:460px;margin-top:4rem">
|
||||
<h1 style="font-size:1.6rem;letter-spacing:-.02em">Admin sign-in</h1>
|
||||
<?php if (! empty($error)): ?><p class="notice error"><?= esc($error) ?></p><?php endif ?>
|
||||
<form method="post" action="/admin/login" class="tool-panel" style="padding:1.4rem">
|
||||
<?= csrf_field() ?>
|
||||
<label for="password" style="display:block;font-size:.85rem;font-weight:650;margin-bottom:.35rem">Password</label>
|
||||
<input type="password" id="password" name="password" required autofocus style="width:100%" class="input-lg">
|
||||
<button class="btn-primary" type="submit" style="margin-top:1rem;width:100%">Sign in</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?= view('partials/admin_nav') ?>
|
||||
<h1 style="font-size:1.7rem;margin-bottom:.4rem">SEO audit</h1>
|
||||
<p class="hint" style="margin-bottom:1rem">
|
||||
Auditing <?= $toolCount ?> tool pages + <?= $guideCount ?> guides ·
|
||||
sitemap: <a href="<?= esc($sitemapUrl) ?>"><?= esc(parse_url($sitemapUrl, PHP_URL_PATH)) ?></a>
|
||||
</p>
|
||||
<div class="card" style="margin-bottom:1.4rem;display:flex;gap:2rem;align-items:center">
|
||||
<h2 style="font-size:2.2rem;font-weight:800;color:<?= $criticalCount ? 'var(--err)' : 'var(--ok)' ?>;margin:0"><?= $criticalCount ?></h2>
|
||||
<p style="margin:0">critical issue<?= $criticalCount===1?'':'s' ?><br><span class="hint">Warnings below do not block publishing.</span></p>
|
||||
</div>
|
||||
<table style="width:100%;font-size:.87rem;border-collapse:collapse;background:#fff;border-radius:var(--radius-sm);box-shadow:var(--shadow-sm)">
|
||||
<tr style="text-align:left;color:var(--ink-500)"><th style="padding:.7rem">Severity</th><th>Page</th><th>Check</th></tr>
|
||||
<?php foreach ($issues as $issue): ?>
|
||||
<tr style="border-top:1px solid #f2f2f5">
|
||||
<td style="padding:.5rem .7rem"><span class="badge" style="<?= $issue['severity']==='critical' ? 'background:#fef2f2;color:#b91c1c' : 'background:#fffbeb;color:#b45309' ?>"><?= esc($issue['severity']) ?></span></td>
|
||||
<td><code><?= esc($issue['page']) ?></code></td>
|
||||
<td><?= esc($issue['check']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
<?php if ($issues === []): ?><tr><td colspan="3" style="padding:1rem">All checks passed.</td></tr><?php endif ?>
|
||||
</table>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?= view('partials/admin_nav') ?>
|
||||
<h1 style="font-size:1.7rem;margin-bottom:1rem">Settings & feature flags</h1>
|
||||
<form method="post" action="/admin/settings" class="tool-panel" style="max-width:640px">
|
||||
<?= csrf_field() ?>
|
||||
<?php foreach ($flags as $key => $label): ?>
|
||||
<label style="display:flex;gap:.7rem;align-items:flex-start;padding:.7rem 0;border-bottom:1px solid #f2f2f5">
|
||||
<input type="checkbox" name="<?= esc($key) ?>" value="1" <?= ($values[$key] ?? '')==='1' ? 'checked' : '' ?>>
|
||||
<span style="font-size:.93rem"><?= esc($label) ?></span>
|
||||
</label>
|
||||
<?php endforeach ?>
|
||||
<div style="margin-top:1rem">
|
||||
<label for="retention_hours" style="font-size:.85rem;font-weight:650">File retention override (hours, 1–72)</label>
|
||||
<input id="retention_hours" name="retention_hours" type="number" min="1" max="72" placeholder="site default"
|
||||
value="<?= esc($values['retention_hours'] ?? '') ?>" style="border:1.5px solid var(--ink-300);border-radius:9px;padding:.5rem .6rem;width:140px">
|
||||
</div>
|
||||
<button class="btn-primary" type="submit" style="margin-top:1.2rem">Save settings</button>
|
||||
</form>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?= view('partials/admin_nav') ?>
|
||||
<h1 style="font-size:1.7rem;margin-bottom:1rem">System health</h1>
|
||||
<div class="grid cols-2">
|
||||
<div class="card">
|
||||
<h3>Environment</h3>
|
||||
<p>PHP <?= esc($phpVersion) ?> · CI <?= esc($ciVersion) ?> · <?= esc($environment) ?></p>
|
||||
<p>Database: <?= $dbOk ? '<span style="color:var(--ok);font-weight:700">connected</span>' : '<span style="color:var(--err);font-weight:700">down</span>' ?></p>
|
||||
<p>Worker heartbeat: <?= $workerBeat ? esc($workerBeat) : '<em>none — is a worker running?</em>' ?></p>
|
||||
<p>yt-dlp capability: <?= $ytDlpEnabled ? 'enabled' : 'disabled by policy' ?></p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Storage</h3>
|
||||
<p>Free disk: <?= number_format($diskFreeMb) ?> MB</p>
|
||||
<p>Result files in temp storage: <?= (int)$storageFiles ?></p>
|
||||
<p>Pending uploads: <?= (int)$incoming ?></p>
|
||||
<p>Retention: every file expires after <strong><?= (int)$retentionHrs ?> h</strong> (<code>spark media:cleanup</code>)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="margin-top:1rem">
|
||||
<h3>Processing binaries</h3>
|
||||
<table style="width:100%;font-size:.88rem">
|
||||
<?php foreach ($binaries as $name => $info): ?>
|
||||
<tr><td style="padding:.35rem 0"><strong><?= esc($name) ?></strong></td><td class="hint"><?= esc($info['path'] ?: 'not configured') ?></td>
|
||||
<td><?= $info['ok'] ? '<span style="color:var(--ok);font-weight:700">available</span>' : '<span style="color:var(--warn);font-weight:700">missing</span>' ?></td></tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
<?= view('partials/admin_nav') ?>
|
||||
<h1 style="font-size:1.7rem;margin-bottom:1rem"><?= $tool ? 'Edit tool' : 'New tool' ?></h1>
|
||||
<?php if ($e = session()->getFlashdata('error')): ?><p class="notice error"><?= esc($e) ?></p><?php endif ?>
|
||||
<form method="post" action="/admin/tools<?= $tool ? '/' . (int)$tool['id'] : '' ?>" class="tool-panel" style="max-width:760px">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="_method" value="">
|
||||
<div class="opt-grid" style="padding-top:0">
|
||||
<div><label>Name *</label><input type="text" name="name" required value="<?= esc($tool['name'] ?? '') ?>"></div>
|
||||
<div><label>Slug *</label><input type="text" name="slug" required pattern="[a-z0-9-]+" value="<?= esc($tool['slug'] ?? '') ?>"></div>
|
||||
<div><label>Category</label>
|
||||
<select name="category_id"><?php foreach ($categories as $c): ?><option value="<?= (int)$c['id'] ?>" <?= ($tool && (int)$tool['category_id']===(int)$c['id'])?'selected':'' ?>><?= esc($c['name']) ?></option><?php endforeach ?></select></div>
|
||||
<div><label>Kind</label>
|
||||
<select name="kind"><?php foreach (['upload','url','text'] as $k): ?><option value="<?= $k ?>" <?= ($tool['kind'] ?? '')===$k?'selected':'' ?>><?= $k ?></option><?php endforeach ?></select></div>
|
||||
<div><label>Operation</label><input type="text" name="operation" value="<?= esc($tool['operation'] ?? 'convert') ?>" placeholder="convert / compress / trim …"></div>
|
||||
<div><label>Primary format in</label><input type="text" name="format_in" value="" placeholder="mp4"></div>
|
||||
<div><label>Primary format out</label><input type="text" name="format_out" value="" placeholder="mp3"></div>
|
||||
<div><label>Status</label>
|
||||
<select name="status"><?php foreach (['active','draft','inactive'] as $st): ?><option value="<?= $st ?>" <?= ($tool['status'] ?? 'active')===$st?'selected':'' ?>><?= $st ?></option><?php endforeach ?></select></div>
|
||||
</div>
|
||||
<div style="margin-top:1rem"><label>Short description</label>
|
||||
<textarea name="short_description" class="tool-area" style="min-height:60px"><?= esc($tool['short_description'] ?? '') ?></textarea></div>
|
||||
<div class="opt-grid" style="margin-top:1rem">
|
||||
<div><label>SEO title</label><input type="text" name="seo_title" value="<?= esc($tool['seo_title'] ?? '') ?>"></div>
|
||||
<div><label>H1</label><input type="text" name="h1" value="<?= esc($tool['h1'] ?? '') ?>"></div>
|
||||
<div><label>Aliases (comma separated)</label><input type="text" name="aliases" value=""></div>
|
||||
<div><label>Popularity</label><input type="number" name="popularity" value="<?= (int)($tool['popularity'] ?? 1000) ?>"></div>
|
||||
</div>
|
||||
<div style="margin-top:1rem"><label>Meta description</label>
|
||||
<textarea name="seo_description" class="tool-area" style="min-height:60px"><?= esc($tool['seo_description'] ?? '') ?></textarea></div>
|
||||
<div style="margin-top:1rem"><label>Intro paragraph</label>
|
||||
<textarea name="intro" class="tool-area" style="min-height:80px"><?= esc($tool['intro'] ?? '') ?></textarea></div>
|
||||
<p class="hint" style="margin-top:.8rem">Leave format fields blank for non-converter tools — SEO copy is auto-generated from the format pair where possible. Critical SEO checks block publishing.</p>
|
||||
<button class="btn-primary" type="submit" style="margin-top:1rem"><?= $tool ? 'Save changes' : 'Create tool' ?></button>
|
||||
</form>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?= view('partials/admin_nav') ?>
|
||||
<div style="display:flex;align-items:center;gap:1rem">
|
||||
<h1 style="font-size:1.7rem">Tools (<?= count($tools) ?>)</h1>
|
||||
<a class="btn-primary" href="/admin/tools/new" style="text-decoration:none;font-size:.9rem;padding:.55rem 1.2rem;margin-left:auto">+ New tool</a>
|
||||
</div>
|
||||
<form method="get" action="/admin/tools" class="hero-search" style="max-width:420px;margin:1rem 0;box-shadow:var(--shadow-sm)">
|
||||
<input type="search" name="q" value="<?= esc($q) ?>" placeholder="Filter by name or slug…">
|
||||
<button type="submit">Filter</button>
|
||||
</form>
|
||||
<table style="width:100%;font-size:.86rem;border-collapse:collapse;background:#fff;border-radius:var(--radius-sm);box-shadow:var(--shadow-sm)">
|
||||
<tr style="text-align:left;color:var(--ink-500)"><th style="padding:.7rem">Tool</th><th>Category</th><th>Kind</th><th>Status</th><th>Pop.</th><th></th></tr>
|
||||
<?php foreach ($tools as $t): ?>
|
||||
<tr style="border-top:1px solid #f2f2f5">
|
||||
<td style="padding:.6rem .7rem"><strong><?= esc($t['name']) ?></strong><br><code style="font-size:.78rem;color:var(--ink-500)">/<?= esc($t['slug']) ?></code></td>
|
||||
<td><?= esc(service('finder')->categoryById((int)$t['category_id'])['name'] ?? '?') ?></td>
|
||||
<td><?= esc($t['kind']) ?></td>
|
||||
<td><span class="badge" <?= $t['status']!=='active' ? 'style="background:#fffbeb;color:#b45309"' : '' ?>><?= esc($t['status']) ?></span></td>
|
||||
<td><?= (int)$t['popularity'] ?></td>
|
||||
<td><a href="/admin/tools/<?= (int)$t['id'] ?>/edit">Edit</a></td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
Reference in New Issue
Block a user