diff --git a/src/API/fetchWrapper.js b/src/API/fetchWrapper.js index 0777560..c92c188 100644 --- a/src/API/fetchWrapper.js +++ b/src/API/fetchWrapper.js @@ -21,19 +21,14 @@ export const fetchWrapper = async (url, useToken = true, contentType = 'applicat credentials: useToken ? 'include' : 'same-origin', }; - console.log(config); - console.log(body); - console.log(contentType); - console.log(method); - console.log(url); - console.log(useToken); + if (body && method !== 'GET') { config.body = contentType === 'application/json' ? JSON.stringify(body) : body; } try { const response = await fetch(url, config); - + if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.message || `Request failed with status ${response.status}`); diff --git a/src/domain/entity/ContentEntity.js b/src/domain/entity/ContentEntity.js index 1708d8d..36aa2bf 100644 --- a/src/domain/entity/ContentEntity.js +++ b/src/domain/entity/ContentEntity.js @@ -21,6 +21,7 @@ export class ContentEntity extends BaseEntity { this.likesCount = props.likesCount || 0; this.commentsCount = props.commentsCount || 0; this.isLiked = !!props.isLiked; + this.isDraft = !!props.isDraft; } /** diff --git a/src/domain/entity/CourseEntity.js b/src/domain/entity/CourseEntity.js index 59611a7..be26d29 100644 --- a/src/domain/entity/CourseEntity.js +++ b/src/domain/entity/CourseEntity.js @@ -116,6 +116,7 @@ export class CoursePreviewEntity extends ContentEntity { this.duration = props.duration || 0; this.completedLessonsCount = props.completedLessonsCount || 0; this.lessonCount = props.lessonCount || 0; + this.course_types = props.course_types || []; } /** diff --git a/src/domain/entity/EventEntity.js b/src/domain/entity/EventEntity.js index 075d314..f35434f 100644 --- a/src/domain/entity/EventEntity.js +++ b/src/domain/entity/EventEntity.js @@ -35,6 +35,7 @@ export class EventEntity extends ContentEntity { this.speakers = props.speakers || []; this.activities = props.activities || []; this.organizer = props.organizer; + this.scanners = props.scanners || []; this.images = props.images || []; // all gallery images // Interactions diff --git a/src/domain/mapper/EntityMapper.js b/src/domain/mapper/EntityMapper.js index 0a74b92..c09eda7 100644 --- a/src/domain/mapper/EntityMapper.js +++ b/src/domain/mapper/EntityMapper.js @@ -62,7 +62,7 @@ export class EntityMapper { if (!data) return null; console.log('[ProfileSync] Mapping toUser - Data:', data); console.log('[ProfileSync] Mapping toUser - Stats Prop:', stats); - + return new UserEntity({ id: data.id, uid: data.documentId, @@ -103,7 +103,8 @@ export class EntityMapper { caption: dto.caption, author: this.toUser(dto.author), media: Array.from(dto.media?.values() || []).map(m => this.toMedia(m)), - article: dto.article ? this.toArticle(dto.article) : null + article: dto.article ? this.toArticle(dto.article) : null, + isDraft: !!dto.isDraft }); } @@ -129,7 +130,8 @@ export class EntityMapper { video: this.toMedia(dto.video), description: dto.description, isPublic: dto.public, - instructor: this.toUser(dto.instructor) + instructor: this.toUser(dto.instructor), + isDraft: !!dto.isDraft }); } @@ -163,7 +165,8 @@ export class EntityMapper { reviewsCount: dto.reviewsCount || 0, duration: dto.duration || 0, completedLessonsCount: dto.completedLessonsCount, - lessonCount: dto.lessonCount + lessonCount: dto.lessonCount, + isDraft: !!dto.isDraft }); } @@ -212,7 +215,17 @@ export class EntityMapper { description: a.description, time: a.time })), - organizer: dto.organizer ? this.toUser(dto.organizer) : null + organizer: dto.organizer ? this.toUser(dto.organizer) : null, + scanners: Array.from(dto.scanners?.values() || []).map(s => ({ + id: s.id, + documentId: s.documentId, + user: s.users_permissions_user ? { + id: s.users_permissions_user.id, + username: s.users_permissions_user.username, + email: s.users_permissions_user.email, + } : null + })), + isDraft: !!dto.isDraft }); } @@ -258,6 +271,7 @@ export class EntityMapper { title: week.title, lessons: Array.from(week.lessons?.values() || []).map(lesson => this.toLesson(lesson)) })); + console.log(dto, "dto") return new CoursePreviewEntity({ id: dto.id, @@ -285,7 +299,9 @@ export class EntityMapper { reviewsCount: dto.reviewsCount || 0, duration: dto.duration || 0, completedLessonsCount: dto.completedLessonsCount, - lessonCount: dto.lessonCount + lessonCount: dto.lessonCount, + isDraft: dto.isDraft, + course_types: Array.from(dto.course_types?.values() || []) }); } @@ -338,7 +354,7 @@ export class EntityMapper { memoryLimit: dto.memoryLimit, submissionStatus: dto.submissionStatus || 'New', - // Nested relations + problemTypes: Array.from(dto.problem_types?.values?.() || []), testCases: Array.from(dto.test_cases?.values?.() || []).map(tc => ({ id: tc.id, documentId: tc.documentId, @@ -365,7 +381,8 @@ export class EntityMapper { commentsCount: dto.interactions?.commentsCount || 0, availableLanguages: dto.availableLanguages, - points: dto.points + points: dto.points, + isDraft: !!dto.isDraft }); } @@ -410,8 +427,8 @@ export class EntityMapper { description: dto.description, image: dto.image ? this.toMedia(dto.image) : null, author: dto.publisher ? this.toUser(dto.publisher) : null, + isDraft: !!dto.isDraft }); - return entity; } /** @@ -438,6 +455,7 @@ export class EntityMapper { author: dto.author ? { username: dto.author.username, avatar: dto.author.avatar ? this.toMedia(dto.author.avatar) : null } : null, + isDraft: !!dto.isDraft }); } @@ -480,7 +498,8 @@ export class EntityMapper { flowData: dto.flowData, color: dto.color, icon: dto.icon, - author: this.toUser(dto.author) + author: this.toUser(dto.author), + isDraft: !!dto.isDraft }); } diff --git a/src/domain/useCase/useCreateBlog.js b/src/domain/useCase/useCreateBlog.js index 96a6245..6f22a48 100644 --- a/src/domain/useCase/useCreateBlog.js +++ b/src/domain/useCase/useCreateBlog.js @@ -11,7 +11,7 @@ export const useCreateBlog = () => { const [error, setError] = useState(null); const [uploadProgress, setUploadProgress] = useState(0); - const createBlog = async ({ description, imageFile, tags }) => { + const createBlog = async ({ description, imageFile, tags, isDraft }) => { setLoading(true); setError(null); setUploadProgress(0); @@ -38,7 +38,8 @@ export const useCreateBlog = () => { const blogData = { description, imageId, // Maps to `image` in BlogRequest - tagIds: tags // Maps to `tags` in BlogRequest + tagIds: tags, // Maps to `tags` in BlogRequest + isDraft }; const response = await blogRepo.create(blogData); diff --git a/src/domain/useCase/useCreateCategorization.js b/src/domain/useCase/useCreateCategorization.js new file mode 100644 index 0000000..d0b9d1f --- /dev/null +++ b/src/domain/useCase/useCreateCategorization.js @@ -0,0 +1,28 @@ +import { useAsyncUseCase } from './useAsyncUseCase'; +import { CategorizationRepository } from '../../infrastructure/repository/CategorizationRepository'; +import { useMemo, useCallback } from 'react'; + +/** + * Use case hook for creating categorization tracks (Course or Problem types). + */ +export const useCreateCategorization = () => { + const repository = useMemo(() => new CategorizationRepository(), []); + + const createCourseTypeLogic = useCallback(async (data) => { + return await repository.createCourseType(data); + }, [repository]); + + const createProblemTypeLogic = useCallback(async (data) => { + return await repository.createProblemType(data); + }, [repository]); + + const { execute: createCourseType, inProgress: isCreatingCourse, error: courseError } = useAsyncUseCase(createCourseTypeLogic); + const { execute: createProblemType, inProgress: isCreatingProblem, error: problemError } = useAsyncUseCase(createProblemTypeLogic); + + return { + createCourseType, + createProblemType, + inProgress: isCreatingCourse || isCreatingProblem, + error: courseError || problemError + }; +}; diff --git a/src/domain/useCase/useCreateEventActivity.js b/src/domain/useCase/useCreateEventActivity.js new file mode 100644 index 0000000..7d364e5 --- /dev/null +++ b/src/domain/useCase/useCreateEventActivity.js @@ -0,0 +1,25 @@ +import { useState, useMemo } from 'react'; +import { EventRepository } from '@infrastructure/repository/EventRepository'; +import { toast } from 'react-hot-toast'; + +export const useCreateEventActivity = () => { + const [inProgress, setInProgress] = useState(false); + const eventRepo = useMemo(() => new EventRepository(), []); + + const createEventActivity = async (dtoData) => { + setInProgress(true); + try { + const response = await eventRepo.createActivity(dtoData); + return response; + } catch (error) { + console.error('[useCreateEventActivity] Error:', error); + const errorMessage = error.response?.data?.error?.message || error.message || "Failed to create activity"; + toast.error(`Activity Creation Failed: ${errorMessage}`); + throw error; + } finally { + setInProgress(false); + } + }; + + return { createEventActivity, inProgress }; +}; diff --git a/src/domain/useCase/useCreateEventScanner.js b/src/domain/useCase/useCreateEventScanner.js new file mode 100644 index 0000000..e138024 --- /dev/null +++ b/src/domain/useCase/useCreateEventScanner.js @@ -0,0 +1,25 @@ +import { useState, useMemo } from 'react'; +import { EventRepository } from '@infrastructure/repository/EventRepository'; +import { toast } from 'react-hot-toast'; + +export const useCreateEventScanner = () => { + const [inProgress, setInProgress] = useState(false); + const eventRepo = useMemo(() => new EventRepository(), []); + + const createEventScanner = async (dtoData) => { + setInProgress(true); + try { + const response = await eventRepo.createScanner(dtoData); + return response; + } catch (error) { + console.error('[useCreateEventScanner] Error:', error); + const errorMessage = error.response?.data?.error?.message || error.message || "Failed to authorize scanner"; + toast.error(`Auth Error: ${errorMessage}`); + throw error; + } finally { + setInProgress(false); + } + }; + + return { createEventScanner, inProgress }; +}; diff --git a/src/domain/useCase/useCreateEventSpeaker.js b/src/domain/useCase/useCreateEventSpeaker.js new file mode 100644 index 0000000..6787c47 --- /dev/null +++ b/src/domain/useCase/useCreateEventSpeaker.js @@ -0,0 +1,25 @@ +import { useState, useMemo } from 'react'; +import { EventRepository } from '@infrastructure/repository/EventRepository'; +import { toast } from 'react-hot-toast'; + +export const useCreateEventSpeaker = () => { + const [inProgress, setInProgress] = useState(false); + const eventRepo = useMemo(() => new EventRepository(), []); + + const createEventSpeaker = async (dtoData) => { + setInProgress(true); + try { + const response = await eventRepo.createSpeaker(dtoData); + return response; + } catch (error) { + console.error('[useCreateEventSpeaker] Error:', error); + const errorMessage = error.response?.data?.error?.message || error.message || "Failed to organize speaker"; + toast.error(`Speaker Orchestration Failed: ${errorMessage}`); + throw error; + } finally { + setInProgress(false); + } + }; + + return { createEventSpeaker, inProgress }; +}; diff --git a/src/domain/useCase/useFetchCoursePreview.js b/src/domain/useCase/useFetchCoursePreview.js index 740bff8..485c086 100644 --- a/src/domain/useCase/useFetchCoursePreview.js +++ b/src/domain/useCase/useFetchCoursePreview.js @@ -19,9 +19,11 @@ export const useFetchCoursePreview = () => { // Strapi v4/v5 deep populate for weeks and their lessons // Use explicit populate settings to avoid 'related' key validation errors in some Strapi versions - const populateQuery = 'populate[picture]=true&populate[weeks][populate][lessons][populate][video]=true'; + const populateQuery = 'populate[picture]=true&populate[weeks][populate][lessons][populate][video]=true&populate[course_types]=true'; const rawData = await repository.getPreview(`${documentId}?${populateQuery}`); const dto = new CourseDTO(rawData); + // console.log(rawData, "raw data") + console.log("EntityMapper.toCoursePreview(dto)", EntityMapper.toCoursePreview(dto)) return EntityMapper.toCoursePreview(dto); }, []); diff --git a/src/domain/useCase/useFetchEvent.js b/src/domain/useCase/useFetchEvent.js index 982cbc6..f603888 100644 --- a/src/domain/useCase/useFetchEvent.js +++ b/src/domain/useCase/useFetchEvent.js @@ -15,7 +15,7 @@ export const useFetchEvent = () => { const fetchLogic = useCallback(async (id) => { if (!id) throw new Error("Event ID is required"); - console.log("id",id); + console.log("id", id); const rawData = await repository.getById(id); diff --git a/src/index.css b/src/index.css index 1fd26e9..eeda061 100644 --- a/src/index.css +++ b/src/index.css @@ -45,8 +45,8 @@ /* ── Radius & Shadows ── */ --radius-bento: 12px; - --shadow-halo: 0 0 0 1px var(--border-subtle), 0 4px 24px -2px rgba(0,0,0,0.05); - --shadow-whisper: rgba(0,0,0,0.05) 0 4px 24px; + --shadow-halo: 0 0 0 1px var(--border-subtle), 0 2px 12px -1px rgba(0,0,0,0.03); + --shadow-whisper: rgba(0,0,0,0.03) 0 2px 12px; --shadow-ring: 0 0 0 1px var(--ring-warm); } @@ -220,6 +220,15 @@ .shimmer { @apply relative overflow-hidden before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_2s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/20 before:to-transparent; } + + .animation-fade-in { + animation: fadeIn 0.4s ease-out forwards; + } +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } } @keyframes shimmer { diff --git a/src/infrastructure/DTO/BaseContentDTO.js b/src/infrastructure/DTO/BaseContentDTO.js index b67351e..7258ea7 100644 --- a/src/infrastructure/DTO/BaseContentDTO.js +++ b/src/infrastructure/DTO/BaseContentDTO.js @@ -19,7 +19,7 @@ export class BaseContentDTO { this.commentsCount = typeof data.commentsCount === 'number' ? data.commentsCount : (interactions.commentsCount || 0); this.isLiked = !!(data.isLikedByMe || interactions.isLikedByMe); - // Custom Draft State - this.isDraft = data.isDraft ?? true; // Default to true if not provided + // Custom Draft State (Standardized across CMS) + this.isDraft = data.isDraft !== undefined ? !!data.isDraft : true; } } diff --git a/src/infrastructure/DTO/CourseDTO.js b/src/infrastructure/DTO/CourseDTO.js index 1d52c8a..9815c93 100644 --- a/src/infrastructure/DTO/CourseDTO.js +++ b/src/infrastructure/DTO/CourseDTO.js @@ -24,7 +24,7 @@ export class CourseDTO extends BaseContentDTO { this.duration = data.duration || 0; // {number} - total minutes this.rating = data.interactions?.rating?.average || 0; // {number} this.reviewsCount = data.interactions?.rating?.count || 0; // {number} - + this.isDraft = data.isDraft || false; // {boolean} // Detailed Entitlement this.entitlement = data.entitlement ? new EntitlementDTO(data.entitlement) : null; // {EntitlementDTO | null} diff --git a/src/infrastructure/DTO/Request/ArticleRequest.js b/src/infrastructure/DTO/Request/ArticleRequest.js index fc544e3..0c0ee7e 100644 --- a/src/infrastructure/DTO/Request/ArticleRequest.js +++ b/src/infrastructure/DTO/Request/ArticleRequest.js @@ -16,9 +16,9 @@ export class ArticleRequest extends BaseRequest { return { data: { title: this.title, - content: this.contentBlocks, + contentBlocks: this.contentBlocks, tags: this.tags, - isDraft: this.isDraft + isDraft: !!this.isDraft } }; } diff --git a/src/infrastructure/DTO/Request/BaseRequest.js b/src/infrastructure/DTO/Request/BaseRequest.js index ac26b1a..224062f 100644 --- a/src/infrastructure/DTO/Request/BaseRequest.js +++ b/src/infrastructure/DTO/Request/BaseRequest.js @@ -20,6 +20,16 @@ export class BaseRequest { return payload; } + /** + * Standard Strapi payload wrapper. + * @returns {object} + */ + toPayload() { + return { + data: this.toJSON() + }; + } + /** * Abstract validation method. * Includes security audit for all fields. diff --git a/src/infrastructure/DTO/Request/BlogRequest.js b/src/infrastructure/DTO/Request/BlogRequest.js index fe4cddb..134e52c 100644 --- a/src/infrastructure/DTO/Request/BlogRequest.js +++ b/src/infrastructure/DTO/Request/BlogRequest.js @@ -15,10 +15,10 @@ export class BlogRequest extends BaseRequest { toPayload() { return { data: { - discription: this.description, // schema uses discription typo + description: this.description, image: this.image, tags: this.tags, - isDraft: this.isDraft + isDraft: !!this.isDraft } }; } diff --git a/src/infrastructure/DTO/Request/CourseRequest.js b/src/infrastructure/DTO/Request/CourseRequest.js index 2edc7ad..0e3f8b7 100644 --- a/src/infrastructure/DTO/Request/CourseRequest.js +++ b/src/infrastructure/DTO/Request/CourseRequest.js @@ -11,7 +11,7 @@ export class CourseRequest extends BaseRequest { this.difficulty = formData.difficulty; // {string} this.picture = formData.picture; // {number} Media ID this.tags = formData.tags || []; // {Array} - this.price = formData.price; // {number} + // this.price = formData.price; // {number} // Relationship IDs this.course_types = formData.courseTypeIds || []; // {Array} @@ -33,15 +33,27 @@ export class CourseRequest extends BaseRequest { * Converts to Strapi-compatible payload. */ toPayload() { + // Defensive: ensure tags is an array of clean strings. + // Prevents corruption if objects or "[object Object]" fragments leak through. + const cleanTags = (Array.isArray(this.tags) ? this.tags : []) + .map(t => { + if (typeof t === 'string') return t; + if (!t) return ""; + return t.name || t.label || String(t); + }) + .filter(t => t && t !== "[object Object]"); + return { data: { title: this.title, description: this.description, difficulty: this.difficulty, picture: this.picture || null, - tags: this.tags, + tags: cleanTags, price: this.price, - isDraft: this.isDraft + isDraft: !!this.isDraft, // Force boolean + course_types: Array.isArray(this.course_types) ? this.course_types : [], + problem_types: Array.isArray(this.problem_types) ? this.problem_types : [] } }; } diff --git a/src/infrastructure/DTO/Request/EventActivityRequest.js b/src/infrastructure/DTO/Request/EventActivityRequest.js new file mode 100644 index 0000000..8c81e31 --- /dev/null +++ b/src/infrastructure/DTO/Request/EventActivityRequest.js @@ -0,0 +1,29 @@ +/** + * DTO for Event Activity + */ +export class EventActivityRequest { + constructor({ title, from, description, eventId }) { + this.data = { + title, + from: this.formatTimeForStrapi(from), + description: description || '', + event: eventId + }; + } + + /** + * Converts an HH:mm string format or any raw time to Strapi's expected time format + */ + formatTimeForStrapi(timeValue) { + if (!timeValue) return null; + // Strapi time is best sent as HH:mm:ss.SSS + if (timeValue.length === 5) { + return `${timeValue}:00.000`; + } + return timeValue; + } + + toJSON() { + return { data: this.data }; + } +} diff --git a/src/infrastructure/DTO/Request/EventRequest.js b/src/infrastructure/DTO/Request/EventRequest.js index 869f138..aee47ea 100644 --- a/src/infrastructure/DTO/Request/EventRequest.js +++ b/src/infrastructure/DTO/Request/EventRequest.js @@ -7,14 +7,14 @@ import { SecurityUtils } from '../../../core/utils/SecurityUtils'; */ export class EventRequest extends BaseRequest { constructor(eventData = {}, entitlementData = null) { + console.log(eventData, "eventData") super(); - this.event = { + this.data = { title: eventData.title, discription: eventData.description, // Backend typo 'discription' preserved location: eventData.location, date: eventData.date, onsite: !!eventData.onsite, - live_streaming: !!eventData.live_streaming, duration: eventData.duration, speakers: eventData.speakerIds || [], event_activities: eventData.activityIds || [], @@ -41,14 +41,14 @@ export class EventRequest extends BaseRequest { */ toPayload() { return { - event: SecurityUtils.sanitizeData(this.event), + data: SecurityUtils.sanitizeData(this.data), entitlement: SecurityUtils.sanitizeData(this.entitlement) }; } validate() { super.validate(); - if (!this.event.title) throw new Error("Event title is required."); + if (!this.data.title) throw new Error("Event title is required."); if (this.entitlement && (!this.entitlement.price && this.entitlement.price !== 0)) { throw new Error("Entitlement price is required if entitlement is provided."); } diff --git a/src/infrastructure/DTO/Request/EventScannerRequest.js b/src/infrastructure/DTO/Request/EventScannerRequest.js new file mode 100644 index 0000000..dee7c26 --- /dev/null +++ b/src/infrastructure/DTO/Request/EventScannerRequest.js @@ -0,0 +1,15 @@ +/** + * DTO for Event Scanner + */ +export class EventScannerRequest { + constructor({ targetUserId, eventId }) { + this.data = { + users_permissions_user: parseInt(targetUserId, 10), + event: eventId + }; + } + + toJSON() { + return { data: this.data }; + } +} diff --git a/src/infrastructure/DTO/Request/EventSpeakerRequest.js b/src/infrastructure/DTO/Request/EventSpeakerRequest.js new file mode 100644 index 0000000..eaa2a48 --- /dev/null +++ b/src/infrastructure/DTO/Request/EventSpeakerRequest.js @@ -0,0 +1,18 @@ +/** + * DTO for Event Speaker + */ +export class EventSpeakerRequest { + constructor({ title, name, eventId, userId, linkedin }) { + this.data = { + title, + name, + event: eventId, + userId: userId ? parseInt(userId, 10) : null, + linkedin: linkedin || null + }; + } + + toJSON() { + return { data: this.data }; + } +} diff --git a/src/infrastructure/repository/BaseRepository.js b/src/infrastructure/repository/BaseRepository.js index e92cef8..0394baa 100644 --- a/src/infrastructure/repository/BaseRepository.js +++ b/src/infrastructure/repository/BaseRepository.js @@ -25,18 +25,32 @@ export class BaseRepository extends IApiClient { if (requestDto && typeof requestDto.validate === 'function') { requestDto.validate(); } + + let payload = requestDto; + if (requestDto && typeof requestDto.toPayload === 'function') { + payload = requestDto.toPayload(); + // If toPayload already wrapped in { data: ... }, don't wrap again + if (payload && payload.data) wrap = false; + } + const url = this.#buildUrl(endpoint); - const body = wrap ? { data: requestDto } : requestDto; + const body = wrap ? { data: payload } : payload; return await fetchWrapper(url, true, 'application/json', 'POST', body); } async put(endpoint, id, requestDto, wrap = true, params = {}) { - console.log('[BaseRepository DEBUG] put arguments:', { endpoint, id, wrap, params }); - if (requestDto && typeof requestDto.validate === 'function') { requestDto.validate(); } - + + let payload = requestDto; + if (requestDto && typeof requestDto.toPayload === 'function') { + payload = requestDto.toPayload(); + // If toPayload already wrapped in { data: ... }, don't wrap again + if (payload && payload.data) wrap = false; + } + + let url = this.#buildUrl(`${endpoint}/${id}`); const queryString = qs.stringify(params, { encodeValuesOnly: true }); @@ -44,7 +58,8 @@ export class BaseRepository extends IApiClient { url += `${url.includes('?') ? '&' : '?'}${queryString}`; } - const body = wrap ? { data: requestDto } : requestDto; + const body = wrap ? { data: payload } : payload; + console.log("body", body); return await fetchWrapper(url, true, 'application/json', 'PUT', body); } diff --git a/src/infrastructure/repository/CategorizationRepository.js b/src/infrastructure/repository/CategorizationRepository.js index 320471a..7502273 100644 --- a/src/infrastructure/repository/CategorizationRepository.js +++ b/src/infrastructure/repository/CategorizationRepository.js @@ -39,4 +39,14 @@ export class CategorizationRepository extends BaseRepository { const endpoint = `${import.meta.env.VITE_API_PROBLEM_TYPES}/${id}`; return await this.delete(endpoint); } + + async createCourseType(data) { + const endpoint = import.meta.env.VITE_API_COURSE_TYPES; + return await this.post(endpoint, data); + } + + async createProblemType(data) { + const endpoint = import.meta.env.VITE_API_PROBLEM_TYPES; + return await this.post(endpoint, data); + } } diff --git a/src/infrastructure/repository/EventRepository.js b/src/infrastructure/repository/EventRepository.js index a45d35a..86d113b 100644 --- a/src/infrastructure/repository/EventRepository.js +++ b/src/infrastructure/repository/EventRepository.js @@ -1,5 +1,8 @@ import { IEventInteraction } from '../../domain/interface/IEventInteraction'; import { EventRequest } from '../DTO/Request/EventRequest'; +import { EventActivityRequest } from '../DTO/Request/EventActivityRequest'; +import { EventSpeakerRequest } from '../DTO/Request/EventSpeakerRequest'; +import { EventScannerRequest } from '../DTO/Request/EventScannerRequest'; import { repositoryRegistry } from './RepositoryRegistry'; /** @@ -20,8 +23,12 @@ export class EventRepository extends IEventInteraction { } async getById(id) { - console.log(`${this.endpointBase}/${id}?populate=*`) - return await this.apiClient.get(`${this.endpointBase}/${id}?populate=*`); + const populate = [ + 'populate[scanners][populate][users_permissions_user]=true', + 'populate[speakers][populate]=*', + 'populate[event_activities][populate]=*', + ].join('&'); + return await this.apiClient.get(`${this.endpointBase}/${id}?${populate}`); } async getAll(page = 1, pageSize = 10, search = '') { const filters = search ? `&filters[title][$containsi]=${encodeURIComponent(search)}` : ''; @@ -35,12 +42,14 @@ export class EventRepository extends IEventInteraction { } async update(id, data) { + console.log(data) const request = new EventRequest(data); + console.log("request", request); return await this.apiClient.put(this.endpointBase, id, request, false); } async delete(id) { - return await this.apiClient.delete(this.endpointBase, id); + return await this.apiClient.delete(`${this.endpointBase}/${id}`); } async registerForEvent(eventId) { } @@ -61,4 +70,23 @@ export class EventRepository extends IEventInteraction { throw error; } } + + // ─── Sub-entities Orchestration ────────────────────────────────────── + + async createActivity(data) { + const request = new EventActivityRequest(data); + return await this.apiClient.post('/api/event-activities', request, false); + } + + async createSpeaker(data) { + const request = new EventSpeakerRequest(data); + return await this.apiClient.post('/api/speakers', request, false); + } + + async createScanner(data) { + // Must use token/auth since assigning scanners implies high permissions, though we'll stick to the base pattern first. + // If false fails, we might need true (authenticated req). Assuming admin token is automatically passed by apiClient. + const request = new EventScannerRequest(data); + return await this.apiClient.post('/api/scanners', request, false); + } } diff --git a/src/infrastructure/repository/RoadmapRepository.js b/src/infrastructure/repository/RoadmapRepository.js index 3f6a0f0..571d7f9 100644 --- a/src/infrastructure/repository/RoadmapRepository.js +++ b/src/infrastructure/repository/RoadmapRepository.js @@ -24,7 +24,7 @@ export class RoadmapRepository extends IRoadmapInteraction { } async delete(id) { - return await this.apiClient.delete(this.endpoint, id); + return await this.apiClient.delete(`${this.endpoint}/${id}`); } /** diff --git a/src/main.jsx b/src/main.jsx index 95f9ddf..dd39eaa 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -9,11 +9,14 @@ import store from "./infrastructure/store/store"; // // import { UIProvider } from "./presentation/shared/provider/UIProvider"; +import { ConfirmationProvider } from "./presentation/shared/provider/ConfirmationProvider"; createRoot(document.getElementById("root")).render( - + + + ); diff --git a/src/presentation/feature/article/components/write/ArticleMetaEditor.jsx b/src/presentation/feature/article/components/write/ArticleMetaEditor.jsx index ac6adcb..ff78066 100644 --- a/src/presentation/feature/article/components/write/ArticleMetaEditor.jsx +++ b/src/presentation/feature/article/components/write/ArticleMetaEditor.jsx @@ -37,17 +37,17 @@ export const ArticleMetaEditor = ({ title, onTitleChange, tags, onAddTag, onRemo className={cn( "flex items-center gap-3 px-4 py-2 rounded-xl border transition-all cursor-pointer select-none", isDraft - ? "bg-surface-sunken border-border-subtle text-text-muted" - : "bg-accent-primary/10 border-accent-primary text-accent-primary shadow-sm" + ? "bg-accent-primary/10 border-accent-primary text-accent-primary shadow-sm" + : "bg-surface-sunken border-border-subtle text-text-muted" )} >
diff --git a/src/presentation/feature/cms/components/CMSCreateCategorizationModal.jsx b/src/presentation/feature/cms/components/CMSCreateCategorizationModal.jsx new file mode 100644 index 0000000..60c822e --- /dev/null +++ b/src/presentation/feature/cms/components/CMSCreateCategorizationModal.jsx @@ -0,0 +1,90 @@ +import React, { useState } from 'react'; +import { X, Loader2, Save, Tag, Type } from 'lucide-react'; +import { cn } from '@core/utils/cn'; + +/** + * Premium Modal for creating new categorization tracks (Course/Problem types). + */ +export const CMSCreateCategorizationModal = ({ + isOpen, + onClose, + onSubmit, + modalHeader = "New Track", + isLoading = false +}) => { + const [title, setTitle] = useState(''); + + if (!isOpen) return null; + + const handleSubmit = (e) => { + e.preventDefault(); + onSubmit({ title }); + }; + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal Content */} +
+
+
+
+ +
+
+

{modalHeader}

+

Append new taxonomy node

+
+
+ +
+ +
+
+
+ + setTitle(e.target.value)} + className="w-full h-14 px-6 bg-white border border-border-default rounded-2xl font-serif text-sm focus:ring-2 focus:ring-accent-primary/20 focus:border-accent-primary outline-none transition-all shadow-sm" + placeholder="e.g. Advanced Calculus, Backend Systems..." + /> +
+
+ +
+ + +
+
+
+
+ ); +}; diff --git a/src/presentation/feature/cms/components/CMSResourceTable.jsx b/src/presentation/feature/cms/components/CMSResourceTable.jsx index 16919a3..0594bbc 100644 --- a/src/presentation/feature/cms/components/CMSResourceTable.jsx +++ b/src/presentation/feature/cms/components/CMSResourceTable.jsx @@ -1,6 +1,7 @@ import React, { useState, useEffect, useRef } from 'react'; import { Link } from 'react-router-dom'; import { PATHS } from '@presentation/routes/paths'; +import { useConfirm } from '@presentation/shared/provider/ConfirmationProvider'; import { useDeleteEvent } from '@domain/useCase/useDeleteEvent'; import { useDeleteProblem } from '@domain/useCase/useDeleteProblem'; import { useDeleteRoadmap } from '@domain/useCase/useDeleteRoadmap'; @@ -31,12 +32,15 @@ export const CMSResourceTable = ({ serverTotalPages = 1, serverTotalItems = 0, onPageChange = null, - onSearchChange = null + onSearchChange = null, + onAdd = null, + addLabel = "Append Entry" }) => { const [openDropdownId, setOpenDropdownId] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [selectedIds, setSelectedIds] = useState([]); + const { confirm } = useConfirm(); const dropdownRef = useRef(null); const itemsPerPage = 10; @@ -74,7 +78,14 @@ export const CMSResourceTable = ({ }; const handleRemove = async (id, title) => { - if (window.confirm(`Are you sure you want to permanently remove "${title}"? This action cannot be undone.`)) { + const ok = await confirm({ + title: 'Deaccession Content', + message: `Are you sure you want to permanently remove "${title}" from the repository? This action cannot be undone.`, + confirmLabel: 'Confirm Removal', + type: 'danger' + }); + + if (ok) { try { if (sectionName === 'Events') await deleteEvent(id); else if (sectionName === 'Problems') await deleteProblem(id); @@ -92,15 +103,26 @@ export const CMSResourceTable = ({ const handleBulkRemove = async () => { if (selectedIds.length === 0) return; - if (window.confirm(`Are you sure you want to permanently remove ${selectedIds.length} select entries?`)) { + const ok = await confirm({ + title: 'Mass Deaccession', + message: `Are you sure you want to permanently remove ${selectedIds.length} select entries from the repository?`, + confirmLabel: 'Purge Selected', + type: 'danger' + }); + + if (ok) { try { for (const id of selectedIds) { - if (sectionName === 'Events') await deleteEvent(id); - else if (sectionName === 'Problems') await deleteProblem(id); - else if (sectionName === 'Roadmaps') await deleteRoadmap(id); - else if (sectionName === 'Report-Reasons') await deleteReportType(id); - else if (sectionName === 'Courses') await deleteCourse(id); - else if (onDelete) await onDelete(id); + try { + if (sectionName === 'Events') await deleteEvent(id); + else if (sectionName === 'Problems') await deleteProblem(id); + else if (sectionName === 'Roadmaps') await deleteRoadmap(id); + else if (sectionName === 'Report-Reasons') await deleteReportType(id); + else if (sectionName === 'Courses') await deleteCourse(id); + else if (onDelete) await onDelete(id); + } catch (itemErr) { + console.error(`Failed to delete item ${id}:`, itemErr); + } } if (onRefresh) onRefresh(); setSelectedIds([]); @@ -161,7 +183,7 @@ export const CMSResourceTable = ({ return (
{/* 1. SCHOLARLY HEADER */} -
+
@@ -187,13 +209,23 @@ export const CMSResourceTable = ({ > - - - Append Entry - + {onAdd ? ( + + ) : ( + + + {addLabel} + + )}
@@ -225,7 +257,7 @@ export const CMSResourceTable = ({
{/* 3. REPOSITORY LEDGER */} -
+
{/* Ledger Header */}
@@ -300,8 +332,8 @@ export const CMSResourceTable = ({ {isDraft ? 'Draft' : 'Published'} @@ -333,7 +365,7 @@ export const CMSResourceTable = ({ { + const [isOpen, setIsOpen] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + + const filteredItems = availableItems.filter(item => + (item.title || item.type || '').toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const toggleItem = (id) => { + const newSelection = selectedIds.includes(id) + ? selectedIds.filter(i => i !== id) + : [...selectedIds, id]; + onChange(newSelection); + }; + + const selectedItems = availableItems.filter(item => selectedIds.includes(item.documentId || item.id)); + + return ( +
+ + +
setIsOpen(!isOpen)} + className={cn( + "min-h-[56px] w-full px-4 py-3 bg-surface-sunken/40 border border-border-subtle rounded-xl flex flex-wrap gap-2 cursor-pointer transition-all hover:bg-surface-sunken/60", + isOpen && "ring-2 ring-accent-primary/20 border-accent-primary/40" + )} + > + {selectedItems.length > 0 ? ( + selectedItems.map(item => ( +
+ {item.title || item.type} +
{ + e.stopPropagation(); + toggleItem(item.documentId || item.id); + }} + className="hover:bg-white/20 p-0.5 rounded transition-colors" + > + +
+
+ )) + ) : ( + {placeholder} + )} + +
+ {isLoading ? : } +
+
+ + {/* Dropdown Panel */} + {isOpen && ( +
+
+
+ + setSearchTerm(e.target.value)} + placeholder="Search taxonomies..." + className="w-full h-10 pl-10 pr-4 bg-surface-sunken/40 border border-border-subtle rounded-xl text-xs focus:outline-none focus:border-accent-primary/40 transition-all font-medium" + onClick={(e) => e.stopPropagation()} + /> +
+
+ +
+ {filteredItems.length > 0 ? ( + filteredItems.map(item => { + const id = item.documentId || item.id; + const isSelected = selectedIds.includes(id); + return ( +
{ + e.stopPropagation(); + toggleItem(id); + }} + className={cn( + "flex items-center justify-between px-4 py-3 rounded-xl cursor-copy transition-all group", + isSelected ? "bg-accent-primary/10 text-accent-primary" : "hover:bg-surface-sunken text-text-muted hover:text-text-primary" + )} + > + + {item.title || item.type} + + {isSelected && } +
+ ); + }) + ) : ( +
+ {isLoading ? "Synching archives..." : "No matches found"} +
+ )} +
+ +
+ +
+
+ )} +
+ ); +}; diff --git a/src/presentation/feature/cms/components/course/CourseMetadataEditor.jsx b/src/presentation/feature/cms/components/course/CourseMetadataEditor.jsx index db295e5..c0f4f79 100644 --- a/src/presentation/feature/cms/components/course/CourseMetadataEditor.jsx +++ b/src/presentation/feature/cms/components/course/CourseMetadataEditor.jsx @@ -10,6 +10,7 @@ export const CourseMetadataEditor = ({ courseId }) => { const { updateCourse, inProgress: isUpdating, error: updateError } = useUpdateCourse(); const { uploadMedia, inProgress: isUploading } = useUploadMedia(); + console.log("coursePreview",coursePreview) const [successMessage, setSuccessMessage] = useState(''); useEffect(() => { @@ -65,22 +66,23 @@ export const CourseMetadataEditor = ({ courseId }) => { const isLoading = isUpdating || isUploading; return ( -
-
-
-
- +
+ {/* Minimalist Tab Heading */} +
+
+
+
-

Course Metadata

-

Edit basic details, difficulty, categorization, and the course display thumbnail.

+

Technical Manuscript

+

Metadata & Core Configuration

{successMessage && ( -
- - {successMessage} +
+ + {successMessage.toUpperCase()}
)}
@@ -92,9 +94,9 @@ export const CourseMetadataEditor = ({ courseId }) => { /> {updateError && ( -
- -

{updateError}

+
+ +

{updateError}

)}
diff --git a/src/presentation/feature/cms/components/course/CourseSubscriptionAnalysis.jsx b/src/presentation/feature/cms/components/course/CourseSubscriptionAnalysis.jsx index 1286cda..50827b6 100644 --- a/src/presentation/feature/cms/components/course/CourseSubscriptionAnalysis.jsx +++ b/src/presentation/feature/cms/components/course/CourseSubscriptionAnalysis.jsx @@ -6,6 +6,7 @@ import { useCreateUserEntitlement } from '@domain/useCase/useCreateUserEntitleme import { useDeleteUserEntitlement } from '@domain/useCase/useDeleteUserEntitlement'; import { useSearchUsers } from '@domain/useCase/useSearchUsers'; import { useFetchCoursePreview } from '@domain/useCase/useFetchCoursePreview'; +import { useConfirm } from '@presentation/shared/provider/ConfirmationProvider'; import { UserEntitlementRequest } from '@infrastructure/DTO/Request/UserEntitlementRequest'; import { SubscriptionChart } from '../shared/SubscriptionChart'; @@ -20,6 +21,7 @@ export const CourseSubscriptionAnalysis = ({ courseId }) => { const { createUserEntitlement, inProgress: isCreating } = useCreateUserEntitlement(); const { deleteUserEntitlement, inProgress: isDeleting } = useDeleteUserEntitlement(); const { searchUsers, foundUser, loading: isSearching } = useSearchUsers(); + const { confirm } = useConfirm(); // ─── Local State ───────────────────────────────────────────────────── const [emailSearch, setEmailSearch] = useState(''); @@ -82,7 +84,13 @@ export const CourseSubscriptionAnalysis = ({ courseId }) => { }; const handleRevokeAccess = async (recordId) => { - if (!window.confirm('Are you sure you want to revoke this student\'s access?')) return; + const ok = await confirm({ + title: 'Revoke Enrollment', + message: 'Are you sure you want to permanently revoke this student\'s access to the course? They will lose all progress data instantly.', + confirmLabel: 'Confirm Revocation', + type: 'danger' + }); + if (!ok) return; try { await deleteUserEntitlement(recordId); diff --git a/src/presentation/feature/cms/components/course/CourseTypesEditor.jsx b/src/presentation/feature/cms/components/course/CourseTypesEditor.jsx new file mode 100644 index 0000000..fb5ca05 --- /dev/null +++ b/src/presentation/feature/cms/components/course/CourseTypesEditor.jsx @@ -0,0 +1,201 @@ +import React, { useState, useEffect } from 'react'; +import { Layers, CheckCircle, AlertCircle, Loader2, Save, Tag } from 'lucide-react'; +import { cn } from '@core/utils/cn'; +import { useFetchCategorizations } from '@domain/useCase/useFetchCategorizations'; +import { useFetchCoursePreview } from '@domain/useCase/useFetchCoursePreview'; +import { useUpdateCourse } from '@domain/useCase/useUpdateCourse'; + +/** + * CourseTypesEditor: Manages the association between a course and its categorization types. + */ +export const CourseTypesEditor = ({ courseId }) => { + const { courseTypes: allTypes, isLoading: isLoadingAll } = useFetchCategorizations(); + const { fetchCoursePreview, coursePreview, loading: isFetchingCourse } = useFetchCoursePreview(); + const { updateCourse, inProgress: isUpdating } = useUpdateCourse(); + + const [selectedTypeIds, setSelectedTypeIds] = useState([]); + const [statusMessage, setStatusMessage] = useState({ type: '', text: '' }); + + // Initial Load + useEffect(() => { + if (courseId) { + fetchCoursePreview(courseId); + } + }, [courseId, fetchCoursePreview]); + + // Sync selected types from course preview + useEffect(() => { + if (coursePreview?.course_types) { + setSelectedTypeIds(coursePreview.course_types.map(t => t.id || t)); + } + }, [coursePreview]); + + const handleToggleType = (id) => { + setSelectedTypeIds(prev => + prev.includes(id) ? prev.filter(item => item !== id) : [...prev, id] + ); + }; + + const handleSave = async () => { + try { + await updateCourse({ + id: courseId, + data: { + courseTypeIds: selectedTypeIds + } + }); + showStatus('success', 'Taxonomy relationships updated successfully.'); + } catch (err) { + showStatus('error', 'Failed to synchronize course types.'); + } + }; + + const showStatus = (type, text) => { + setStatusMessage({ type, text }); + setTimeout(() => setStatusMessage({ type: '', text: '' }), 5000); + }; + + if ((isLoadingAll || isFetchingCourse) && !coursePreview) { + return ( +
+ +

Harmonizing Taxonomy Data...

+
+ ); + } + + return ( +
+ {/* Header Area */} +
+
+
+ +
+
+

Categorization Suite

+

Manage Course Types & Tracks

+
+
+ + {statusMessage.text && ( +
+ {statusMessage.type === 'success' ? : } + {statusMessage.text} +
+ )} +
+ +
+ {/* Available Types Selection */} +
+
+ {allTypes.map((type) => { + const isSelected = selectedTypeIds.includes(type.id); + return ( +
handleToggleType(type.id)} + className={cn( + "group p-5 rounded-2xl border transition-all cursor-pointer relative overflow-hidden active:scale-[0.98]", + isSelected + ? "bg-accent-violet/5 border-accent-violet/40 ring-1 ring-accent-violet/20" + : "bg-surface border-border-subtle hover:border-border-default hover:bg-surface-elevated/50 shadow-sm" + )} + > +
+
+
+ +
+
+

{type.title}

+

Course Track

+
+
+
+ {isSelected && } +
+
+ + {/* Abstract Decoration */} +
+ +
+
+ ); + })} +
+ + {allTypes.length === 0 && ( +
+ +

No Categorizations Found

+

Manage global course types in the Taxonomy module before assigning them here.

+
+ )} +
+ + {/* Sidebar Info & Actions */} +
+
+
+

+ + Hierarchy Logic +

+

+ Assigning a course to specific types determines its placement in the global catalogue and recommendation algorithms. +

+
+ +
+
+ Selected Slots + {selectedTypeIds.length} +
+ + +
+
+ +
+ +

+ Changes affect the categorization immediately across the platform. Ensure the tracks match the course content accurately. +

+
+
+
+
+ ); +}; + +const Shield = ({ size, className }) => ( + +); diff --git a/src/presentation/feature/cms/components/course/CourseWeeksEditor.jsx b/src/presentation/feature/cms/components/course/CourseWeeksEditor.jsx index 1f79e9d..52b3e8d 100644 --- a/src/presentation/feature/cms/components/course/CourseWeeksEditor.jsx +++ b/src/presentation/feature/cms/components/course/CourseWeeksEditor.jsx @@ -5,6 +5,7 @@ import { useCreateWeek } from '@domain/useCase/useCreateWeek'; import { useUpdateWeek } from '@domain/useCase/useUpdateWeek'; import { useDeleteWeek } from '@domain/useCase/useDeleteWeek'; import { useDeleteLesson } from '@domain/useCase/useDeleteLesson'; +import { useConfirm } from '@presentation/shared/provider/ConfirmationProvider'; import { WeekList } from './WeekList'; import { WeekFormModal } from './WeekFormModal'; @@ -21,6 +22,7 @@ export const CourseWeeksEditor = ({ courseId }) => { const { updateWeek, inProgress: isUpdating } = useUpdateWeek(); const { deleteWeek, inProgress: isDeleting } = useDeleteWeek(); const { deleteLesson, inProgress: isDeletingLesson } = useDeleteLesson(); + const { confirm } = useConfirm(); // Week Modal State const [isModalOpen, setIsModalOpen] = useState(false); @@ -70,8 +72,13 @@ export const CourseWeeksEditor = ({ courseId }) => { const handleDelete = async (week) => { const weekId = week.documentId || week.id; - const confirmed = window.confirm(`Are you sure you want to delete "${week.title || 'this week'}"?`); - if (!confirmed) return; + const ok = await confirm({ + title: 'Expunge Syllabus Segment', + message: `Are you sure you want to permanently delete "${week.title || 'this week'}" and all its associated pedagogical data?`, + confirmLabel: 'Confirm Deletion', + type: 'danger' + }); + if (!ok) return; try { await deleteWeek(weekId); @@ -83,8 +90,13 @@ export const CourseWeeksEditor = ({ courseId }) => { const handleDeleteLesson = async (lesson) => { const lessonId = lesson.uid || lesson.id; - const confirmed = window.confirm(`Are you sure you want to delete "${lesson.title || 'this lesson'}"?`); - if (!confirmed) return; + const ok = await confirm({ + title: 'Discard Lesson Module', + message: `Are you sure you want to remove "${lesson.title || 'this lesson'}" from the curriculum?`, + confirmLabel: 'Discard Module', + type: 'danger' + }); + if (!ok) return; try { await deleteLesson(lessonId); diff --git a/src/presentation/feature/cms/components/course/LessonFormModal.jsx b/src/presentation/feature/cms/components/course/LessonFormModal.jsx index fc57073..30b47a3 100644 --- a/src/presentation/feature/cms/components/course/LessonFormModal.jsx +++ b/src/presentation/feature/cms/components/course/LessonFormModal.jsx @@ -33,7 +33,7 @@ export const LessonFormModal = ({ isOpen, onClose, onSubmit, weekTitle = '', isL return (
-
+
{/* Decorative background element */}
@@ -129,17 +129,17 @@ export const LessonFormModal = ({ isOpen, onClose, onSubmit, weekTitle = '', isL className={cn( "flex items-center gap-2 px-3 py-1.5 rounded-full border transition-all cursor-pointer select-none", isDraft - ? "bg-surface-sunken border-border-subtle text-text-muted" - : "bg-accent-primary/10 border-accent-primary text-accent-primary" + ? "bg-accent-primary/10 border-accent-primary text-accent-primary" + : "bg-surface-sunken border-border-subtle text-text-muted" )} >
diff --git a/src/presentation/feature/cms/components/course/SubscriptionChart.jsx b/src/presentation/feature/cms/components/course/SubscriptionChart.jsx index f3d0cd4..0ac8f31 100644 --- a/src/presentation/feature/cms/components/course/SubscriptionChart.jsx +++ b/src/presentation/feature/cms/components/course/SubscriptionChart.jsx @@ -105,7 +105,7 @@ export const SubscriptionChart = ({ data = [], filter = 'all' }) => {
-
+
@@ -128,8 +128,8 @@ export const SubscriptionChart = ({ data = [], filter = 'all' }) => { > - - + + @@ -165,7 +165,7 @@ export const SubscriptionChart = ({ data = [], filter = 'all' }) => { { cx={p.x} cy={p.y} r="4" - fill="var(--color-accent-blue)" + fill="var(--color-accent-primary)" className={cn( "transition-all duration-300 pointer-events-none", hoveredPoint?.index === p.index ? "opacity-100 r-6" : "opacity-0" @@ -225,7 +225,7 @@ export const SubscriptionChart = ({ data = [], filter = 'all' }) => { }} >
-

{hoveredPoint.data.label}

+

{hoveredPoint.data.label}

Enrollments {hoveredPoint.data.value} diff --git a/src/presentation/feature/cms/components/course/WeekFormModal.jsx b/src/presentation/feature/cms/components/course/WeekFormModal.jsx index c7159ee..a26fd73 100644 --- a/src/presentation/feature/cms/components/course/WeekFormModal.jsx +++ b/src/presentation/feature/cms/components/course/WeekFormModal.jsx @@ -33,7 +33,7 @@ export const WeekFormModal = ({ isOpen, onClose, onSubmit, editingWeek = null, i return (
-
+
{/* Decorative background element */}
diff --git a/src/presentation/feature/cms/components/dashboard/viewers/GrowthTimelineViewer.jsx b/src/presentation/feature/cms/components/dashboard/viewers/GrowthTimelineViewer.jsx index 4a3d98f..9f62af2 100644 --- a/src/presentation/feature/cms/components/dashboard/viewers/GrowthTimelineViewer.jsx +++ b/src/presentation/feature/cms/components/dashboard/viewers/GrowthTimelineViewer.jsx @@ -3,15 +3,15 @@ import { TrendingUp, Users } from 'lucide-react'; const COLOR_MAPS = { "accent-primary": { bar: "bg-accent-primary/40", hover: "hover:bg-accent-primary", text: "text-accent-primary", border: "border-accent-primary/20", bg: "bg-accent-primary/5" }, - "accent-blue": { bar: "bg-accent-blue/40", hover: "hover:bg-accent-blue", text: "text-accent-blue", border: "border-accent-blue/20", bg: "bg-accent-blue/5" }, + "accent-primary": { bar: "bg-accent-primary/40", hover: "hover:bg-accent-primary", text: "text-accent-primary", border: "border-accent-primary/20", bg: "bg-accent-primary/5" }, "accent-violet": { bar: "bg-accent-violet/40", hover: "hover:bg-accent-violet", text: "text-accent-violet", border: "border-accent-violet/20", bg: "bg-accent-violet/5" }, "accent-rose": { bar: "bg-accent-rose/40", hover: "hover:bg-accent-rose", text: "text-accent-rose", border: "border-accent-rose/20", bg: "bg-accent-rose/5" }, }; -const UnifiedTimelineViewer = ({ data = {}, title, subtitle, colorClass = "accent-blue" }) => { +const UnifiedTimelineViewer = ({ data = {}, title, subtitle, colorClass = "accent-primary" }) => { const timeline = data.timeline || []; const maxVal = Math.max(...timeline.map(d => d.count), 1); - const colors = COLOR_MAPS[colorClass] || COLOR_MAPS["accent-blue"]; + const colors = COLOR_MAPS[colorClass] || COLOR_MAPS["accent-primary"]; return (
diff --git a/src/presentation/feature/cms/components/event/EventActivitiesEditor.jsx b/src/presentation/feature/cms/components/event/EventActivitiesEditor.jsx new file mode 100644 index 0000000..f07b12f --- /dev/null +++ b/src/presentation/feature/cms/components/event/EventActivitiesEditor.jsx @@ -0,0 +1,163 @@ +import React, { useEffect, useState } from 'react'; +import { Loader2, CalendarClock, Clock, AlertCircle, Plus } from 'lucide-react'; +import { useFetchEvent } from '@domain/useCase/useFetchEvent'; +import { useCreateEventActivity } from '@domain/useCase/useCreateEventActivity'; + +/** + * EventActivitiesEditor: Displays and orchestrates the itinerary of an event. + */ +export const EventActivitiesEditor = ({ eventId }) => { + const { fetchEvent, event, loading } = useFetchEvent(); + const { createEventActivity, inProgress } = useCreateEventActivity(); + + // Form State + const [title, setTitle] = useState(''); + const [from, setFrom] = useState(''); + const [description, setDescription] = useState(''); + + useEffect(() => { + if (eventId) { + fetchEvent(eventId); + } + }, [eventId, fetchEvent]); + + const handleSubmit = async (e) => { + e.preventDefault(); + try { + await createEventActivity({ + title, + from, + description, + eventId: event.id + }); + // Reset form + setTitle(''); + setFrom(''); + setDescription(''); + // Refresh list + fetchEvent(eventId); + } catch (e) { + // Toast is handled in UseCase + } + }; + + if (loading && !event) { + return ( +
+ +

Constructing Itinerary...

+
+ ); + } + + if (!event) return null; + + const activities = event.activities || []; + + return ( +
+
+
+ +
+
+

Event Itinerary

+

+ Schedule and sequencing protocols for this event. +

+
+
+ + {/* Creation Form */} +
+
+ +

Add Itinerary Segment

+
+ +
+
+ + setTitle(e.target.value)} + placeholder="e.g. Opening Keynote" + className="bg-surface-sunken border border-border-subtle rounded-2xl px-4 py-3 text-sm text-text-primary focus:border-accent-primary/50 outline-none transition-all" + /> +
+
+ + setFrom(e.target.value)} + className="bg-surface-sunken border border-border-subtle rounded-2xl px-4 py-3 text-sm text-text-primary focus:border-accent-primary/50 outline-none transition-all tabular-nums" + /> +
+
+ +
+ +