Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions CMS_UX_ROADMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# CMS Experience & Capabilities Blueprint

This master document outlines the strategic UX/UI and structural enhancements targeted for the AxeCode CMS. The goal is to upgrade the current modular grid interface into a highly robust, enterprise-grade Content Management System.

---

## 🏗️ 1. Scalability & Performance Engine (Server-Side Overhaul)
Currently, data grids fetch entire datasets and compute pagination or search filtering on the client side (in browser). This architecture fails at scale.

### Future Implementation:
* **Server-Side Pagination:** Refactor API repositories to integrate natively with Strapi's standard pagination parameters (`?pagination[page]=1&pagination[pageSize]=10`).
* **Query Builder Mapping:** Transform frontend filter criteria into Strapi Query strings within the Domain layer before executing API calls.
* **Component Shift:** `CMSResourceTable.jsx` must shift from slicing a complete array (e.g., `items.slice()`) to passing `onPageChange` commands up to the useCase hooks, which then fetch subsequent chunks.
* **Debounced Instant Search:** Ensure that typing in the search bar uses lodash `debounce` to fetch data from the server without exhausting the API rate limit.

## 📊 2. "Bird's-Eye" Analytics Dashboard
Currently, logging into the CMS throws the Admin directly into a table. A modern CMS requires a primary overview context.

### Future Implementation:
* **Entry Dashboard Component:** Introduce `CMSDashboard.jsx` as the default route (`/cms`).
* **Real-time Metrics:**
* **Quick Counters:** Display aggregated metrics for total published Courses, active Events, pending Reports, and active accounts.
* **Submission Trend Graphs:** Use lightweight chart libraries (e.g., `recharts`) to plot system engagement over the past 30 days.
* **Pending Actions:** Highlighting critical items requiring immediate administrative action (e.g. Unresolved User Reports).

## 🚀 3. Batch Productivity & Bulk Workflows
Management involves repetitive and bulky tasks. The multi-select implementation currently only supports batch deletion.

### Future Implementation:
* **Bulk Status Mutators:** Allow selecting 10 entries and publishing or rolling them back to "Draft" via a single click in the action bar.
* **Bulk Categorization:** Add the ability to assign massive arrays of Courses to specific Tag groups or Course Types all at once.
* **Smart Header Sorting:** Headers (Date, Engagement Score, Status) must become clickable. Clicking issues a server-side sort request (`?sort=publishedAt:desc`) returning the appropriately sorted list.
* **Pre-Set Fast Filters:** Buttons acting as pre-baked queries above the table (e.g., "Active Modules", "Legacy Content", "Hidden").

## 🎨 4. Media Library Visual Recognition (Visual UX)
Rows representing images with a "Storage" metric are difficult to interact with. Media manipulation requires graphical indexing.

### Future Implementation:
* **Grid Layout Paradigm:** Introduce a dual-mode component (`Table Mode` / `Grid Mode`), keeping `Grid Mode` default for the Media page.
* **Lazy-loaded Thumbnail UI:** Directly fetch image thumbnails generated by Strapi (Small/Thumbnail formats) from the bucket to ensure snappy rendering of visuals.
* **Asset Preview Context:** Introduce a side-panel upon clicking a media item that displays file resolution, alt tags, and a unified preview window.

## 🔒 5. Role-Based Access Control (RBAC) & Governance UI
The CMS must not present unauthorized actions to non-privileged users.

### Future Implementation:
* **Dynamic Sidebar Extraction:** The sidebar (`CMSSidebar.jsx`) needs to read the incoming `userSession.role`. If the user is a `Support Agent`, they should only see "Help Centers" and "Reports".
* **Safe-State UI Overrides:** If a user navigates to an endpoint they don't have access to, the dashboard should lock the screen and provide an elegant 403 "Restricted Clearance" manuscript error screen.
* **Action Logs/Audit Trails:** A logging table specifically for Super Admins showing which User Account modified what article and at what time.

---

**Strategic Priority for Next Execution Phase:** Start systematically addressing `CMSResourceTable` by implementing Server-Side scaling, which protects the application from catastrophic memory overflows, then advance sequentially to the Master Dashboard.
47 changes: 0 additions & 47 deletions scratch/verify_mapper.test.js

This file was deleted.

