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,105 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
/** Tools CRUD: list, create (with generated SEO content), edit, delete. */
|
||||
final class ToolsAdmin extends AdminBase
|
||||
{
|
||||
public function index(): string
|
||||
{
|
||||
$q = trim((string) $this->request->getGet('q'));
|
||||
|
||||
$query = $this->toolsModel()->orderBy('name', 'ASC')->limit(300);
|
||||
if ($q !== '') {
|
||||
$query->like('name', $q)->orLike('slug', $q);
|
||||
}
|
||||
|
||||
return $this->render('tools_index', [
|
||||
'tools' => $query->findAll(),
|
||||
'categories' => $this->categories(),
|
||||
'q' => $q,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): string
|
||||
{
|
||||
return $this->render('tools_form', [
|
||||
'tool' => null,
|
||||
'categories' => $this->categories(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(): ResponseInterface
|
||||
{
|
||||
return $this->save(null);
|
||||
}
|
||||
|
||||
public function edit(int $id): string
|
||||
{
|
||||
$tool = $this->toolsModel()->find($id) ?? throw new \CodeIgniter\Exceptions\PageNotFoundException();
|
||||
|
||||
return $this->render('tools_form', [
|
||||
'tool' => $tool,
|
||||
'categories' => $this->categories(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(int $id): ResponseInterface
|
||||
{
|
||||
return $this->save($id);
|
||||
}
|
||||
|
||||
private function save(?int $id): ResponseInterface
|
||||
{
|
||||
$post = $this->request->getPost();
|
||||
$catId = (int) ($post['category_id'] ?? 0);
|
||||
|
||||
try {
|
||||
$row = $this->toolFromInput((array) $post, $catId);
|
||||
} catch (\Throwable $e) {
|
||||
session()->setFlashdata('error', 'Could not build the tool: ' . $e->getMessage());
|
||||
|
||||
return redirect()->back()->withInput();
|
||||
}
|
||||
|
||||
// pre-publish SEO validation (critical checks only)
|
||||
$problems = [];
|
||||
if (trim((string) $row['seo_title']) === '') {
|
||||
$problems[] = 'SEO title missing';
|
||||
}
|
||||
if (mb_strlen((string) $row['seo_description']) < 50) {
|
||||
$problems[] = 'SEO description too short (<50 chars)';
|
||||
}
|
||||
if (trim((string) $row['h1']) === '') {
|
||||
$problems[] = 'H1 missing';
|
||||
}
|
||||
if ($problems !== [] && ($row['status'] ?? '') === 'active') {
|
||||
session()->setFlashdata('error', 'Blocked from publishing: ' . implode('; ', $problems));
|
||||
session()->setFlashdata('form_data', $post);
|
||||
|
||||
return redirect()->to($id ? '/admin/tools/' . $id . '/edit' : '/admin/tools/new')->withInput();
|
||||
}
|
||||
|
||||
if ($id !== null) {
|
||||
$this->toolsModel()->update($id, $row);
|
||||
session()->setFlashdata('success', 'Tool updated.');
|
||||
} else {
|
||||
$this->toolsModel()->insert($row);
|
||||
session()->setFlashdata('success', 'Tool created.');
|
||||
}
|
||||
|
||||
return redirect()->to('/admin/tools');
|
||||
}
|
||||
|
||||
public function delete(int $id): ResponseInterface
|
||||
{
|
||||
$this->toolsModel()->delete($id);
|
||||
service('cache')->clear();
|
||||
|
||||
return redirect()->to('/admin/tools');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user