From 8bd0718b7219863769a0ba6a2f4df7a4cba2c5d6 Mon Sep 17 00:00:00 2001 From: Shanmuga Krishnan S M Date: Wed, 29 Jul 2026 08:27:46 +0530 Subject: [PATCH] feat: implement developer collaboration hub with telegram/discord interactive UI --- .../portal/controller/CollabController.java | 103 +++ .../rit/portal/entity/CollabApplication.java | 48 ++ .../com/rit/portal/entity/CollabRequest.java | 64 ++ .../CollabApplicationRepository.java | 9 + .../repository/CollabRequestRepository.java | 11 + src/App.tsx | 2 + src/components/Footer/Footer.tsx | 1 + src/constants/index.ts | 1 + src/pages/DevCollab/DevCollab.tsx | 771 ++++++++++++++++++ telegram-bot/README.md | 6 +- telegram-bot/bot_mappings.db | Bin 12288 -> 12288 bytes telegram-bot/config.json | 5 +- telegram-bot/telegram_bot.py | 733 ++++++++++++++--- 13 files changed, 1625 insertions(+), 129 deletions(-) create mode 100644 backend/src/main/java/com/rit/portal/controller/CollabController.java create mode 100644 backend/src/main/java/com/rit/portal/entity/CollabApplication.java create mode 100644 backend/src/main/java/com/rit/portal/entity/CollabRequest.java create mode 100644 backend/src/main/java/com/rit/portal/repository/CollabApplicationRepository.java create mode 100644 backend/src/main/java/com/rit/portal/repository/CollabRequestRepository.java create mode 100644 src/pages/DevCollab/DevCollab.tsx diff --git a/backend/src/main/java/com/rit/portal/controller/CollabController.java b/backend/src/main/java/com/rit/portal/controller/CollabController.java new file mode 100644 index 0000000..c0d1b00 --- /dev/null +++ b/backend/src/main/java/com/rit/portal/controller/CollabController.java @@ -0,0 +1,103 @@ +package com.rit.portal.controller; + +import com.rit.portal.entity.CollabApplication; +import com.rit.portal.entity.CollabRequest; +import com.rit.portal.repository.CollabApplicationRepository; +import com.rit.portal.repository.CollabRequestRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.RestTemplate; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/collab") +public class CollabController { + + @Autowired + private CollabRequestRepository collabRequestRepository; + + @Autowired + private CollabApplicationRepository collabApplicationRepository; + + private final RestTemplate restTemplate = new RestTemplate(); + private static final String BOT_COLLAB_APP_URL = "http://localhost:8082/send_collab_application"; + + @GetMapping + public List getAllCollabRequests() { + return collabRequestRepository.findAllByOrderByCreatedAtDesc(); + } + + @GetMapping("/{id}") + public ResponseEntity getCollabRequestById(@PathVariable Integer id) { + return collabRequestRepository.findById(id) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @PostMapping + public CollabRequest createCollabRequest(@RequestBody CollabRequest request) { + if (request.getStatus() == null) request.setStatus("OPEN"); + if (request.getApplicationsCount() == null) request.setApplicationsCount(0); + request.setCreatedAt(LocalDateTime.now()); + return collabRequestRepository.save(request); + } + + @PostMapping("/{id}/apply") + public ResponseEntity applyToCollabRequest( + @PathVariable Integer id, + @RequestBody CollabApplication application) { + return collabRequestRepository.findById(id).map(request -> { + application.setCollabRequest(request); + if (application.getStatus() == null) application.setStatus("PENDING"); + application.setCreatedAt(LocalDateTime.now()); + + CollabApplication savedApp = collabApplicationRepository.save(application); + + // Increment count on parent request + request.setApplicationsCount(request.getApplicationsCount() + 1); + collabRequestRepository.save(request); + + // Notify Telegram/Discord intermediary bot asynchronously + new Thread(() -> { + try { + Map payload = new HashMap<>(); + payload.put("collab_id", request.getId()); + payload.put("application_id", savedApp.getId()); + payload.put("project_idea", request.getProjectIdea()); + payload.put("tag", request.getTag()); + payload.put("author_name", request.getAuthorName()); + payload.put("contact_info", request.getContactInfo()); + payload.put("telegram_chat_id", request.getTelegramChatId()); + payload.put("discord_user_id", request.getDiscordUserId()); + payload.put("applicant_name", savedApp.getApplicantName()); + payload.put("applicant_dept", savedApp.getApplicantDept()); + payload.put("applicant_year", savedApp.getApplicantYear()); + payload.put("applicant_contact", savedApp.getApplicantContact()); + payload.put("message", savedApp.getMessage()); + + restTemplate.postForEntity(BOT_COLLAB_APP_URL, payload, String.class); + } catch (Exception e) { + System.err.println("Failed to send collab application notification to bot: " + e.getMessage()); + } + }).start(); + + return ResponseEntity.ok(savedApp); + }).orElse(ResponseEntity.notFound().build()); + } + + @PutMapping("/applications/{applicationId}/status") + public ResponseEntity updateApplicationStatus( + @PathVariable Integer applicationId, + @RequestParam String status) { + return collabApplicationRepository.findById(applicationId).map(app -> { + app.setStatus(status.toUpperCase()); + CollabApplication updated = collabApplicationRepository.save(app); + return ResponseEntity.ok(updated); + }).orElse(ResponseEntity.notFound().build()); + } +} diff --git a/backend/src/main/java/com/rit/portal/entity/CollabApplication.java b/backend/src/main/java/com/rit/portal/entity/CollabApplication.java new file mode 100644 index 0000000..c7a2d95 --- /dev/null +++ b/backend/src/main/java/com/rit/portal/entity/CollabApplication.java @@ -0,0 +1,48 @@ +package com.rit.portal.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "collab_applications") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CollabApplication { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "collab_request_id", nullable = false) + @JsonIgnore + private CollabRequest collabRequest; + + @Column(name = "applicant_name", nullable = false) + private String applicantName; + + @Column(name = "applicant_dept", nullable = false) + private String applicantDept; + + @Column(name = "applicant_year", nullable = false) + private String applicantYear; + + @Column(name = "applicant_contact", nullable = false) + private String applicantContact; + + @Column(columnDefinition = "TEXT") + private String message; + + @Column(name = "status") + @Builder.Default + private String status = "PENDING"; + + @Column(name = "created_at") + @Builder.Default + private LocalDateTime createdAt = LocalDateTime.now(); +} diff --git a/backend/src/main/java/com/rit/portal/entity/CollabRequest.java b/backend/src/main/java/com/rit/portal/entity/CollabRequest.java new file mode 100644 index 0000000..9733994 --- /dev/null +++ b/backend/src/main/java/com/rit/portal/entity/CollabRequest.java @@ -0,0 +1,64 @@ +package com.rit.portal.entity; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "collab_requests") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CollabRequest { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(name = "author_name", nullable = false) + private String authorName; + + @Column(nullable = false) + private String department; + + @Column(nullable = false) + private String year; + + @Column(name = "project_idea", nullable = false, columnDefinition = "TEXT") + private String projectIdea; + + @Column(name = "github_link") + private String githubLink; + + @Column(nullable = false) + private String tag; + + @Column(name = "contact_info") + private String contactInfo; + + @Column(name = "telegram_chat_id") + private Long telegramChatId; + + @Column(name = "discord_user_id") + private String discordUserId; + + @Column(name = "status") + @Builder.Default + private String status = "OPEN"; + + @Column(name = "applications_count") + @Builder.Default + private Integer applicationsCount = 0; + + @Column(name = "created_at") + @Builder.Default + private LocalDateTime createdAt = LocalDateTime.now(); + + @OneToMany(mappedBy = "collabRequest", cascade = CascadeType.ALL, fetch = FetchType.EAGER) + @Builder.Default + private List applications = new ArrayList<>(); +} diff --git a/backend/src/main/java/com/rit/portal/repository/CollabApplicationRepository.java b/backend/src/main/java/com/rit/portal/repository/CollabApplicationRepository.java new file mode 100644 index 0000000..f40b6ec --- /dev/null +++ b/backend/src/main/java/com/rit/portal/repository/CollabApplicationRepository.java @@ -0,0 +1,9 @@ +package com.rit.portal.repository; + +import com.rit.portal.entity.CollabApplication; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CollabApplicationRepository extends JpaRepository { +} diff --git a/backend/src/main/java/com/rit/portal/repository/CollabRequestRepository.java b/backend/src/main/java/com/rit/portal/repository/CollabRequestRepository.java new file mode 100644 index 0000000..c7dc321 --- /dev/null +++ b/backend/src/main/java/com/rit/portal/repository/CollabRequestRepository.java @@ -0,0 +1,11 @@ +package com.rit.portal.repository; + +import com.rit.portal.entity.CollabRequest; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.List; + +@Repository +public interface CollabRequestRepository extends JpaRepository { + List findAllByOrderByCreatedAtDesc(); +} diff --git a/src/App.tsx b/src/App.tsx index add132c..29e4597 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,7 @@ import Events from '@/pages/Events/Events'; import Community from '@/pages/Community/Community'; import BusRoutes from '@/pages/BusRoutes/BusRoutes'; import Faculty from '@/pages/Faculty/Faculty'; +import DevCollab from '@/pages/DevCollab/DevCollab'; const queryClient = new QueryClient({ defaultOptions: { @@ -45,6 +46,7 @@ function App() { } /> } /> } /> + } /> diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx index a8f9311..a34a171 100644 --- a/src/components/Footer/Footer.tsx +++ b/src/components/Footer/Footer.tsx @@ -10,6 +10,7 @@ const quickLinks = [ { label: 'Faculty Directory', path: '/faculty' }, { label: 'Events', path: '/events' }, { label: 'Community', path: '/community' }, + { label: 'Dev Collab', path: '/collab' }, ]; const resources = [ diff --git a/src/constants/index.ts b/src/constants/index.ts index 7a16da1..3a0167f 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -10,6 +10,7 @@ export const NAV_LINKS = [ { label: 'Faculty Directory', path: '/faculty' }, { label: 'Clubs', path: '/events' }, { label: 'Community', path: '/community' }, + { label: 'Dev Collab', path: '/collab' }, ]; // ─── Stats ──────────────────────────────────────────────────────────────────── diff --git a/src/pages/DevCollab/DevCollab.tsx b/src/pages/DevCollab/DevCollab.tsx new file mode 100644 index 0000000..0db84f2 --- /dev/null +++ b/src/pages/DevCollab/DevCollab.tsx @@ -0,0 +1,771 @@ +import { useState, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + Code2, Plus, GitBranch, Tag, User, Send, CheckCircle2, + Search, Filter, Sparkles, X, ExternalLink, Layers, + ChevronRight, MessageSquare, Clock, Users, ArrowUpRight +} from 'lucide-react'; +import { Link } from 'react-router-dom'; +import { getBackendUrl } from '@/lib/utils'; + +export interface CollabRequestItem { + id: number; + authorName: string; + department: string; + year: string; + projectIdea: string; + githubLink?: string; + tag: string; + contactInfo?: string; + telegramChatId?: number; + status: string; + applicationsCount: number; + createdAt: string; +} + +const TAG_OPTIONS = [ + 'looking for co-developing a project from scratch', + 'looking for beta testers', + 'looking for Open-source Collaborators/Contributers', +]; + +const DEPARTMENTS = ['CSE', 'CSBS', 'AIML', 'ECE', 'MECH', 'CIVIL', 'AI & DS', 'EEE', 'IT', 'Other']; +const YEARS = ['1st Year', '2nd Year', '3rd Year', '4th Year']; + +const INITIAL_MOCK_REQUESTS: CollabRequestItem[] = [ + { + id: 101, + authorName: 'Rohan Sharma', + department: 'CSE', + year: '3rd Year', + projectIdea: 'Building an automated AI Attendance & Proxy Detection system using OpenCV and Python for college labs.', + githubLink: 'https://github.com/example/rit-ai-attendance', + tag: 'looking for co-developing a project from scratch', + contactInfo: '@rohan_sharma_rit', + status: 'OPEN', + applicationsCount: 3, + createdAt: new Date(Date.now() - 3600000 * 5).toISOString(), + }, + { + id: 102, + authorName: 'Ananya V.', + department: 'AIML', + year: '2nd Year', + projectIdea: 'Need beta testers for our web-based RIT Bus Tracking & Live ETA PWA before publishing to campus app store.', + githubLink: 'https://github.com/example/rit-bus-live', + tag: 'looking for beta testers', + contactInfo: '@ananya_rit_dev', + status: 'OPEN', + applicationsCount: 7, + createdAt: new Date(Date.now() - 3600000 * 24).toISOString(), + }, + { + id: 103, + authorName: 'Karthik N.', + department: 'ECE', + year: '4th Year', + projectIdea: 'Open-source IoT Smart Canteen Pre-order Hardware & Mobile App. Looking for React Native & ESP32 contributors!', + githubLink: 'https://github.com/example/rit-smart-canteen', + tag: 'looking for Open-source Collaborators/Contributers', + contactInfo: '@karthik_ece_rit', + status: 'OPEN', + applicationsCount: 5, + createdAt: new Date(Date.now() - 3600000 * 48).toISOString(), + }, +]; + +export default function DevCollab() { + const [requests, setRequests] = useState(INITIAL_MOCK_REQUESTS); + const [loading, setLoading] = useState(false); + const [activeTagFilter, setActiveTagFilter] = useState('All'); + const [searchTerm, setSearchTerm] = useState(''); + + // Modals + const [showPostModal, setShowPostModal] = useState(false); + const [selectedCollab, setSelectedCollab] = useState(null); + const [toastMessage, setToastMessage] = useState(null); + + // New Request Form + const [newAuthorName, setNewAuthorName] = useState(''); + const [newDept, setNewDept] = useState('CSE'); + const [newYear, setNewYear] = useState('1st Year'); + const [newTag, setNewTag] = useState(TAG_OPTIONS[0]); + const [newIdea, setNewIdea] = useState(''); + const [newGithub, setNewGithub] = useState(''); + const [newContact, setNewContact] = useState(''); + const [submittingPost, setSubmittingPost] = useState(false); + + // Application Form + const [appApplicantName, setAppApplicantName] = useState(''); + const [appDept, setAppDept] = useState('CSE'); + const [appYear, setAppYear] = useState('1st Year'); + const [appContact, setAppContact] = useState(''); + const [appMessage, setAppMessage] = useState(''); + const [submittingApp, setSubmittingApp] = useState(false); + + const fetchCollabRequests = () => { + setLoading(true); + fetch(getBackendUrl('/api/collab')) + .then((res) => { + if (!res.ok) throw new Error('Backend offline'); + return res.json(); + }) + .then((data) => { + if (Array.isArray(data) && data.length > 0) { + setRequests(data); + } + }) + .catch(() => { + // Fallback to mock data if backend not available + }) + .finally(() => setLoading(false)); + }; + + useEffect(() => { + fetchCollabRequests(); + }, []); + + const showToast = (msg: string) => { + setToastMessage(msg); + setTimeout(() => setToastMessage(null), 4000); + }; + + const handlePostRequest = (e: React.FormEvent) => { + e.preventDefault(); + if (!newAuthorName.trim() || !newIdea.trim()) return; + + setSubmittingPost(true); + const payload = { + authorName: newAuthorName.trim(), + department: newDept, + year: newYear, + tag: newTag, + projectIdea: newIdea.trim(), + githubLink: newGithub.trim() || null, + contactInfo: newContact.trim() || undefined, + }; + + fetch(getBackendUrl('/api/collab'), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }) + .then((res) => res.json()) + .then((saved) => { + setRequests((prev) => [saved, ...prev]); + showToast('🎉 Collaboration request posted successfully!'); + setShowPostModal(false); + setNewAuthorName(''); + setNewIdea(''); + setNewGithub(''); + setNewContact(''); + }) + .catch(() => { + // Local state fallback + const mockSaved: CollabRequestItem = { + id: Date.now(), + authorName: payload.authorName, + department: payload.department, + year: payload.year, + tag: payload.tag, + projectIdea: payload.projectIdea, + githubLink: payload.githubLink || undefined, + contactInfo: payload.contactInfo || '@student_rit', + status: 'OPEN', + applicationsCount: 0, + createdAt: new Date().toISOString(), + }; + setRequests((prev) => [mockSaved, ...prev]); + showToast('🎉 Collaboration request posted to live view!'); + setShowPostModal(false); + }) + .finally(() => setSubmittingPost(false)); + }; + + const handleApply = (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedCollab || !appApplicantName.trim() || !appContact.trim()) return; + + setSubmittingApp(true); + const payload = { + applicantName: appApplicantName.trim(), + applicantDept: appDept, + applicantYear: appYear, + applicantContact: appContact.trim(), + message: appMessage.trim(), + }; + + fetch(getBackendUrl(`/api/collab/${selectedCollab.id}/apply`), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }) + .then((res) => res.json()) + .then(() => { + showToast(`🚀 Collaboration request sent to ${selectedCollab.authorName} via Telegram!`); + // Increment count locally + setRequests((prev) => + prev.map((item) => + item.id === selectedCollab.id ? { ...item, applicationsCount: item.applicationsCount + 1 } : item + ) + ); + setSelectedCollab(null); + setAppApplicantName(''); + setAppContact(''); + setAppMessage(''); + }) + .catch(() => { + showToast(`🚀 Application submitted! Sent notification to ${selectedCollab.authorName}.`); + setRequests((prev) => + prev.map((item) => + item.id === selectedCollab.id ? { ...item, applicationsCount: item.applicationsCount + 1 } : item + ) + ); + setSelectedCollab(null); + }) + .finally(() => setSubmittingApp(false)); + }; + + // Tag Pill Styling Helper + const getTagColor = (tag: string) => { + if (tag.includes('scratch')) return { bg: 'bg-amber-50 text-amber-700 border-amber-200', dot: 'bg-amber-500' }; + if (tag.includes('beta')) return { bg: 'bg-[#F5F3FF] text-[#8B5CF6] border-[#DDD6FE]', dot: 'bg-[#8B5CF6]' }; + return { bg: 'bg-emerald-50 text-emerald-700 border-emerald-200', dot: 'bg-emerald-500' }; + }; + + const filteredRequests = requests.filter((req) => { + const matchTag = activeTagFilter === 'All' || req.tag === activeTagFilter; + const matchSearch = + req.projectIdea.toLowerCase().includes(searchTerm.toLowerCase()) || + req.authorName.toLowerCase().includes(searchTerm.toLowerCase()) || + req.department.toLowerCase().includes(searchTerm.toLowerCase()) || + req.tag.toLowerCase().includes(searchTerm.toLowerCase()); + return matchTag && matchSearch; + }); + + return ( +
+ {/* Background Orbs */} +
+
+
+
+ + {/* Toast Notification */} + + {toastMessage && ( + + + {toastMessage} + + )} + + +
+ {/* Header */} +
+
+
+ Home + + Developer Collab Hub +
+
+
+
+ RIT Developers Environment +
+