10 changes: 10 additions & 0 deletions src/domain/entity/UserEntity.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export class UserEntity extends BaseEntity {
* @param {MediaEntity|null} props.avatar
* @param {number} props.submissionCount
* @param {number} props.passedSubmissionsCount
* @param {boolean} props.confirmed
* @param {boolean} props.blocked
* @param {object} props.role
* @param {string} props.createdAt
*/
constructor(props = {}) {
super(props);
Expand All @@ -28,6 +32,12 @@ export class UserEntity extends BaseEntity {
this.bio = props.bio;
this.avatar = props.avatar;

// Admin fields
this.confirmed = props.confirmed;
this.blocked = props.blocked;
this.role = props.role;
this.createdAt = props.createdAt;

// Direct statistics (fetched from table, not populated)
this.submissionCount = props.submissionCount || 0;
this.passedSubmissionsCount = props.passedSubmissionsCount || 0;
Expand Down
4 changes: 4 additions & 0 deletions src/domain/mapper/EntityMapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ export class EntityMapper {
university: data.university,
bio: data.bio,
avatar: this.toMedia(data.avatar),
confirmed: data.confirmed,
blocked: data.blocked,
role: data.role,
createdAt: data.createdAt,
submissionCount: stats.total || 0,
passedSubmissionsCount: stats.passed || 0
});
Expand Down
21 changes: 21 additions & 0 deletions src/domain/useCase/useDeleteArticle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useAsyncUseCase } from './useAsyncUseCase';
import { ArticleRepository } from '../../infrastructure/repository/ArticleRepository';
import { useMemo } from 'react';

/**
* UseCase for deleting an article.
*/
export const useDeleteArticle = () => {
const repository = useMemo(() => new ArticleRepository(), []);

const { execute, inProgress, error, returnedData } = useAsyncUseCase(
(id) => repository.delete(id)
);

return {
deleteArticle: execute,
inProgress,
error,
success: !!returnedData
};
};
21 changes: 21 additions & 0 deletions src/domain/useCase/useDeleteBlog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useAsyncUseCase } from './useAsyncUseCase';
import { BlogRepository } from '../../infrastructure/repository/BlogRepository';
import { useMemo } from 'react';

/**
* UseCase for deleting a blog post.
*/
export const useDeleteBlog = () => {
const repository = useMemo(() => new BlogRepository(), []);

const { execute, inProgress, error, returnedData } = useAsyncUseCase(
(id) => repository.delete(id)
);

return {
deleteBlog: execute,
inProgress,
error,
success: !!returnedData
};
};
21 changes: 21 additions & 0 deletions src/domain/useCase/useDeleteCourse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useAsyncUseCase } from './useAsyncUseCase';
import { CourseRepository } from '../../infrastructure/repository/CourseRepository';
import { useMemo } from 'react';

/**
* UseCase for deleting a course.
*/
export const useDeleteCourse = () => {
const repository = useMemo(() => new CourseRepository(), []);

const { execute, inProgress, error, returnedData } = useAsyncUseCase(
(id) => repository.delete(id)
);

return {
deleteCourse: execute,
inProgress,
error,
success: !!returnedData
};
};
78 changes: 78 additions & 0 deletions src/domain/useCase/useFetchAdminCategorizations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { useAsyncUseCase } from './useAsyncUseCase';
import { CategorizationRepository } from '../../infrastructure/repository/CategorizationRepository';
import { useMemo, useEffect, useCallback, useState } from 'react';

