Skip to content

Commit 43be1d1

Browse files
authored
feat(ui): Collaboration Hub (#98)
* feat(collaboration-hub): implement AI matches and general collaboration dashboard UI (frontend) * feat(active-collabs): add UI for active collaborations with progress bars, timeline, and latest update (frontend) * feat: added the ui of the view details view of the active collabs card(frontend only) * feat: added the UI of the requests tab in the collaboration hub (Frontend only) * feat(ui): add comprehensive accessibility features to Dialog component(resolving coderabbit suggestions). * a11y: Add accessibility labels and ARIA attributes to collaboration-hub grid and card components - Added <label> and aria-label to status and sort selects in ActiveCollabsGrid - Added aria-labels and focus styles to action buttons in ActiveCollabCard - Confirmed ConnectModal and CreatorMatchCard have required ARIA attributes and subcomponents for accessibility * ally: improved accessibility * ally: improved accessibility
1 parent a561306 commit 43be1d1

27 files changed

+3589
-228
lines changed

Frontend/src/App.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import HomePage from "../src/pages/HomePage";
44
import DashboardPage from "../src/pages/DashboardPage";
55
import SponsorshipsPage from "../src/pages/Sponsorships";
66
import CollaborationsPage from "../src/pages/Collaborations";
7+
import CollaborationDetails from "../src/pages/CollaborationDetails";
78
import MessagesPage from "../src/pages/Messages";
89
import LoginPage from "./pages/Login";
910
import SignupPage from "./pages/Signup";
@@ -100,6 +101,14 @@ function App() {
100101
</ProtectedRoute>
101102
}
102103
/>
104+
<Route
105+
path="/dashboard/collaborations/:id"
106+
element={
107+
<ProtectedRoute>
108+
<CollaborationDetails />
109+
</ProtectedRoute>
110+
}
111+
/>
103112
<Route
104113
path="/dashboard/messages"
105114
element={
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import React from "react";
2+
import { useNavigate } from "react-router-dom";
3+
import { Button } from "../ui/button";
4+
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
5+
6+
export interface ActiveCollabCardProps {
7+
id: number;
8+
collaborator: {
9+
name: string;
10+
avatar: string;
11+
contentType: string;
12+
};
13+
collabTitle: string;
14+
status: string;
15+
startDate: string;
16+
dueDate: string;
17+
messages: number;
18+
deliverables: { completed: number; total: number };
19+
lastActivity: string;
20+
latestUpdate: string;
21+
}
22+
23+
const statusColors: Record<string, string> = {
24+
"In Progress": "bg-blue-100 text-blue-700",
25+
"Awaiting Response": "bg-yellow-100 text-yellow-700",
26+
"Completed": "bg-green-100 text-green-700"
27+
};
28+
29+
function getDaysBetween(start: string, end: string) {
30+
const s = new Date(start);
31+
const e = new Date(end);
32+
if (isNaN(s.getTime()) || isNaN(e.getTime())) return 0;
33+
const diff = e.getTime() - s.getTime();
34+
if (diff < 0) return 0;
35+
return Math.ceil(diff / (1000 * 60 * 60 * 24));
36+
}
37+
38+
function getDaysLeft(due: string) {
39+
const now = new Date();
40+
const d = new Date(due);
41+
if (isNaN(d.getTime())) return 0;
42+
const diff = d.getTime() - now.getTime();
43+
// Allow negative for overdue, but if invalid, return 0
44+
return Math.ceil(diff / (1000 * 60 * 60 * 24));
45+
}
46+
47+
function getTimelineProgress(start: string, due: string) {
48+
const total = getDaysBetween(start, due);
49+
if (total === 0) return 0;
50+
const elapsed = getDaysBetween(start, new Date().toISOString().slice(0, 10));
51+
return Math.min(100, Math.max(0, Math.round((elapsed / total) * 100)));
52+
}
53+
54+
const ActiveCollabCard: React.FC<ActiveCollabCardProps> = ({
55+
id,
56+
collaborator,
57+
collabTitle,
58+
status,
59+
startDate,
60+
dueDate,
61+
messages,
62+
deliverables,
63+
lastActivity,
64+
latestUpdate
65+
}) => {
66+
const navigate = useNavigate();
67+
const deliverableProgress = Math.round((deliverables.completed / deliverables.total) * 100);
68+
const timelineProgress = getTimelineProgress(startDate, dueDate);
69+
const daysLeft = getDaysLeft(dueDate);
70+
const overdue = daysLeft < 0 && status !== "Completed";
71+
72+
return (
73+
<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">
74+
<div className="flex items-center gap-4">
75+
<Avatar className="h-12 w-12">
76+
<AvatarImage src={collaborator.avatar} alt={collaborator.name} />
77+
<AvatarFallback className="bg-gray-200">{collaborator.name.slice(0,2).toUpperCase()}</AvatarFallback>
78+
</Avatar>
79+
<div className="flex-1">
80+
<div className="font-semibold text-lg text-gray-900">{collaborator.name}</div>
81+
<div className="text-xs text-gray-500">{collaborator.contentType}</div>
82+
</div>
83+
<span className={`px-3 py-1 rounded-full text-xs font-semibold ${statusColors[status] || "bg-gray-100 text-gray-700"}`}>{status}</span>
84+
</div>
85+
<div className="flex flex-wrap items-center gap-2 text-sm text-gray-700">
86+
<span className="font-semibold">Collab:</span> {collabTitle}
87+
<span className="ml-4 font-semibold">Start:</span> {startDate}
88+
<span className="ml-4 font-semibold">Due:</span> <span className={overdue ? "text-red-600 font-bold" : ""}>{dueDate}</span>
89+
<span className="ml-4 font-semibold">{overdue ? `Overdue by ${Math.abs(daysLeft)} days` : daysLeft === 0 ? "Due today" : `${daysLeft} days left`}</span>
90+
</div>
91+
{/* Timeline Progress Bar */}
92+
<div className="w-full flex flex-col gap-1">
93+
<div className="flex justify-between text-xs text-gray-500">
94+
<span>Timeline</span>
95+
<span>{timelineProgress}%</span>
96+
</div>
97+
<div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
98+
<div className="h-2 rounded-full bg-blue-400" style={{ width: `${timelineProgress}%` }} />
99+
</div>
100+
</div>
101+
{/* Deliverables Progress Bar */}
102+
<div className="w-full flex flex-col gap-1">
103+
<div className="flex justify-between text-xs text-gray-500">
104+
<span>Deliverables</span>
105+
<span>{deliverables.completed}/{deliverables.total} ({deliverableProgress}%)</span>
106+
</div>
107+
<div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
108+
<div className="h-2 rounded-full bg-green-400" style={{ width: `${deliverableProgress}%` }} />
109+
</div>
110+
</div>
111+
<div className="flex flex-wrap items-center gap-4 text-xs text-gray-600">
112+
<span>Messages: <span className="font-semibold text-gray-900">{messages}</span></span>
113+
<span>Last activity: <span className="font-semibold text-gray-900">{lastActivity}</span></span>
114+
</div>
115+
<div className="text-xs text-gray-700 italic bg-gray-50 rounded px-3 py-2 border border-gray-100">
116+
<span className="font-semibold text-gray-800">Latest update:</span> {latestUpdate}
117+
</div>
118+
<div className="flex gap-2 mt-2">
119+
<Button
120+
className="bg-gray-100 text-gray-900 hover:bg-gray-200 font-semibold rounded-full py-2 focus:outline-none focus:ring-2 focus:ring-blue-400"
121+
variant="secondary"
122+
onClick={() => navigate(`/dashboard/collaborations/${id}`)}
123+
aria-label="View collaboration details"
124+
>
125+
View Details
126+
</Button>
127+
<Button
128+
className="bg-blue-100 text-blue-700 hover:bg-blue-200 font-semibold rounded-full py-2 focus:outline-none focus:ring-2 focus:ring-blue-400"
129+
aria-label="Send message to collaborator"
130+
>
131+
Message
132+
</Button>
133+
{status !== "Completed" && (
134+
<Button
135+
className="bg-green-100 text-green-700 hover:bg-green-200 font-semibold rounded-full py-2 focus:outline-none focus:ring-2 focus:ring-green-400"
136+
aria-label="Mark collaboration as complete"
137+
>
138+
Mark Complete
139+
</Button>
140+
)}
141+
</div>
142+
</div>
143+
);
144+
};
145+
146+
export default ActiveCollabCard;
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import React, { useState } from "react";
2+
import { activeCollabsMock } from "./activeCollabsMockData";
3+
import ActiveCollabCard from "./ActiveCollabCard";
4+
5+
const statusOptions = ["All", "In Progress", "Completed"];
6+
const sortOptions = ["Start Date", "Due Date", "Name"];
7+
8+
const ActiveCollabsGrid: React.FC = () => {
9+
const [statusFilter, setStatusFilter] = useState("All");
10+
const [sortBy, setSortBy] = useState("Start Date");
11+
12+
// Only show In Progress and Completed
13+
let filtered = activeCollabsMock.filter(c => c.status !== "Awaiting Response");
14+
if (statusFilter !== "All") {
15+
filtered = filtered.filter(c => c.status === statusFilter);
16+
}
17+
if (sortBy === "Start Date") {
18+
filtered = [...filtered].sort((a, b) => a.startDate.localeCompare(b.startDate));
19+
} else if (sortBy === "Due Date") {
20+
filtered = [...filtered].sort((a, b) => a.dueDate.localeCompare(b.dueDate));
21+
} else if (sortBy === "Name") {
22+
filtered = [...filtered].sort((a, b) => a.collaborator.name.localeCompare(b.collaborator.name));
23+
}
24+
25+
return (
26+
<div className="w-full max-w-4xl mx-auto">
27+
<div className="flex flex-wrap items-center justify-between gap-4 mb-6">
28+
<div className="flex gap-2 items-center">
29+
<label htmlFor="statusFilter" className="font-semibold text-gray-700">Status:</label>
30+
<select
31+
id="statusFilter"
32+
className="border rounded px-2 py-1 text-sm"
33+
value={statusFilter}
34+
onChange={e => setStatusFilter(e.target.value)}
35+
aria-label="Filter collaborations by status"
36+
>
37+
{statusOptions.map(opt => (
38+
<option key={opt} value={opt}>{opt}</option>
39+
))}
40+
</select>
41+
</div>
42+
<div className="flex gap-2 items-center">
43+
<label htmlFor="sortBy" className="font-semibold text-gray-700">Sort by:</label>
44+
<select
45+
id="sortBy"
46+
className="border rounded px-2 py-1 text-sm"
47+
value={sortBy}
48+
onChange={e => setSortBy(e.target.value)}
49+
aria-label="Sort collaborations by criteria"
50+
>
51+
{sortOptions.map(opt => (
52+
<option key={opt} value={opt}>{opt}</option>
53+
))}
54+
</select>
55+
</div>
56+
</div>
57+
{filtered.length === 0 ? (
58+
<div className="text-center text-gray-400 py-16">
59+
<div className="text-2xl mb-2">No active collaborations</div>
60+
<div className="text-sm">Start a new collaboration to see it here!</div>
61+
</div>
62+
) : (
63+
<div className="flex flex-col gap-6">
64+
{filtered.map(collab => (
65+
<ActiveCollabCard key={collab.id} {...collab} />
66+
))}
67+
</div>
68+
)}
69+
</div>
70+
);
71+
};
72+
73+
export default ActiveCollabsGrid;

0 commit comments

Comments
 (0)