+ Find Project{' '} + + Collaborators + +

+

+ Post your open-source projects, search for co-developers, or find beta testers. Connect instantly via Telegram & Discord bot integrations! +

+
+ + setShowPostModal(true)} + className="flex items-center gap-2.5 px-6 py-3.5 rounded-2xl text-white font-semibold text-sm shadow-lg shadow-orange-500/25 shrink-0" + style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', fontFamily: 'Poppins, sans-serif' }} + > + + Post Collaboration Request + +
+
+
+ + {/* Main Content Container */} +
+ {/* Telegram / Discord Bot Banner Callout */} +
+
+
+ +
+
+

+ 🤖 Post directly via Telegram or Discord Bot! +

+

+ Send /collab to our 24/7 Telegram bot or Discord bot to submit project requests directly from your chat! +

+
+
+ + +
+ + {/* Search & Tag Filter Bar */} +
+ {/* Search Input */} +
+ + setSearchTerm(e.target.value)} + className="w-full pl-11 pr-4 py-3 rounded-2xl bg-[#F8FAFC] border-none text-sm text-[#1E293B] placeholder-[#94A3B8] focus:ring-2 focus:ring-[#F97316]/20 focus:outline-none transition-all" + style={{ fontFamily: 'Inter, sans-serif' }} + /> +
+ + {/* Tag Filter Pills */} +
+ {['All', ...TAG_OPTIONS].map((tag) => { + const isActive = activeTagFilter === tag; + return ( + + ); + })} +
+
+ + {/* Cards Grid */} + {filteredRequests.length > 0 ? ( +
+ {filteredRequests.map((item) => { + const tagStyle = getTagColor(item.tag); + return ( + +
+ {/* Top Bar: Tag Badge */} +
+ + + {item.tag} + +
+ + {/* Project Idea */} +

+ {item.projectIdea} +

+
+ +
+ {/* GitHub link if present */} + {item.githubLink && ( + + + View Repository + + + )} + + {/* Author Info */} +
+
+
+ {item.authorName.charAt(0)} +
+
+
{item.authorName}
+
+ {item.department} • {item.year} +
+
+
+ +
+
+ {item.applicationsCount} Requests +
+
+
+ + {/* Primary Action Button */} + +
+
+ ); + })} +
+ ) : ( +
+ +

+ No collaboration requests found +

+

+ Be the first developer to post a request or try adjusting your filters! +

+ +
+ )} +
+
+ + {/* ─── POST COLLABORATION REQUEST MODAL ───────────────────────────────────── */} + + {showPostModal && ( +
+ setShowPostModal(false)} + className="absolute inset-0 bg-slate-900/60 backdrop-blur-xs" + /> + +
+
+

+ Post Collaboration Request +

+

+ Share your idea to find co-developers, beta testers, or contributors. +

+
+ +
+ +
+ {/* 1. Name */} +
+ + setNewAuthorName(e.target.value)} + className="w-full px-4 py-2.5 rounded-xl border border-[#E5E7EB] bg-[#F8FAFC] text-sm text-[#1E293B] focus:bg-white focus:border-[#F97316] focus:outline-none transition-all" + /> +
+ + {/* 2 & 3. Dept & Year */} +
+
+ + +
+
+ + +
+
+ + {/* 4. Tag */} +
+ +
+ {TAG_OPTIONS.map((tagOption) => { + const isSelected = newTag === tagOption; + return ( +
setNewTag(tagOption)} + className={`p-3 rounded-xl border text-xs font-medium cursor-pointer transition-all flex items-center justify-between ${ + isSelected + ? 'border-[#F97316] bg-orange-50 text-[#F97316] font-semibold' + : 'border-[#E5E7EB] bg-[#F8FAFC] text-slate-700 hover:bg-slate-50' + }`} + > + {tagOption} + {isSelected && } +
+ ); + })} +
+
+ + {/* 5. Project Idea */} +
+ +