/**
* Hook for managing Course Types and Problem Types in Admin CMS.
* Each type maintains its own independent pagination state.
*/
export const useFetchAdminCategorizations = () => {
const repository = useMemo(() => new CategorizationRepository(), []);
const pageSize = 10;

// Course Types pagination state
const [courseTypePage, setCourseTypePage] = useState(1);
const [courseTypeSearch, setCourseTypeSearch] = useState('');

// Problem Types pagination state
const [problemTypePage, setProblemTypePage] = useState(1);
const [problemTypeSearch, setProblemTypeSearch] = useState('');

// Course Types
const fetchCourseTypes = useCallback(async () => {
return await repository.getCourseTypes(courseTypePage, pageSize, courseTypeSearch);
}, [repository, courseTypePage, courseTypeSearch]);

const { execute: loadCourseTypes, returnedData: courseTypesData, inProgress: isLoadingCourses, error: courseError } = useAsyncUseCase(fetchCourseTypes);

// Problem Types
const fetchProblemTypes = useCallback(async () => {
return await repository.getProblemTypes(problemTypePage, pageSize, problemTypeSearch);
}, [repository, problemTypePage, problemTypeSearch]);

const { execute: loadProblemTypes, returnedData: problemTypesData, inProgress: isLoadingProblems, error: problemError } = useAsyncUseCase(fetchProblemTypes);

useEffect(() => {
loadCourseTypes();
}, [loadCourseTypes]);

useEffect(() => {
loadProblemTypes();
}, [loadProblemTypes]);

const deleteCourseType = useCallback(async (id) => {
await repository.deleteCourseType(id);
await loadCourseTypes();
}, [repository, loadCourseTypes]);

const deleteProblemType = useCallback(async (id) => {
await repository.deleteProblemType(id);
await loadProblemTypes();
}, [repository, loadProblemTypes]);

return {
// Course Types
courseTypes: courseTypesData?.items || [],
courseTypesTotalItems: courseTypesData?.meta?.pagination?.total || 0,
courseTypesTotalPages: Math.max(1, Math.ceil((courseTypesData?.meta?.pagination?.total || 0) / pageSize)),
courseTypesPage: courseTypePage,
setCourseTypePage,
setCourseTypeSearch,
reloadCourseTypes: loadCourseTypes,
deleteCourseType,

// Problem Types
problemTypes: problemTypesData?.items || [],
problemTypesTotalItems: problemTypesData?.meta?.pagination?.total || 0,
problemTypesTotalPages: Math.max(1, Math.ceil((problemTypesData?.meta?.pagination?.total || 0) / pageSize)),
problemTypesPage: problemTypePage,
setProblemTypePage,
setProblemTypeSearch,
reloadProblemTypes: loadProblemTypes,
deleteProblemType,

// Shared
isLoading: isLoadingCourses || isLoadingProblems,
error: courseError || problemError
};
};
23 changes: 16 additions & 7 deletions src/domain/useCase/useFetchAdminCourses.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useAsyncUseCase } from './useAsyncUseCase';
import { CourseRepository } from '../../infrastructure/repository/CourseRepository';
import { useMemo, useEffect } from 'react';
import { useMemo, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';

/**
Expand All @@ -10,20 +10,29 @@ export const useFetchAdminCourses = () => {
const userId = useSelector((state) => state.auth?.user?.id);
const repository = useMemo(() => new CourseRepository(), []);

// Pass userId up to the filter
const { execute, returnedData: courses, inProgress: isLoading, error } = useAsyncUseCase(
() => repository.getAll(userId)
const [page, setPage] = useState(1);
const [search, setSearch] = useState('');
const pageSize = 10; // Match what's configured in CMSResourceTable logic

// Pass page, pageSize, search to repository
const { execute, returnedData: data, inProgress: isLoading, error } = useAsyncUseCase(
() => repository.getAll(userId, page, pageSize, search)
);

useEffect(() => {
if (userId) {
execute();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId]);
}, [userId, page, search]);

return {
courses: courses || [],
courses: data?.items || [],
totalItems: data?.meta?.pagination?.total || 0,
totalPages: Math.max(1, Math.ceil((data?.meta?.pagination?.total || 0) / pageSize)),
currentPage: page,
setPage,
searchQuery: search,
setSearch,
isLoading,
error,
fetch: execute
Expand Down
20 changes: 14 additions & 6 deletions src/domain/useCase/useFetchAdminEvents.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { useAsyncUseCase } from './useAsyncUseCase';
import { EventRepository } from '../../infrastructure/repository/EventRepository';
import { useMemo, useEffect, useCallback } from 'react';
import { useMemo, useEffect, useCallback, useState } from 'react';

/**
* UseCase hook for fetching all events (Admin/CMS view).
* Supports server-side pagination & search.
*/
export const useFetchAdminEvents = () => {
const repository = useMemo(() => new EventRepository(), []);
const [page, setPage] = useState(1);
const [search, setSearch] = useState('');
const pageSize = 10;

const fetchEvents = useCallback(async () => {
const response = await repository.apiClient.get(repository.endpointBase);
return response.data || [];
}, [repository]);
return await repository.getAll(page, pageSize, search);
}, [repository, page, search]);

const { execute, returnedData, inProgress, error } = useAsyncUseCase(fetchEvents);

Expand All @@ -20,9 +23,14 @@ export const useFetchAdminEvents = () => {
}, [execute]);

return {
events: returnedData || [],
events: returnedData?.items || [],
totalItems: returnedData?.meta?.pagination?.total || 0,
totalPages: Math.max(1, Math.ceil((returnedData?.meta?.pagination?.total || 0) / pageSize)),
currentPage: page,
setPage,
setSearch,
isLoading: inProgress,
error,
fetch: fetchEvents
fetch: execute
};
};
Loading
Loading