diff --git a/.gitignore b/.gitignore index a547bf3..2d2c8d2 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ dist-ssr *.njsproj *.sln *.sw? +.env* diff --git a/src/domain/entity/TaskEntity.js b/src/domain/entity/TaskEntity.js index a6c8d6d..e2d4e3d 100644 --- a/src/domain/entity/TaskEntity.js +++ b/src/domain/entity/TaskEntity.js @@ -9,6 +9,9 @@ export class TaskEntity { this.branchPattern = data.branchPattern; this.githubPrId = data.githubPrId; this.ciStatus = data.ciStatus; + this.lastCommitSha = data.lastCommitSha; // latest commit SHA (short) + this.prUrl = data.prUrl; // GitHub PR URL + this.prTitle = data.prTitle; // GitHub PR title this.order = data.order; this.createdAt = data.createdAt; this.updatedAt = data.updatedAt; diff --git a/src/domain/mapper/EntityMapper.js b/src/domain/mapper/EntityMapper.js index 9079fe0..3908649 100644 --- a/src/domain/mapper/EntityMapper.js +++ b/src/domain/mapper/EntityMapper.js @@ -436,7 +436,10 @@ 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 + isDraft: !!dto.isDraft, + type: dto.type || 'blog', + projectRole: dto.project_role ? this.toProjectRole(dto.project_role) : null, + project: dto.project ? this.toProject(dto.project) : null }); } @@ -739,6 +742,9 @@ export class EntityMapper { branchPattern: dto.branch_pattern, githubPrId: dto.github_pr_id, ciStatus: dto.ci_status, + lastCommitSha: dto.last_commit_sha, + prUrl: dto.pr_url, + prTitle: dto.pr_title, order: dto.order, createdAt: dto.createdAt, updatedAt: dto.updatedAt, @@ -753,6 +759,7 @@ export class EntityMapper { }); } + /** * Maps a CheckpointDTO to CheckpointEntity */ diff --git a/src/domain/useCase/task/useKanbanBoard.js b/src/domain/useCase/task/useKanbanBoard.js index ce1e31c..3c422ff 100644 --- a/src/domain/useCase/task/useKanbanBoard.js +++ b/src/domain/useCase/task/useKanbanBoard.js @@ -1,21 +1,47 @@ -import { useCallback, useMemo } from 'react'; +import { useCallback, useMemo, useRef } from 'react'; import { useAsyncUseCase } from '../useAsyncUseCase'; import { repositoryRegistry } from '@infrastructure/repository/RepositoryRegistry'; import { EntityMapper } from '../../mapper/EntityMapper'; +// Auto-refresh interval in milliseconds (15s when tab is visible) +const REFRESH_INTERVAL_MS = 15000; + export const useKanbanBoard = (projectId) => { const repository = useMemo(() => repositoryRegistry.taskRepository, []); - // Fetch tasks + // ─── Fetch tasks ───────────────────────────────────────────────────────── const fetchLogic = useCallback(async () => { if (!projectId) return []; const result = await repository.getTasks(projectId); return result.map(dto => EntityMapper.toTask(dto)); }, [repository, projectId]); - const { execute: fetchTasks, returnedData: tasks, inProgress: loadingTasks, error: tasksError } = useAsyncUseCase(fetchLogic); + const { + execute: fetchTasks, + returnedData: tasks, + inProgress: loadingTasks, + error: tasksError, + } = useAsyncUseCase(fetchLogic); + + // ─── Silent refresh (no loading spinner) ───────────────────────────────── + // Returns a stable ref so the polling interval can always call the latest version + const fetchTasksRef = useRef(fetchTasks); + fetchTasksRef.current = fetchTasks; + + const startPolling = useCallback(() => { + if (!projectId) return () => {}; + + const id = setInterval(() => { + // Only refresh when the browser tab is visible to avoid wasted requests + if (document.visibilityState === 'visible') { + fetchTasksRef.current?.(); + } + }, REFRESH_INTERVAL_MS); + + return () => clearInterval(id); + }, [projectId]); - // Transition Task Status + // ─── Transition Task Status ─────────────────────────────────────────────── const transitionLogic = useCallback(async (taskId, newStatus) => { const result = await repository.transitionTask(projectId, taskId, newStatus); return EntityMapper.toTask(result); @@ -23,15 +49,14 @@ export const useKanbanBoard = (projectId) => { const { execute: transitionTask, inProgress: isTransitioning } = useAsyncUseCase(transitionLogic); - // Reorder Tasks + // ─── Reorder Tasks ──────────────────────────────────────────────────────── const reorderLogic = useCallback(async (taskOrders) => { - // taskOrders = [{ documentId, order }] return await repository.reorderTasks(projectId, taskOrders); }, [repository, projectId]); const { execute: reorderTasks, inProgress: isReordering } = useAsyncUseCase(reorderLogic); - // Create Task + // ─── Create Task ────────────────────────────────────────────────────────── const createLogic = useCallback(async (taskData) => { const result = await repository.createTask(projectId, taskData); return EntityMapper.toTask(result); @@ -39,7 +64,7 @@ export const useKanbanBoard = (projectId) => { const { execute: createTask, inProgress: isCreatingTask } = useAsyncUseCase(createLogic); - // Delete Task + // ─── Delete Task ────────────────────────────────────────────────────────── const deleteLogic = useCallback(async (taskId) => { return await repository.deleteTask(projectId, taskId); }, [repository, projectId]); @@ -51,6 +76,7 @@ export const useKanbanBoard = (projectId) => { tasks, loadingTasks, tasksError, + startPolling, // call this in KanbanBoard's useEffect → returns cleanup fn transitionTask, isTransitioning, reorderTasks, @@ -58,6 +84,6 @@ export const useKanbanBoard = (projectId) => { createTask, isCreatingTask, deleteTask, - isDeletingTask + isDeletingTask, }; }; diff --git a/src/domain/useCase/useFetchBlogs.js b/src/domain/useCase/useFetchBlogs.js index e4da6e6..f5f487d 100644 --- a/src/domain/useCase/useFetchBlogs.js +++ b/src/domain/useCase/useFetchBlogs.js @@ -33,7 +33,13 @@ export const useFetchBlogs = () => { fields: ['firstname', 'lastname', 'email', 'username', 'birthday', 'university', 'bio'], populate: ['avatar'] }, - image: true + image: true, + project_role: { + populate: ['job_title_tag'] + }, + project: { + populate: ['publisher'] + } }; const rawData = await repository.getBlogs(limit, excludeIds, feedType, populate); diff --git a/src/infrastructure/DTO/BlogDTO.js b/src/infrastructure/DTO/BlogDTO.js index 8d373e3..b7df9b6 100644 --- a/src/infrastructure/DTO/BlogDTO.js +++ b/src/infrastructure/DTO/BlogDTO.js @@ -14,5 +14,8 @@ export class BlogDTO extends BaseContentDTO { * Relationships */ this.publisher = data.publisher ? new UserDTO(data.publisher) : null; // {UserDTO | null} + this.type = data.type || 'blog'; + this.project_role = data.project_role || null; + this.project = data.project || null; } } diff --git a/src/infrastructure/DTO/TaskDTO.js b/src/infrastructure/DTO/TaskDTO.js index e4c40a8..a9aa53f 100644 --- a/src/infrastructure/DTO/TaskDTO.js +++ b/src/infrastructure/DTO/TaskDTO.js @@ -11,6 +11,9 @@ export class TaskDTO { this.branch_pattern = data.branch_pattern; this.github_pr_id = data.github_pr_id; this.ci_status = data.ci_status; // pending, success, failure, cancelled + this.last_commit_sha = data.last_commit_sha; + this.pr_url = data.pr_url; + this.pr_title = data.pr_title; this.order = data.order; this.createdAt = data.createdAt; this.updatedAt = data.updatedAt; diff --git a/src/infrastructure/repository/ProjectRepository.js b/src/infrastructure/repository/ProjectRepository.js index 1ba161f..0ef3561 100644 --- a/src/infrastructure/repository/ProjectRepository.js +++ b/src/infrastructure/repository/ProjectRepository.js @@ -71,6 +71,23 @@ export class ProjectRepository extends IProjectAccess { return response?.data || response; } + async updateMemberGithubUsername(projectId, memberId, githubUsername) { + const response = await this.apiClient.patch( + `${this.projectsEndpoint}/${projectId}/members/${memberId}/github-username`, + { github_username: githubUsername } + ); + return new ProjectMemberDTO(response?.data || response); + } + + // --- GitHub Repo Linking --- + async linkGithubRepo(projectId, { github_repo_url, github_repo_id, github_webhook_secret }) { + const response = await this.apiClient.patch( + `${this.projectsEndpoint}/${projectId}/github-repo`, + { github_repo_url, github_repo_id, github_webhook_secret } + ); + return response?.data || response; + } + // --- Applications --- async applyToRole(projectId, data) { const response = await this.apiClient.post(`${this.projectsEndpoint}/${projectId}/apply`, data); diff --git a/src/presentation/feature/feed/components/FeedItem.jsx b/src/presentation/feature/feed/components/FeedItem.jsx index 5dfe969..c8a1cb1 100644 --- a/src/presentation/feature/feed/components/FeedItem.jsx +++ b/src/presentation/feature/feed/components/FeedItem.jsx @@ -55,6 +55,8 @@ export const FeedItem = ({ blog, className, rank, highlight }) => { return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); }; + const isJobOpportunity = blog.type === 'job_opportunity'; + // Determine Podium Styles let podiumStyles = "border-border-subtle bg-surface-dark"; let badge = null; @@ -71,7 +73,17 @@ export const FeedItem = ({ blog, className, rank, highlight }) => { } else if (highlight) { podiumStyles = "border-accent-primary/50 bg-accent-primary/5 shadow-[0_0_40px_rgba(var(--accent-primary-rgb),0.1)] ring-1 ring-accent-primary/20"; } - console.log(blog); + + if (isJobOpportunity) { + podiumStyles = "border-blue-500/50 bg-gradient-to-br from-blue-900/10 to-blue-500/5 shadow-[0_0_40px_rgba(59,130,246,0.15)] ring-1 ring-blue-500/30"; + badge = ( +
{task.description} -
{suggestedBranch}
+
+ git commit -m "fix: ... TASK-{task.uid}"
+
+ + When you open a PR from this branch, the task moves to In Review. + When merged, it becomes Done ✅. +
+
+ {task.branchPattern || `task/${task.uid}`}
+
+
+
+ git commit -m "fix: ... TASK-{task.uid}"
+
+
+
+ {task.branchPattern || `task/${task.uid}`}
+
+ Coming Soon
-Inshaa Allah
-