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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.env*
3 changes: 3 additions & 0 deletions src/domain/entity/TaskEntity.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 8 additions & 1 deletion src/domain/mapper/EntityMapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
}

Expand Down Expand Up @@ -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,
Expand All @@ -753,6 +759,7 @@ export class EntityMapper {
});
}


/**
* Maps a CheckpointDTO to CheckpointEntity
*/
Expand Down
44 changes: 35 additions & 9 deletions src/domain/useCase/task/useKanbanBoard.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,70 @@
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);
}, [repository, 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);
}, [repository, 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]);
Expand All @@ -51,13 +76,14 @@ export const useKanbanBoard = (projectId) => {
tasks,
loadingTasks,
tasksError,
startPolling, // call this in KanbanBoard's useEffect → returns cleanup fn
transitionTask,
isTransitioning,
reorderTasks,
isReordering,
createTask,
isCreatingTask,
deleteTask,
isDeletingTask
isDeletingTask,
};
};
8 changes: 7 additions & 1 deletion src/domain/useCase/useFetchBlogs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions src/infrastructure/DTO/BlogDTO.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
3 changes: 3 additions & 0 deletions src/infrastructure/DTO/TaskDTO.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
17 changes: 17 additions & 0 deletions src/infrastructure/repository/ProjectRepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
26 changes: 24 additions & 2 deletions src/presentation/feature/feed/components/FeedItem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 = (
<div className="absolute -top-3 -left-3 px-3 py-1 rounded-full bg-gradient-to-r from-blue-500 to-blue-700 flex items-center justify-center text-white text-[10px] font-bold shadow-lg border-2 border-background uppercase tracking-widest gap-1">
<span className="text-sm">🔥</span> Project Opportunity
</div>
);
}

// console.log(blog);

return (
<div className={cn(
Expand All @@ -81,7 +93,7 @@ export const FeedItem = ({ blog, className, rank, highlight }) => {
)}>
{badge}
{/* Header: Author & Meta */}
<div className="flex justify-between items-center">
<div className="flex justify-between items-center mt-2">
<div
onClick={(e) => {
if (blog.author?.username) {
Expand Down Expand Up @@ -188,6 +200,16 @@ export const FeedItem = ({ blog, className, rank, highlight }) => {
/>
</div>
)}

{isJobOpportunity && blog.project && (
<div className="mt-4 pt-4 border-t border-border-subtle/50">
<Link to={`/projects/${blog.project.uid}`}>
<button className="w-full py-3 rounded-xl bg-gradient-to-r from-blue-600 to-blue-500 hover:from-blue-500 hover:to-blue-400 text-white font-bold text-sm uppercase tracking-widest shadow-lg shadow-blue-500/25 hover:shadow-blue-500/40 transition-all flex items-center justify-center gap-2">
Join Now 🚀
</button>
</Link>
</div>
)}
</div>

{/* Interaction Footer */}
Expand Down
9 changes: 0 additions & 9 deletions src/presentation/feature/feed/routes/FeedPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,21 +144,12 @@ const FeedPage = () => {

{/* 2. Show regular feed, filtering out the linked blog to avoid duplication */}
{(blogs || []).filter(b => b.id !== linkedBlog?.id).map((blog, index) => {
const showJobAd = (index > 0 && index % 10 === 0);
const jobAdIndex = (index / 10) - 1;
const jobAdToRender = showJobAd && jobAds ? jobAds[jobAdIndex] : null;

return (
<React.Fragment key={blog.id || index}>
<FeedItem
blog={blog}
rank={activeFilter === 'trend' ? index + 1 : null}
/>
{jobAdToRender && (
<div className="my-6">
<JobAdCard jobAd={jobAdToRender} />
</div>
)}
</React.Fragment>
);
})}
Expand Down
Loading
Loading