AI Implementation feature(869): Admin Area Blog and Blog Page (#58)

This commit was merged in pull request #58.
This commit is contained in:
2026-07-23 10:17:55 +00:00
parent d468cca766
commit 294873240b
32 changed files with 1798 additions and 117 deletions
+14 -12
View File
@@ -1,9 +1,9 @@
---
type: guide
title: Admin Shell & Side Navigation
description: How an authenticated admin navigates the post-login admin area, the side-nav layout, and how the General and Categories pages are reached.
description: How an authenticated admin navigates the post-login admin area, including General, Challenges, Players, Categories, and Blog management pages.
tags: [guide, admin, shell, navigation, tester]
timestamp: 2026-07-22T12:00:00Z
timestamp: 2026-07-23T10:12:24Z
---
# When this view is available
@@ -47,9 +47,9 @@ The side-nav is defined as a static `ENTRIES` array in
|-------------|-----------------------|---------|----------------------------------------------------------------------------------------------|
| General | `/admin/general` | yes | [Admin — General Settings](/guides/admin-general-settings.md) (default — `/admin` redirects here). |
| Challenges | `/admin/challenges` | yes | [Admin — Challenges](/guides/admin-challenges.md) — sortable list, search, Add/Edit modal, file staging, import/export. |
| Players | `/admin/players` | no | Greyed-out placeholder. No route registered. |
| Blog | `/admin/blog` | no | Greyed-out placeholder. No route registered. |
| System | `/admin/system` | no | Greyed-out placeholder. No route registered. |
| Players | `/admin/players` | yes | User management page. |
| Blog | `/admin/blog` | yes | [Blog Publishing and Reading](/guides/blog.md) — post list with draft/publish, edit, preview, and delete workflows. |
| System | `/admin/system` | no | Greyed-out placeholder. No route registered. |
Each `<li>` carries `data-testid="admin-nav-{id}"`. Disabled entries
have `cursor: not-allowed` and `opacity: 0.45`. Clicking a disabled
@@ -95,17 +95,18 @@ current URL starts with the entry's path. General is the default child
| 4 | `frontend/src/app/features/admin/admin-shell.component.ts` | Renders the aside + body; `ENTRIES` is the static side-nav list. |
| 5 | `frontend/src/app/features/admin/general.component.ts` | `AdminGeneralComponent` (default `/admin/general`); embeds `AdminCategoriesComponent`. |
| 6 | `frontend/src/app/features/admin/categories/categories.component.ts` | `AdminCategoriesComponent` (`/admin/categories`); also rendered inside the General page. |
| 7 | `frontend/src/app/features/home/home.shell.ts` | `shouldShowAdminNav({isAuthenticated, role})` predicate (exported and unit-tested). |
| 8 | `frontend/src/app/features/home/home.component.ts` | Shell template renders header, conditional nav (`*ngIf="showAdminNav()"`), and `<router-outlet>`. |
| 9 | `frontend/src/app/core/services/admin.service.ts` | Typed wrappers for `/api/v1/admin/users`, `/admin/general/*`, `/admin/categories/*`, `/uploads/{logo,category-icon}`. |
| 10 | `backend/src/modules/admin/admin.module.ts` | Registers `AdminController`, `AdminGeneralController`, `AdminCategoriesController` + their services. Imports `AuthModule`, `UsersModule`, `SettingsModule`, `CommonModule`, and `TypeOrmModule.forFeature([UserEntity, CategoryEntity, ChallengeEntity])`. |
| 7 | `frontend/src/app/features/admin/blog/blog.component.ts` | `AdminBlogComponent` (`/admin/blog`); coordinates post listing, editing, publication, and deletion. |
| 8 | `frontend/src/app/features/home/home.shell.ts` | `shouldShowAdminNav({isAuthenticated, role})` predicate (exported and unit-tested). |
| 9 | `frontend/src/app/features/home/home.component.ts` | Shell template renders header, conditional nav (`*ngIf="showAdminNav()"`), and `<router-outlet>`. |
| 10 | `frontend/src/app/core/services/blog.service.ts` | Typed wrappers for public blog listing and `/api/v1/admin/blog/posts` CRUD. |
| 11 | `backend/src/modules/blog/blog.module.ts` | Registers public and admin blog controllers/services with the blog repository. |
# Notes
* The shell deliberately keeps `AdminShellComponent` as a thin layout
owner — child routes (`general`, `categories`) render into its
`<router-outlet>`. New admin sub-pages can be added by enabling
entries in `ENTRIES` and registering a child route in `app.routes.ts`.
owner — enabled child routes render into its `<router-outlet>`. New admin
sub-pages require both an enabled `ENTRIES` item and a child route in
`app.routes.ts`.
* The guard decision function (`decideAdminGuard`) is a pure module so
it is straightforward to unit-test without Angular DI. See
`tests/frontend/admin-shell.spec.ts` and
@@ -124,5 +125,6 @@ current URL starts with the entry's path. General is the default child
- [Change Password](/guides/change-password.md)
- [Admin — General Settings](/guides/admin-general-settings.md)
- [Admin — Categories](/guides/admin-categories.md)
- [Blog Publishing and Reading](/guides/blog.md)
- [REST API Overview](/api/rest-overview.md)
- [Admin Endpoints](/api/admin.md)
+71
View File
@@ -0,0 +1,71 @@
---
type: guide
title: Blog Publishing and Reading
description: How administrators create, edit, publish, and delete Markdown posts and how users read published posts.
tags: [guide, blog, admin, markdown, tester]
timestamp: 2026-07-23T10:12:24Z
---
# User views
Published posts are available in two places:
| View | Access | Expected content |
|---|---|---|
| `/login` | Unauthenticated users | The landing-page blog section lists published posts below the Login button. |
| `/blog` | Signed-in users | The Blog page loads and lists all published posts. |
Both views use `BlogPresenterComponent` to show the post title, publication date, and sanitized Markdown body. Draft posts never appear in either public list. The `/blog` page shows `Loading blog posts…` while loading, `No announcements yet.` when no posts are published, and an error message if loading fails.
# Admin workflow
1. Sign in as an administrator.
2. Open **Admin**, then select **Blog**, or navigate directly to `/admin/blog`.
3. Confirm the page displays a table with **Title**, **Status**, **Created**, **Updated**, **Published**, and **Actions** columns. If no posts exist, it displays `No posts yet.`.
4. Click **+ New post**.
5. Enter a required title and an optional Markdown body. The live preview updates beside the editor.
6. Click **Save draft** to keep the post hidden from users, or **Publish** to expose it in `/blog` and on `/login`.
7. Confirm a success banner displays `Draft saved.` or `Post published.`, and the refreshed table contains the post with the correct status badge.
# Editing and deleting
* Click the pencil action on a row to open the prefilled **Edit post** modal. It shows created, updated, and publication timestamps where available.
* Saving as draft changes the status to **Draft** and hides the post from public lists. Republishing preserves the post's original publication timestamp.
* Click the trash action to open **Delete post**. The confirmation includes the title and warns that deletion cannot be undone.
* Click **Delete** to remove the post. The table refreshes and displays `Post deleted.`. Click **Cancel** or the modal backdrop to leave the post unchanged.
# Validation and expected errors
| Input or operation | Expected behavior |
|---|---|
| Empty or whitespace-only title | `Title is required`; save and publish remain unavailable. |
| Title over 200 characters | `Title must be at most 200 characters`. |
| Markdown body over 200,000 characters | `Body must be at most 200000 characters`. |
| API list failure | An error banner replaces the table state. |
| Create or update failure | The form stays open and displays the server message. |
| Delete failure | The confirmation stays open and displays the server message. |
# Architecture map
| Layer | File | Responsibility |
|---|---|---|
| Routes | `frontend/src/app/app.routes.ts` | Registers authenticated `/blog` and admin-only `/admin/blog`. |
| Admin page | `frontend/src/app/features/admin/blog/blog.component.ts` | Loads all posts and coordinates create, edit, publish, draft, and delete operations. |
| Admin form | `frontend/src/app/features/admin/blog/blog-form-modal.component.ts` | Reactive title/body editor with sanitized live Markdown preview. |
| Admin form logic | `frontend/src/app/features/admin/blog/blog-form.pure.ts` | Validation, form synchronization, request cleanup, and status labels. |
| Delete confirmation | `frontend/src/app/features/admin/blog/blog-delete-modal.component.ts` | Confirms destructive deletion and displays failures. |
| User page | `frontend/src/app/features/blog/blog.page.ts` | Loads published posts and selects loading, error, empty, or list state. |
| Shared presenter | `frontend/src/app/features/blog/blog-presenter.component.ts` | Renders a published post consistently on `/blog` and `/login`. |
| HTTP client | `frontend/src/app/core/services/blog.service.ts` | Calls public list and admin CRUD endpoints. |
| Backend wiring | `backend/src/modules/blog/blog.module.ts` | Registers public and admin controllers and services with the blog repository. |
| Admin API | `backend/src/modules/blog/admin-blog.controller.ts` | Registers guarded CRUD routes under `/api/v1/admin/blog/posts`. |
| Persistence logic | `backend/src/modules/blog/admin-blog.service.ts` | Queries and mutates `blog_post` rows and controls publication timestamps. |
The Angular client uses HTTP JSON calls. Existing auth and CSRF interceptors attach the administrator JWT and CSRF token to unsafe requests. The backend controller is protected by `AdminGuard` and `@Roles('admin')`, then delegates to `AdminBlogService`, which uses the TypeORM `BlogPostEntity` repository.
# See also
- [Blog API](/api/blog.md)
- [Blog Post Table](/database/blog-posts.md)
- [Landing Page](/guides/landing-page.md)
- [Admin Shell & Side Navigation](/guides/admin-shell.md)
+4 -1
View File
@@ -3,7 +3,7 @@ type: guide
title: Landing Page
description: How the public landing page renders, how the login + registration modal is opened, what each form does, and how the modal stays in sync with admin `registrationsEnabled` changes via SSE.
tags: [guide, landing, login, register, ui, sse, bootstrap]
timestamp: 2026-07-22T16:15:00Z
timestamp: 2026-07-23T10:12:24Z
---
# What you see
@@ -31,6 +31,8 @@ On a fresh visit the page shows, in order:
5. **Blog list**`<section data-testid="landing-blog">` listing every
row returned by `GET /api/v1/blog/posts` (only `status='published'`
rows), or the text `No announcements yet.` when the list is empty.
Each row is rendered by the shared `BlogPresenterComponent` with its
title, publication date, and sanitized Markdown body, matching `/blog`.
# Guard
@@ -144,4 +146,5 @@ response to the unsafe call itself.
- [Auth Endpoints](/api/auth.md)
- [Database Schema Overview](/database/schema.md) (`blog_post`)
- [Frontend Structure](/architecture/frontend-structure.md)
- [Blog Publishing and Reading](/guides/blog.md)
- [First-Run Bootstrap](/guides/bootstrap.md)