diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx index be41d2e..60f7ecd 100644 --- a/Frontend/src/App.tsx +++ b/Frontend/src/App.tsx @@ -4,6 +4,7 @@ import HomePage from "../src/pages/HomePage"; import DashboardPage from "../src/pages/DashboardPage"; import SponsorshipsPage from "../src/pages/Sponsorships"; import CollaborationsPage from "../src/pages/Collaborations"; +import CollaborationDetails from "../src/pages/CollaborationDetails"; import MessagesPage from "../src/pages/Messages"; import LoginPage from "./pages/Login"; import SignupPage from "./pages/Signup"; @@ -100,6 +101,14 @@ function App() { } /> + + + + } + /> = { + "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); + if (isNaN(s.getTime()) || isNaN(e.getTime())) return 0; + const diff = e.getTime() - s.getTime(); + if (diff < 0) return 0; + return Math.ceil(diff / (1000 * 60 * 60 * 24)); +} + +function getDaysLeft(due: string) { + const now = new Date(); + const d = new Date(due); + if (isNaN(d.getTime())) return 0; + const diff = d.getTime() - now.getTime(); + // Allow negative for overdue, but if invalid, return 0 + return Math.ceil(diff / (1000 * 60 * 60 * 24)); +} + +function getTimelineProgress(start: string, due: string) { + const total = getDaysBetween(start, due); + if (total === 0) return 0; + const elapsed = getDaysBetween(start, new Date().toISOString().slice(0, 10)); + return Math.min(100, Math.max(0, Math.round((elapsed / total) * 100))); +} + +const ActiveCollabCard: React.FC = ({ + id, + collaborator, + collabTitle, + status, + startDate, + dueDate, + messages, + deliverables, + lastActivity, + latestUpdate +}) => { + const navigate = useNavigate(); + 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 ( +
+
+ + + {collaborator.name.slice(0,2).toUpperCase()} + +
+
{collaborator.name}
+
{collaborator.contentType}
+
+ {status} +
+
+ Collab: {collabTitle} + Start: {startDate} + Due: {dueDate} + {overdue ? `Overdue by ${Math.abs(daysLeft)} days` : daysLeft === 0 ? "Due today" : `${daysLeft} days left`} +
+ {/* Timeline Progress Bar */} +
+
+ Timeline + {timelineProgress}% +
+
+
+
+
+ {/* Deliverables Progress Bar */} +
+
+ Deliverables + {deliverables.completed}/{deliverables.total} ({deliverableProgress}%) +
+
+
+
+
+
+ Messages: {messages} + Last activity: {lastActivity} +
+
+ Latest update: {latestUpdate} +
+
+ + + {status !== "Completed" && ( + + )} +
+
+ ); +}; + +export default ActiveCollabCard; \ No newline at end of file diff --git a/Frontend/src/components/collaboration-hub/ActiveCollabsGrid.tsx b/Frontend/src/components/collaboration-hub/ActiveCollabsGrid.tsx new file mode 100644 index 0000000..bbad3e4 --- /dev/null +++ b/Frontend/src/components/collaboration-hub/ActiveCollabsGrid.tsx @@ -0,0 +1,73 @@ +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 ( +
+
+
+ + +
+
+ + +
+
+ {filtered.length === 0 ? ( +
+
No active collaborations
+
Start a new collaboration to see it here!
+
+ ) : ( +
+ {filtered.map(collab => ( + + ))} +
+ )} +
+ ); +}; + +export default ActiveCollabsGrid; \ No newline at end of file diff --git a/Frontend/src/components/collaboration-hub/CollabRequests.tsx b/Frontend/src/components/collaboration-hub/CollabRequests.tsx new file mode 100644 index 0000000..57cfb2c --- /dev/null +++ b/Frontend/src/components/collaboration-hub/CollabRequests.tsx @@ -0,0 +1,207 @@ +import React, { useState } from "react"; +import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "../ui/card"; +import { Button } from "../ui/button"; +import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; +import { Badge } from "../ui/badge"; +import { Separator } from "../ui/separator"; +import { MessageSquare, CheckCircle, XCircle, Lightbulb, TrendingUp, Users, Star, Mail } from "lucide-react"; + +// Mock data for incoming requests +const mockRequests = [ + { + id: 1, + sender: { + name: "TechSavvy", + avatar: "https://randomuser.me/api/portraits/men/32.jpg", + contentNiche: "Tech Reviews", + audienceSize: "250K", + followers: 250000, + engagement: "4.2%", + rating: 4.7, + }, + summary: "Collaboration for a new smartphone launch campaign.", + proposal: { + contentLength: "5-7 min video", + paymentSchedule: "50% upfront, 50% after delivery", + numberOfPosts: "2 Instagram posts, 1 YouTube video", + timeline: "Within 3 weeks of product launch", + notes: "Open to creative input and additional deliverables." + }, + stats: { + posts: 120, + completionRate: "98%", + avgViews: "80K" + }, + ai: { + advantages: [ + "Access to a highly engaged tech audience.", + "Boosts brand credibility through trusted reviews.", + "Potential for long-term partnership." + ], + ideas: [ + "Unboxing and first impressions video.", + "Live Q&A session with audience.", + "Social media giveaway collaboration." + ], + recommendations: [ + "Highlight unique features in the first 60 seconds.", + "Leverage Instagram Stories for behind-the-scenes content.", + "Schedule a follow-up review after 1 month." + ] + } + }, + { + id: 2, + sender: { + name: "EcoChic", + avatar: "https://randomuser.me/api/portraits/women/44.jpg", + contentNiche: "Sustainable Fashion", + audienceSize: "180K", + followers: 180000, + engagement: "5.1%", + rating: 4.9, + }, + summary: "Proposal for a sustainable clothing line promotion.", + proposal: { + contentLength: "3-5 min Instagram Reel", + paymentSchedule: "Full payment after campaign", + numberOfPosts: "1 Reel, 2 Stories", + timeline: "Next month", + notes: "Would love to brainstorm eco-friendly angles together." + }, + stats: { + posts: 95, + completionRate: "96%", + avgViews: "60K" + }, + ai: { + advantages: [ + "Tap into eco-conscious audience.", + "Enhance brand's sustainability image.", + "Opportunity for co-branded content." + ], + ideas: [ + "Instagram Reels styling challenge.", + "Joint blog post on sustainable fashion tips.", + "Giveaway of eco-friendly products." + ], + recommendations: [ + "Feature behind-the-scenes of production.", + "Encourage user-generated content with a hashtag.", + "Host a live styling session." + ] + } + } +]; + +const CollabRequests: React.FC = () => { + const [requests, setRequests] = useState(mockRequests); + + const handleAccept = (id: number) => { + setRequests(prev => prev.filter(req => req.id !== id)); + // TODO: Integrate with backend to accept request + }; + + const handleDeny = (id: number) => { + setRequests(prev => prev.filter(req => req.id !== id)); + // TODO: Integrate with backend to deny request + }; + + const handleMessage = (id: number) => { + // TODO: Open message modal or redirect to chat + alert("Open chat with sender (not implemented)"); + }; + + return ( +
+ {requests.length === 0 ? ( +
No collaboration requests at this time.
+ ) : ( + requests.map((req) => ( + + +
+ + + {req.sender.name.slice(0,2).toUpperCase()} + +
+ {req.sender.name} + {req.sender.contentNiche} • {req.sender.audienceSize} audience +
+ {req.sender.followers.toLocaleString()} followers + {req.sender.engagement} engagement + {req.sender.rating}/5 +
+
+
+
+ +
+ Request: {req.summary} +
+ {/* Initial Collaboration Proposal Section */} +
+
+ 📝 Initial Collaboration Proposal +
+
    +
  • Content Length: {req.proposal.contentLength}
  • +
  • Payment Schedule: {req.proposal.paymentSchedule}
  • +
  • Number of Posts: {req.proposal.numberOfPosts}
  • +
  • Timeline: {req.proposal.timeline}
  • +
  • Notes: {req.proposal.notes}
  • +
+
+
+ {/* AI Advantages */} +
+
Advantages
+
    + {req.ai.advantages.map((adv, i) =>
  • {adv}
  • )} +
+
+ {/* AI Ideas */} +
+
Collaboration Ideas
+
    + {req.ai.ideas.map((idea, i) =>
  • {idea}
  • )} +
+
+ {/* AI Recommendations */} +
+
Recommendations
+
    + {req.ai.recommendations.map((rec, i) =>
  • {rec}
  • )} +
+
+
+ +
+ Posts: {req.stats.posts} + Completion Rate: {req.stats.completionRate} + Avg Views: {req.stats.avgViews} +
+
+ + + + +
+
+
+ )) + )} +
+ ); +}; + +export default CollabRequests; \ No newline at end of file diff --git a/Frontend/src/components/collaboration-hub/CollaborationDeliverables.tsx b/Frontend/src/components/collaboration-hub/CollaborationDeliverables.tsx new file mode 100644 index 0000000..c2e2291 --- /dev/null +++ b/Frontend/src/components/collaboration-hub/CollaborationDeliverables.tsx @@ -0,0 +1,76 @@ +import React from "react"; +import { Card, CardHeader, CardTitle, CardContent } from "../ui/card"; +import { Button } from "../ui/button"; +import { Badge } from "../ui/badge"; +import { FileText, Download, Eye, Edit } from "lucide-react"; + +const CollaborationDeliverables = ({ + mockDeliverables, + getDeliverableStatusColor, + handleViewDeliverable, +}) => ( + + + + + Deliverables + + + +
+ {mockDeliverables.map((deliverable) => ( +
+
+
+

{deliverable.title}

+

{deliverable.description}

+
+ + {deliverable.status.replace('-', ' ')} + +
+
+
+ Due Date: + {deliverable.dueDate} +
+
+ Assigned To: + {deliverable.assignedTo} +
+
+ {deliverable.files && deliverable.files.length > 0 && ( +
+ Files: +
+ {deliverable.files.map((file, index) => ( + + ))} +
+
+ )} +
+ + +
+
+ ))} +
+
+
+); + +export default CollaborationDeliverables; \ No newline at end of file diff --git a/Frontend/src/components/collaboration-hub/CollaborationMessages.tsx b/Frontend/src/components/collaboration-hub/CollaborationMessages.tsx new file mode 100644 index 0000000..27633f3 --- /dev/null +++ b/Frontend/src/components/collaboration-hub/CollaborationMessages.tsx @@ -0,0 +1,102 @@ +import React from "react"; +import { Card, CardHeader, CardTitle, CardContent } from "../ui/card"; +import { Button } from "../ui/button"; +import { Separator } from "../ui/separator"; +import { MessageSquare, Send } from "lucide-react"; +import { Input } from "../ui/input"; +import { Textarea } from "../ui/textarea"; + +const CollaborationMessages = ({ + mockMessages, + messageStyles, + messageStyle, + showStyleOptions, + setShowStyleOptions, + newMessage, + setNewMessage, + handleSendMessage, + handleStyleChange, + customStyle, + setCustomStyle, + handleCustomStyle, +}) => ( + + + + + Messages + + + +
+ {mockMessages.map((message) => ( +
+
+
{message.sender}
+
{message.content}
+
{message.timestamp}
+
+
+ ))} +
+ + {/* AI Message Style Enhancement */} +
+
+

Message Style

+ +
+ {showStyleOptions && ( +
+
+ {messageStyles.map((style) => ( + + ))} +
+
+ setCustomStyle(e.target.value)} + className="flex-1 text-xs" + onKeyPress={(e) => e.key === 'Enter' && handleCustomStyle()} + /> + +
+
+ )} +
+
+