view = new View(__DIR__ . '/views'); } public function index(): Response { $db = Database::get(); $posts = []; $page = null; if ($db instanceof PDO) { $pageStmt = $db->prepare("SELECT title, content_html FROM ac_pages WHERE is_blog_index = 1 AND is_published = 1 LIMIT 1"); $pageStmt->execute(); $page = $pageStmt->fetch(PDO::FETCH_ASSOC) ?: null; $stmt = $db->prepare(" SELECT title, slug, excerpt, published_at, featured_image_url, author_name, category, tags FROM ac_posts WHERE is_published = 1 ORDER BY COALESCE(published_at, created_at) DESC "); $stmt->execute(); $posts = $stmt->fetchAll(PDO::FETCH_ASSOC); } return new Response($this->view->render('site/index.php', [ 'title' => 'News', 'posts' => $posts, 'page' => $page, ])); } public function show(): Response { $slug = trim((string)($_GET['slug'] ?? '')); if ($slug === '') { return $this->notFound(); } $db = Database::get(); if (!$db instanceof PDO) { return $this->notFound(); } $stmt = $db->prepare(" SELECT title, content_html, published_at, featured_image_url, author_name, category, tags FROM ac_posts WHERE slug = :slug AND is_published = 1 LIMIT 1 "); $stmt->execute([':slug' => $slug]); $post = $stmt->fetch(PDO::FETCH_ASSOC); if (!$post) { return $this->notFound(); } return new Response($this->view->render('site/show.php', [ 'title' => (string)$post['title'], 'content_html' => (string)$post['content_html'], 'published_at' => (string)($post['published_at'] ?? ''), 'featured_image_url' => (string)($post['featured_image_url'] ?? ''), 'author_name' => (string)($post['author_name'] ?? ''), 'category' => (string)($post['category'] ?? ''), 'tags' => (string)($post['tags'] ?? ''), ])); } public function adminIndex(): Response { if ($guard = $this->guard(['admin', 'manager'])) { return $guard; } $db = Database::get(); $posts = []; if ($db instanceof PDO) { $stmt = $db->query("SELECT id, title, slug, author_name, is_published, published_at, updated_at FROM ac_posts ORDER BY updated_at DESC"); $posts = $stmt ? $stmt->fetchAll(PDO::FETCH_ASSOC) : []; } return new Response($this->view->render('admin/index.php', [ 'title' => 'Posts', 'posts' => $posts, ])); } public function adminEdit(): Response { if ($guard = $this->guard(['admin', 'manager'])) { return $guard; } $id = isset($_GET['id']) ? (int)$_GET['id'] : 0; $post = [ 'id' => 0, 'title' => '', 'slug' => '', 'excerpt' => '', 'featured_image_url' => '', 'author_name' => '', 'category' => '', 'tags' => '', 'content_html' => '', 'is_published' => 0, 'published_at' => '', ]; $db = Database::get(); if ($id > 0 && $db instanceof PDO) { $stmt = $db->prepare("SELECT * FROM ac_posts WHERE id = :id LIMIT 1"); $stmt->execute([':id' => $id]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if ($row) { $post = $row; } } return new Response($this->view->render('admin/edit.php', [ 'title' => $id > 0 ? 'Edit Post' : 'New Post', 'post' => $post, 'error' => '', ])); } public function adminSave(): Response { if ($guard = $this->guard(['admin', 'manager'])) { return $guard; } $db = Database::get(); if (!$db instanceof PDO) { return new Response('', 302, ['Location' => '/admin/posts']); } $id = (int)($_POST['id'] ?? 0); $title = trim((string)($_POST['title'] ?? '')); $slug = trim((string)($_POST['slug'] ?? '')); $excerpt = trim((string)($_POST['excerpt'] ?? '')); $featuredImage = trim((string)($_POST['featured_image_url'] ?? '')); $authorName = trim((string)($_POST['author_name'] ?? '')); $category = trim((string)($_POST['category'] ?? '')); $tags = trim((string)($_POST['tags'] ?? '')); $content = (string)($_POST['content_html'] ?? ''); $isPublished = isset($_POST['is_published']) ? 1 : 0; $publishedAt = trim((string)($_POST['published_at'] ?? '')); if ($title === '') { return $this->renderEditError($id, $title, $slug, $excerpt, $featuredImage, $authorName, $category, $tags, $content, $isPublished, $publishedAt, 'Title is required.'); } if ($slug === '') { $slug = $this->slugify($title); } else { $slug = $this->slugify($slug); } if ($publishedAt !== '') { try { $dt = new DateTime($publishedAt); $publishedAt = $dt->format('Y-m-d H:i:s'); } catch (Throwable $e) { $publishedAt = ''; } } try { if ($id > 0) { $chk = $db->prepare("SELECT id FROM ac_posts WHERE slug = :slug AND id != :id LIMIT 1"); $chk->execute([':slug' => $slug, ':id' => $id]); } else { $chk = $db->prepare("SELECT id FROM ac_posts WHERE slug = :slug LIMIT 1"); $chk->execute([':slug' => $slug]); } if ($chk->fetch()) { return $this->renderEditError($id, $title, $slug, $excerpt, $featuredImage, $authorName, $category, $tags, $content, $isPublished, $publishedAt, 'Slug already exists.'); } if ($id > 0) { $stmt = $db->prepare(" UPDATE ac_posts SET title = :title, slug = :slug, excerpt = :excerpt, featured_image_url = :featured_image_url, author_name = :author_name, category = :category, tags = :tags, content_html = :content, is_published = :published, published_at = :published_at WHERE id = :id "); $stmt->execute([ ':title' => $title, ':slug' => $slug, ':excerpt' => $excerpt !== '' ? $excerpt : null, ':featured_image_url' => $featuredImage !== '' ? $featuredImage : null, ':author_name' => $authorName !== '' ? $authorName : null, ':category' => $category !== '' ? $category : null, ':tags' => $tags !== '' ? $tags : null, ':content' => $content, ':published' => $isPublished, ':published_at' => $publishedAt !== '' ? $publishedAt : null, ':id' => $id, ]); } else { $stmt = $db->prepare(" INSERT INTO ac_posts (title, slug, excerpt, featured_image_url, author_name, category, tags, content_html, is_published, published_at) VALUES (:title, :slug, :excerpt, :featured_image_url, :author_name, :category, :tags, :content, :published, :published_at) "); $stmt->execute([ ':title' => $title, ':slug' => $slug, ':excerpt' => $excerpt !== '' ? $excerpt : null, ':featured_image_url' => $featuredImage !== '' ? $featuredImage : null, ':author_name' => $authorName !== '' ? $authorName : null, ':category' => $category !== '' ? $category : null, ':tags' => $tags !== '' ? $tags : null, ':content' => $content, ':published' => $isPublished, ':published_at' => $publishedAt !== '' ? $publishedAt : null, ]); } } catch (Throwable $e) { return $this->renderEditError($id, $title, $slug, $excerpt, $featuredImage, $authorName, $category, $tags, $content, $isPublished, $publishedAt, 'Unable to save post.'); } return new Response('', 302, ['Location' => '/admin/posts']); } public function adminDelete(): Response { if ($guard = $this->guard(['admin', 'manager'])) { return $guard; } $db = Database::get(); if (!$db instanceof PDO) { return new Response('', 302, ['Location' => '/admin/posts']); } $id = (int)($_POST['id'] ?? 0); if ($id > 0) { $stmt = $db->prepare("DELETE FROM ac_posts WHERE id = :id"); $stmt->execute([':id' => $id]); } return new Response('', 302, ['Location' => '/admin/posts']); } private function renderEditError( int $id, string $title, string $slug, string $excerpt, string $featuredImage, string $authorName, string $category, string $tags, string $content, int $isPublished, string $publishedAt, string $error ): Response { $post = [ 'id' => $id, 'title' => $title, 'slug' => $slug, 'excerpt' => $excerpt, 'featured_image_url' => $featuredImage, 'author_name' => $authorName, 'category' => $category, 'tags' => $tags, 'content_html' => $content, 'is_published' => $isPublished, 'published_at' => $publishedAt, ]; return new Response($this->view->render('admin/edit.php', [ 'title' => $id > 0 ? 'Edit Post' : 'New Post', 'post' => $post, 'error' => $error, ])); } private function notFound(): Response { $view = new View(); return new Response($view->render('site/404.php', [ 'title' => 'Not Found', 'message' => 'Post not found.', ]), 404); } private function slugify(string $value): string { $value = strtolower(trim($value)); $value = preg_replace('~[^a-z0-9]+~', '-', $value) ?? $value; $value = trim($value, '-'); return $value !== '' ? $value : 'post'; } private function guard(array $roles): ?Response { if (!Auth::check()) { return new Response('', 302, ['Location' => '/admin/login']); } if (!Auth::hasRole($roles)) { return new Response('', 302, ['Location' => '/admin']); } return null; } }