diff --git a/backend/controllers/feed.controller.js b/backend/controllers/feed.controller.js index cf7b510..c2d4094 100644 --- a/backend/controllers/feed.controller.js +++ b/backend/controllers/feed.controller.js @@ -3,113 +3,70 @@ import supabase from "../config/db.js"; const manualItems = []; -function calculateEngagementScore( - item -) { - let score = 0; - - if (item.type === "milestone") { - score += 50; - } +/* ----------------------------- SCORING ENGINE ---------------------------- */ - if (item.type === "code") { - score += 30; - } - - if (item.type === "discussion") { - score += 20; - } +function calculateEngagementScore(item) { + let score = 0; - if (item.progress) { - score += item.progress; - } + if (item.type === "milestone") score += 50; + else if (item.type === "code") score += 30; + else if (item.type === "discussion") score += 20; - return score; + return score + (item.progress || 0); } -function calculateRecencyScore( - createdAt -) { - const ageHours = - Math.max( - 1, - (Date.now() - - new Date( - createdAt - ).getTime()) / - 3600000 - ); - - return Math.max( +function calculateRecencyScore(createdAt) { + const ageHours = Math.max( 1, - Math.round(100 / ageHours) + (Date.now() - new Date(createdAt).getTime()) / 3600000 ); + + return Math.max(1, Math.round(100 / ageHours)); } -function buildRankingMetadata( - item -) { - const engagement = - calculateEngagementScore( - item - ); +/* ---------------------------- METADATA BUILDERS -------------------------- */ - const recency = - calculateRecencyScore( - item.createdAt - ); +function buildRankingMetadata(item) { + const engagement = calculateEngagementScore(item); + const recency = calculateRecencyScore(item.createdAt); return { ...item, - rankingScore: - engagement + recency, - rankingFactors: { - engagement, - recency, - }, + rankingScore: engagement + recency, + rankingFactors: { engagement, recency }, }; } -function buildStableRankingMetadata( - item, - index -) { +function buildStableRankingMetadata(item, index) { return { stableRankKey: `${item.id}-${index}`, insertionOrder: index, - refreshSequence: - Date.now(), + refreshSequence: Date.now(), }; } -function buildFeedMetadata( - items = [] -) { +function buildFeedMetadata(items = []) { return { - totalItems: - items.length, + totalItems: items.length, stableSortingEnabled: true, refreshSafeOrdering: true, - generatedAt: - Date.now(), + generatedAt: Date.now(), }; } - +/* ------------------------------ TIME HELPERS ----------------------------- */ function toRelativeTime(value) { if (!value) return "Just now"; - const createdAt = new Date(value).getTime(); const diffMinutes = Math.max( 1, - Math.floor((Date.now() - createdAt) / 60000) + Math.floor((Date.now() - new Date(value).getTime()) / 60000) ); if (diffMinutes < 60) return `${diffMinutes} min ago`; const diffHours = Math.floor(diffMinutes / 60); - if (diffHours < 24) return `${diffHours} hours ago`; return new Date(value).toLocaleDateString("en", { @@ -122,35 +79,32 @@ function groupForDate(value) { if (!value) return "Today"; const itemDate = new Date(value); - const today = new Date(); - - const yesterday = new Date(); + const yesterday = new Date(today); yesterday.setDate(today.getDate() - 1); - if (itemDate.toDateString() === today.toDateString()) { - return "Today"; - } - - if (itemDate.toDateString() === yesterday.toDateString()) { - return "Yesterday"; - } + if (itemDate.toDateString() === today.toDateString()) return "Today"; + if (itemDate.toDateString() === yesterday.toDateString()) return "Yesterday"; return "Earlier"; } -function normalizeFeedItem({ - id, - type, - actor, - action, - title, - body, - createdAt, - meta, - image = null, - progress = null, -}) { +/* --------------------------- NORMALIZATION LAYER ------------------------- */ + +function normalizeFeedItem(item) { + const { + id, + type, + actor, + action, + title, + body, + createdAt, + meta, + image = null, + progress = null, + } = item; + return { id, type, @@ -167,6 +121,8 @@ function normalizeFeedItem({ }; } +/* ---------------------------- TRANSFORMERS ------------------------------- */ + function taskToFeedItem(task) { const isDone = task.status === "done"; @@ -178,17 +134,10 @@ function taskToFeedItem(task) { title: task.title || "Untitled task", body: task.description || - `Status changed to ${String( - task.status || "todo" - ).replace("_", " ")}.`, + `Status changed to ${String(task.status || "todo").replace("_", " ")}.`, createdAt: task.created_at, meta: isDone ? "Task completed" : "Task activity", - progress: - isDone - ? 100 - : task.status === "in_progress" - ? 65 - : 20, + progress: isDone ? 100 : task.status === "in_progress" ? 65 : 20, }); } @@ -202,151 +151,82 @@ function messageToFeedItem(message) { body: message.text || "Shared an attachment.", createdAt: message.created_at, meta: "Chat activity", - image: - "https://i.pravatar.cc/96?u=" + - encodeURIComponent( - message.username || message.id - ), + image: `https://i.pravatar.cc/96?u=${encodeURIComponent( + message.username || message.id + )}`, }); } +/* ------------------------------ CORE ENGINE ------------------------------ */ + function buildFeedItems(tasks = [], messages = []) { return [ ...manualItems, - ...(tasks || []).map(taskToFeedItem), - ...(messages || []).map(messageToFeedItem), + ...tasks.map(taskToFeedItem), + ...messages.map(messageToFeedItem), ]; } function sortFeedItems(items = []) { return items - .map( - ( - item, - index - ) => ({ - ...buildRankingMetadata( - item - ), - ...buildStableRankingMetadata( - item, - index - ), - }) - ) + .map((item, index) => ({ + ...buildRankingMetadata(item), + ...buildStableRankingMetadata(item, index), + })) .sort((a, b) => { - if ( - b.rankingScore !== - a.rankingScore - ) { - return ( - b.rankingScore - - a.rankingScore - ); + if (b.rankingScore !== a.rankingScore) { + return b.rankingScore - a.rankingScore; } - const aTime = - Date.parse( - a.createdAt || "" - ) || 0; - - const bTime = - Date.parse( - b.createdAt || "" - ) || 0; + const timeA = Date.parse(a.createdAt || "") || 0; + const timeB = Date.parse(b.createdAt || "") || 0; - if (bTime !== aTime) { - return bTime - aTime; - } + if (timeB !== timeA) return timeB - timeA; - return ( - a.insertionOrder - - b.insertionOrder - ); + return a.insertionOrder - b.insertionOrder; }); } +/* ------------------------------ CONTROLLERS ------------------------------ */ + export const getFeedItems = async (req, res) => { const page = Number(req.query.page || 1); const limit = Number(req.query.limit || 20); const offset = (page - 1) * limit; try { - const [ - { data: tasks, error: tasksError }, - { data: messages, error: messagesError }, - ] = await Promise.all([ + const [{ data: tasks }, { data: messages }] = await Promise.all([ supabase .from("tasks") - .select( - "id,title,description,status,created_at" - ) - .order("created_at", { - ascending: false, - }) + .select("id,title,description,status,created_at") + .order("created_at", { ascending: false }) .limit(50), supabase .from("messages") - .select( - "id,username,text,created_at" - ) - .order("created_at", { - ascending: false, - }) + .select("id,username,text,created_at") + .order("created_at", { ascending: false }) .limit(50), ]); - if (tasksError) throw tasksError; - if (messagesError) throw messagesError; - - const aggregatedItems = - sortFeedItems( - buildFeedItems( - tasks, - messages - ) - ); - - const refreshCycleMetadata = { - refreshEvaluatedAt: - Date.now(), - stableOrdering: - true, - rapidInsertionSafe: - true, - }; - - const feedMetadata = - buildFeedMetadata( - aggregatedItems - ); - - const items = aggregatedItems.slice( - offset, - offset + limit + const aggregatedItems = sortFeedItems( + buildFeedItems(tasks || [], messages || []) ); + const paginatedItems = aggregatedItems.slice(offset, offset + limit); + res.status(200).json({ - metadata: { - ...feedMetadata, - ...refreshCycleMetadata, - }, - items, + metadata: buildFeedMetadata(aggregatedItems), + items: paginatedItems, pagination: { page, limit, total: aggregatedItems.length, - hasMore: - offset + limit < - aggregatedItems.length, + hasMore: offset + limit < aggregatedItems.length, }, }); } catch (error) { - console.error( - "Error loading feed:", - error - ); + console.error("Error loading feed:", error); res.status(500).json({ error: "Failed to load activity feed", @@ -354,15 +234,8 @@ export const getFeedItems = async (req, res) => { } }; -export const createFeedItem = async ( - req, - res -) => { - const { - title, - body, - type = "discussion", - } = req.body || {}; +export const createFeedItem = async (req, res) => { + const { title, body, type = "discussion" } = req.body || {}; if (!title || !body) { return res.status(400).json({ @@ -383,9 +256,7 @@ export const createFeedItem = async ( manualItems.unshift(item); - req.app - .get("io") - ?.emit("feed-created", item); + req.app.get("io")?.emit("feed-created", item); res.status(201).json({ item }); }; \ No newline at end of file diff --git a/frontend/app/components/kanban/KanbanBoard.tsx b/frontend/app/components/kanban/KanbanBoard.tsx index a23a94b..5c4ca45 100644 --- a/frontend/app/components/kanban/KanbanBoard.tsx +++ b/frontend/app/components/kanban/KanbanBoard.tsx @@ -733,4 +733,4 @@ export function KanbanBoard() { ); -} +} \ No newline at end of file diff --git a/frontend/app/components/kanban/KanbanColumn.tsx b/frontend/app/components/kanban/KanbanColumn.tsx index 28a9f5a..ff72aca 100644 --- a/frontend/app/components/kanban/KanbanColumn.tsx +++ b/frontend/app/components/kanban/KanbanColumn.tsx @@ -73,4 +73,4 @@ export function KanbanColumn({ column, tasks, onCreateTask, onDeleteTask, onEdit ); -} +} \ No newline at end of file