Skip to content
118 changes: 118 additions & 0 deletions Frontend/src/components/collaboration-hub/ActiveCollabCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import React from "react";
import { Button } from "../ui/button";
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";

export interface ActiveCollabCardProps {
collaborator: {
name: string;
avatar: string;
contentType: string;
};
collabTitle: string;
status: string;
startDate: string;
dueDate: string;
messages: number;
deliverables: { completed: number; total: number };
lastActivity: string;
latestUpdate: string;
}

const statusColors: Record<string, string> = {
"In Progress": "bg-blue-100 text-blue-700",
"Awaiting Response": "bg-yellow-100 text-yellow-700",
"Completed": "bg-green-100 text-green-700"
};

function getDaysBetween(start: string, end: string) {
const s = new Date(start);
const e = new Date(end);
return Math.ceil((e.getTime() - s.getTime()) / (1000 * 60 * 60 * 24));
}

function getDaysLeft(due: string) {
const now = new Date();
const d = new Date(due);
return Math.ceil((d.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
}

function getTimelineProgress(start: string, due: string) {
const total = getDaysBetween(start, due);
const elapsed = getDaysBetween(start, new Date().toISOString().slice(0, 10));
return Math.min(100, Math.max(0, Math.round((elapsed / total) * 100)));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const ActiveCollabCard: React.FC<ActiveCollabCardProps> = ({
collaborator,
collabTitle,
status,
startDate,
dueDate,
messages,
deliverables,
lastActivity,
latestUpdate
}) => {
const deliverableProgress = Math.round((deliverables.completed / deliverables.total) * 100);
const timelineProgress = getTimelineProgress(startDate, dueDate);
const daysLeft = getDaysLeft(dueDate);
const overdue = daysLeft < 0 && status !== "Completed";

return (
<div className="bg-white rounded-xl shadow p-5 flex flex-col gap-3 border border-gray-100 w-full max-w-xl mx-auto">
<div className="flex items-center gap-4">
<Avatar className="h-12 w-12">
<AvatarImage src={collaborator.avatar} alt={collaborator.name} />
<AvatarFallback className="bg-gray-200">{collaborator.name.slice(0,2).toUpperCase()}</AvatarFallback>
</Avatar>
<div className="flex-1">
<div className="font-semibold text-lg text-gray-900">{collaborator.name}</div>
<div className="text-xs text-gray-500">{collaborator.contentType}</div>
</div>
<span className={`px-3 py-1 rounded-full text-xs font-semibold ${statusColors[status] || "bg-gray-100 text-gray-700"}`}>{status}</span>
</div>
<div className="flex flex-wrap items-center gap-2 text-sm text-gray-700">
<span className="font-semibold">Collab:</span> {collabTitle}
<span className="ml-4 font-semibold">Start:</span> {startDate}
<span className="ml-4 font-semibold">Due:</span> <span className={overdue ? "text-red-600 font-bold" : ""}>{dueDate}</span>
<span className="ml-4 font-semibold">{overdue ? `Overdue by ${Math.abs(daysLeft)} days` : daysLeft === 0 ? "Due today" : `${daysLeft} days left`}</span>
</div>
{/* Timeline Progress Bar */}
<div className="w-full flex flex-col gap-1">
<div className="flex justify-between text-xs text-gray-500">
<span>Timeline</span>
<span>{timelineProgress}%</span>
</div>
<div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
<div className="h-2 rounded-full bg-blue-400" style={{ width: `${timelineProgress}%` }} />
</div>
</div>
{/* Deliverables Progress Bar */}
<div className="w-full flex flex-col gap-1">
<div className="flex justify-between text-xs text-gray-500">
<span>Deliverables</span>
<span>{deliverables.completed}/{deliverables.total} ({deliverableProgress}%)</span>
</div>
<div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
<div className="h-2 rounded-full bg-green-400" style={{ width: `${deliverableProgress}%` }} />
</div>
</div>
<div className="flex flex-wrap items-center gap-4 text-xs text-gray-600">
<span>Messages: <span className="font-semibold text-gray-900">{messages}</span></span>
<span>Last activity: <span className="font-semibold text-gray-900">{lastActivity}</span></span>
</div>
<div className="text-xs text-gray-700 italic bg-gray-50 rounded px-3 py-2 border border-gray-100">
<span className="font-semibold text-gray-800">Latest update:</span> {latestUpdate}
</div>
<div className="flex gap-2 mt-2">
<Button className="bg-gray-100 text-gray-900 hover:bg-gray-200 font-semibold rounded-full py-2" variant="secondary">View Details</Button>
<Button className="bg-blue-100 text-blue-700 hover:bg-blue-200 font-semibold rounded-full py-2">Message</Button>
{status !== "Completed" && (
<Button className="bg-green-100 text-green-700 hover:bg-green-200 font-semibold rounded-full py-2">Mark Complete</Button>
)}
</div>
Comment thread
Saahi30 marked this conversation as resolved.
</div>
);
};

export default ActiveCollabCard;
69 changes: 69 additions & 0 deletions Frontend/src/components/collaboration-hub/ActiveCollabsGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React, { useState } from "react";
import { activeCollabsMock } from "./activeCollabsMockData";
import ActiveCollabCard from "./ActiveCollabCard";

const statusOptions = ["All", "In Progress", "Completed"];
const sortOptions = ["Start Date", "Due Date", "Name"];

const ActiveCollabsGrid: React.FC = () => {
const [statusFilter, setStatusFilter] = useState("All");
const [sortBy, setSortBy] = useState("Start Date");

// Only show In Progress and Completed
let filtered = activeCollabsMock.filter(c => c.status !== "Awaiting Response");
if (statusFilter !== "All") {
filtered = filtered.filter(c => c.status === statusFilter);
}
if (sortBy === "Start Date") {
filtered = [...filtered].sort((a, b) => a.startDate.localeCompare(b.startDate));
} else if (sortBy === "Due Date") {
filtered = [...filtered].sort((a, b) => a.dueDate.localeCompare(b.dueDate));
} else if (sortBy === "Name") {
filtered = [...filtered].sort((a, b) => a.collaborator.name.localeCompare(b.collaborator.name));
}

return (
<div className="w-full max-w-4xl mx-auto">
<div className="flex flex-wrap items-center justify-between gap-4 mb-6">
<div className="flex gap-2 items-center">
<span className="font-semibold text-gray-700">Status:</span>
<select
className="border rounded px-2 py-1 text-sm"
value={statusFilter}
onChange={e => setStatusFilter(e.target.value)}
>
{statusOptions.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</div>
<div className="flex gap-2 items-center">
<span className="font-semibold text-gray-700">Sort by:</span>
<select
className="border rounded px-2 py-1 text-sm"
value={sortBy}
onChange={e => setSortBy(e.target.value)}
>
{sortOptions.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</div>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
{filtered.length === 0 ? (
<div className="text-center text-gray-400 py-16">
<div className="text-2xl mb-2">No active collaborations</div>
<div className="text-sm">Start a new collaboration to see it here!</div>
</div>
) : (
<div className="flex flex-col gap-6">
{filtered.map(collab => (
<ActiveCollabCard key={collab.id} {...collab} />
))}
</div>
)}
</div>
);
};

export default ActiveCollabsGrid;
113 changes: 113 additions & 0 deletions Frontend/src/components/collaboration-hub/ConnectModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React, { useState } from "react";
import { mockProfileDetails, mockCollabIdeas, mockRequestTexts, mockWhyMatch } from "./mockProfileData";
Comment on lines +1 to +2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add missing useEffect import for keyboard accessibility.

The component needs useEffect import to implement escape key handling for better accessibility, which was suggested in previous reviews but appears to be missing from the current implementation.

-import React, { useState } from "react";
+import React, { useState, useEffect } from "react";

Then add escape key handling after line 25:

  const [selectedText, setSelectedText] = useState(mockRequestTexts[0]);
+  
+  useEffect(() => {
+    const handleEscape = (e: KeyboardEvent) => {
+      if (e.key === 'Escape') {
+        onClose();
+      }
+    };
+    if (open) {
+      document.addEventListener('keydown', handleEscape);
+      return () => document.removeEventListener('keydown', handleEscape);
+    }
+  }, [open, onClose]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import React, { useState } from "react";
import { mockProfileDetails, mockCollabIdeas, mockRequestTexts, mockWhyMatch } from "./mockProfileData";
import React, { useState, useEffect } from "react";
import { mockProfileDetails, mockCollabIdeas, mockRequestTexts, mockWhyMatch } from "./mockProfileData";
function ConnectModal({ open, onClose, /* …other props */ }) {
const [selectedText, setSelectedText] = useState(mockRequestTexts[0]);
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
};
if (open) {
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}
}, [open, onClose]);
// …rest of component
}
🤖 Prompt for AI Agents
In Frontend/src/components/collaboration-hub/ConnectModal.tsx at lines 1-2, the
React import statement is missing the useEffect hook, which is necessary for
implementing escape key handling for accessibility. Add useEffect to the import
from React. Then, after line 25, implement an effect that adds a keydown event
listener to detect the Escape key and triggers the appropriate handler to close
the modal, ensuring to clean up the event listener on component unmount.

import { Button } from "../ui/button";
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
import { Badge } from "../ui/badge";

interface WhyMatchReason {
point: string;
description: string;
}

interface ConnectModalProps {
open: boolean;
onClose: () => void;
onSend: (selectedText: string) => void;
matchPercentage?: number;
whyMatch?: WhyMatchReason[];
}

const defaultMatch = 98;
const IDEAS_PER_PAGE = 3;

const ConnectModal: React.FC<ConnectModalProps> = ({ open, onClose, onSend, matchPercentage = defaultMatch, whyMatch = mockWhyMatch }) => {
const [ideasPage, setIdeasPage] = useState(0);
const [selectedText, setSelectedText] = useState(mockRequestTexts[0]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add defensive programming for mock data access.

The component assumes mockRequestTexts[0] exists, which could cause a runtime error if the array is empty.

-const [selectedText, setSelectedText] = useState(mockRequestTexts[0]);
+const [selectedText, setSelectedText] = useState(mockRequestTexts[0] || "");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [selectedText, setSelectedText] = useState(mockRequestTexts[0]);
const [selectedText, setSelectedText] = useState(mockRequestTexts[0] || "");
🤖 Prompt for AI Agents
In Frontend/src/components/collaboration-hub/ConnectModal.tsx at line 25, the
code assumes mockRequestTexts[0] exists, which can cause a runtime error if the
array is empty. Add a defensive check to ensure mockRequestTexts has at least
one element before accessing index 0; if empty, initialize selectedText with a
safe default value such as an empty string or null to prevent errors.

if (!open) return null;
const profile = mockProfileDetails;
const totalIdeas = mockCollabIdeas.length;
const startIdx = (ideasPage * IDEAS_PER_PAGE) % totalIdeas;
const ideasToShow = Array.from({ length: IDEAS_PER_PAGE }, (_, i) => mockCollabIdeas[(startIdx + i) % totalIdeas]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

const handleNextIdeas = () => {
setIdeasPage((prev) => prev + 1);
};

return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-20">
<div className="bg-white rounded-2xl shadow-xl w-auto min-w-[320px] max-w-full mx-2 p-0 sm:p-0 relative flex flex-col sm:flex-row overflow-hidden">
<button className="absolute top-4 right-4 text-gray-400 hover:text-gray-700 z-10" onClick={onClose} aria-label="Close">
×
</button>
Comment thread
Saahi30 marked this conversation as resolved.
{/* Left: Profile Info */}
<div className="flex flex-col items-center sm:items-start bg-gray-50 sm:bg-white p-6 sm:p-8 w-full sm:w-auto border-b sm:border-b-0 sm:border-r border-gray-100">
<Badge className="bg-yellow-100 text-yellow-700 text-sm font-semibold px-3 py-1 mb-2">
{matchPercentage}% Match
</Badge>
<Avatar className="h-16 w-16 mb-2">
<AvatarImage src={profile.avatar} alt={profile.name} />
<AvatarFallback className="bg-gray-200">{profile.name.slice(0,2).toUpperCase()}</AvatarFallback>
</Avatar>
<h2 className="text-xl font-bold text-gray-900 text-center sm:text-left">Connect with {profile.name}</h2>
<div className="text-gray-500 text-sm text-center sm:text-left mb-2">{profile.contentType}</div>
<div className="mb-3 bg-gray-50 border border-gray-100 rounded-lg p-3 w-full">
<div className="font-semibold text-sm mb-1">Why you match</div>
<ul className="space-y-3">
{whyMatch.map((reason, idx) => (
<li key={idx}>
<div className="font-semibold text-xs text-gray-800 mb-1">{reason.point}</div>
<div className="text-xs text-gray-600 pl-2">{reason.description}</div>
</li>
))}
</ul>
</div>
</div>
{/* Right: Ideas and Messages */}
<div className="flex-1 flex flex-col p-6 sm:p-8 w-full sm:w-auto">
<div className="mb-4">
<div className="font-semibold text-sm mb-2">AI-Generated Collaboration Ideas</div>
<ul className="space-y-2">
{ideasToShow.map((idea, idx) => (
<li key={idx} className="bg-gray-50 rounded-lg p-3 border border-gray-100">
<div className="font-semibold text-gray-800">{idea.title}</div>
<div className="text-xs text-gray-600">{idea.description}</div>
</li>
))}
</ul>
{totalIdeas > IDEAS_PER_PAGE && (
<div className="flex justify-center mt-3">
<Button className="bg-yellow-100 text-yellow-800 hover:bg-yellow-200 font-semibold rounded-full px-6 py-2 text-sm" onClick={handleNextIdeas}>
See More Ideas
</Button>
</div>
)}
</div>
<div className="mb-4">
<div className="font-semibold text-sm mb-2">Select a message to send</div>
<div className="space-y-2">
{mockRequestTexts.map((text, idx) => (
<label key={idx} className="flex items-start gap-2 cursor-pointer">
<input
type="radio"
name="requestText"
value={text}
checked={selectedText === text}
onChange={() => setSelectedText(text)}
className="mt-1 accent-yellow-400"
/>
<span className={`text-sm px-4 py-2 rounded-2xl inline-block ${selectedText === text ? 'bg-yellow-100 text-yellow-900 border border-yellow-300' : 'bg-gray-100 text-gray-700'}`}>{text}</span>
</label>
))}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
</div>
</div>
<div className="flex gap-2 w-full mt-auto justify-center pt-2 pb-2">
<Button className="bg-gray-100 text-gray-900 hover:bg-gray-200 flex-1 font-semibold rounded-full py-2" variant="secondary" onClick={onClose}>Cancel</Button>
<Button className="bg-yellow-400 text-white hover:bg-yellow-500 flex-1 font-semibold rounded-full py-2" onClick={() => onSend(selectedText)}>Send Request</Button>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add safety check before sending request.

The send button should be disabled when no message is selected to prevent sending empty requests.

-<Button className="bg-yellow-400 text-white hover:bg-yellow-500 flex-1 font-semibold rounded-full py-2" onClick={() => onSend(selectedText)}>Send Request</Button>
+<Button 
+  className="bg-yellow-400 text-white hover:bg-yellow-500 flex-1 font-semibold rounded-full py-2 disabled:opacity-50 disabled:cursor-not-allowed" 
+  onClick={() => onSend(selectedText)}
+  disabled={!selectedText.trim()}
+>
+  Send Request
+</Button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Button className="bg-yellow-400 text-white hover:bg-yellow-500 flex-1 font-semibold rounded-full py-2" onClick={() => onSend(selectedText)}>Send Request</Button>
<Button
className="bg-yellow-400 text-white hover:bg-yellow-500 flex-1 font-semibold rounded-full py-2 disabled:opacity-50 disabled:cursor-not-allowed"
onClick={() => onSend(selectedText)}
disabled={!selectedText.trim()}
>
Send Request
</Button>
🤖 Prompt for AI Agents
In Frontend/src/components/collaboration-hub/ConnectModal.tsx at line 138, the
Send Request button currently allows sending requests even when no message is
selected. Add a condition to disable the button when selectedText is empty or
undefined to prevent sending empty requests. Update the Button component's
disabled prop accordingly to reflect this safety check.

</div>
</div>
</div>
</div>
);
};

export default ConnectModal;
Loading