From d51d538ee9ad3903566c7b6b1eb2e17dacf87554 Mon Sep 17 00:00:00 2001 From: Shanmuga Krishnan S M Date: Mon, 3 Aug 2026 08:28:47 +0530 Subject: [PATCH] feat: enhance chatbot Telegram/Discord integrations, add /remove commands and Q&A reply handling --- .gitignore | 1 + .../com/rit/portal/PortalApplication.java | 2 + .../portal/controller/CollabController.java | 45 +- .../portal/controller/LeetcodeController.java | 63 ++ .../com/rit/portal/entity/CollabRequest.java | 8 + .../rit/portal/entity/LeetcodeProfile.java | 67 ++ .../repository/CollabRequestRepository.java | 2 + .../repository/LeetcodeProfileRepository.java | 15 + .../rit/portal/service/LeetcodeService.java | 153 +++++ .../compile/default-compile/createdFiles.lst | 26 - .../compile/default-compile/inputFiles.lst | 20 - .../src/main/java/com/rit/driver/Config.java | 6 +- src/App.tsx | 2 + src/components/Navbar/Navbar.tsx | 16 +- src/constants/index.ts | 1 + src/pages/DevCollab/DevCollab.tsx | 94 ++- .../LeetcodeLeaderboard.tsx | 621 ++++++++++++++++++ telegram-bot/config.json | 2 +- telegram-bot/telegram_bot.py | 197 +++++- vite.config.ts | 1 + 20 files changed, 1243 insertions(+), 99 deletions(-) create mode 100644 backend/src/main/java/com/rit/portal/controller/LeetcodeController.java create mode 100644 backend/src/main/java/com/rit/portal/entity/LeetcodeProfile.java create mode 100644 backend/src/main/java/com/rit/portal/repository/LeetcodeProfileRepository.java create mode 100644 backend/src/main/java/com/rit/portal/service/LeetcodeService.java delete mode 100644 backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst delete mode 100644 backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst create mode 100644 src/pages/LeetcodeLeaderboard/LeetcodeLeaderboard.tsx diff --git a/.gitignore b/.gitignore index 6281aae..dcb3f57 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ dist-ssr # Maven & Java target/ +**/target/ .mvn/ # Android & Gradle diff --git a/backend/src/main/java/com/rit/portal/PortalApplication.java b/backend/src/main/java/com/rit/portal/PortalApplication.java index 30b334c..9057fe7 100644 --- a/backend/src/main/java/com/rit/portal/PortalApplication.java +++ b/backend/src/main/java/com/rit/portal/PortalApplication.java @@ -2,8 +2,10 @@ package com.rit.portal; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +@EnableScheduling public class PortalApplication { public static void main(String[] args) { diff --git a/backend/src/main/java/com/rit/portal/controller/CollabController.java b/backend/src/main/java/com/rit/portal/controller/CollabController.java index c0d1b00..c0c27a3 100644 --- a/backend/src/main/java/com/rit/portal/controller/CollabController.java +++ b/backend/src/main/java/com/rit/portal/controller/CollabController.java @@ -43,6 +43,10 @@ public class CollabController { public CollabRequest createCollabRequest(@RequestBody CollabRequest request) { if (request.getStatus() == null) request.setStatus("OPEN"); if (request.getApplicationsCount() == null) request.setApplicationsCount(0); + if (request.getCollaboratorsNeeded() == null || request.getCollaboratorsNeeded() < 1) { + request.setCollaboratorsNeeded(1); + } + if (request.getAcceptedCount() == null) request.setAcceptedCount(0); request.setCreatedAt(LocalDateTime.now()); return collabRequestRepository.save(request); } @@ -52,6 +56,10 @@ public class CollabController { @PathVariable Integer id, @RequestBody CollabApplication application) { return collabRequestRepository.findById(id).map(request -> { + if ("CLOSED".equalsIgnoreCase(request.getStatus()) || "CANCELLED".equalsIgnoreCase(request.getStatus())) { + return ResponseEntity.badRequest().build(); + } + application.setCollabRequest(request); if (application.getStatus() == null) application.setStatus("PENDING"); application.setCreatedAt(LocalDateTime.now()); @@ -95,9 +103,44 @@ public class CollabController { @PathVariable Integer applicationId, @RequestParam String status) { return collabApplicationRepository.findById(applicationId).map(app -> { - app.setStatus(status.toUpperCase()); + String newStatus = status.toUpperCase(); + String oldStatus = app.getStatus(); + app.setStatus(newStatus); CollabApplication updated = collabApplicationRepository.save(app); + + // Delete request entirely when accepted count meets requested collaborators count + if ("ACCEPTED".equals(newStatus) && !"ACCEPTED".equals(oldStatus)) { + CollabRequest parentReq = app.getCollabRequest(); + if (parentReq != null) { + int accepted = (parentReq.getAcceptedCount() == null ? 0 : parentReq.getAcceptedCount()) + 1; + parentReq.setAcceptedCount(accepted); + int needed = parentReq.getCollaboratorsNeeded() == null ? 1 : parentReq.getCollaboratorsNeeded(); + if (accepted >= needed) { + collabRequestRepository.delete(parentReq); + } else { + collabRequestRepository.save(parentReq); + } + } + } return ResponseEntity.ok(updated); }).orElse(ResponseEntity.notFound().build()); } + + @GetMapping("/active/telegram/{chatId}") + public List getActiveRequestsByTelegram(@PathVariable Long chatId) { + return collabRequestRepository.findByTelegramChatIdAndStatusOrderByCreatedAtDesc(chatId, "OPEN"); + } + + @GetMapping("/active/discord/{userId}") + public List getActiveRequestsByDiscord(@PathVariable String userId) { + return collabRequestRepository.findByDiscordUserIdAndStatusOrderByCreatedAtDesc(userId, "OPEN"); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteOrCancelCollabRequest(@PathVariable Integer id) { + return collabRequestRepository.findById(id).map(req -> { + collabRequestRepository.delete(req); + return ResponseEntity.noContent().build(); + }).orElse(ResponseEntity.notFound().build()); + } } diff --git a/backend/src/main/java/com/rit/portal/controller/LeetcodeController.java b/backend/src/main/java/com/rit/portal/controller/LeetcodeController.java new file mode 100644 index 0000000..117f25a --- /dev/null +++ b/backend/src/main/java/com/rit/portal/controller/LeetcodeController.java @@ -0,0 +1,63 @@ +package com.rit.portal.controller; + +import com.rit.portal.entity.LeetcodeProfile; +import com.rit.portal.service.LeetcodeService; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/leetcode") +@CrossOrigin(originPatterns = "*") +@RequiredArgsConstructor +public class LeetcodeController { + + private final LeetcodeService leetcodeService; + + @GetMapping("/leaderboard") + public List getLeaderboard() { + return leetcodeService.getAllRankedProfiles(); + } + + @PostMapping("/register") + public ResponseEntity registerProfile(@RequestBody RegisterRequest request) { + if (request.getLeetcodeUsername() == null || request.getLeetcodeUsername().trim().isEmpty()) { + return ResponseEntity.badRequest().body(Map.of("error", "LeetCode username is required")); + } + if (request.getStudentName() == null || request.getStudentName().trim().isEmpty()) { + return ResponseEntity.badRequest().body(Map.of("error", "Student name is required")); + } + + try { + LeetcodeProfile profile = leetcodeService.registerOrUpdateStudent( + request.getStudentName(), + request.getLeetcodeUsername(), + request.getDepartment() != null ? request.getDepartment() : "CSE", + request.getYear() != null ? request.getYear() : "1st Year" + ); + return ResponseEntity.ok(profile); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body(Map.of("error", e.getMessage())); + } catch (Exception e) { + return ResponseEntity.internalServerError().body(Map.of("error", "Failed to register profile: " + e.getMessage())); + } + } + + @PostMapping("/sync") + public ResponseEntity triggerSpacedSync() { + leetcodeService.syncAllProfilesWithSpacing(); + return ResponseEntity.ok(Map.of("message", "24h background sync initiated with 3-second request spacing between users.")); + } + + @Data + public static class RegisterRequest { + private String studentName; + private String leetcodeUsername; + private String department; + private String year; + } +} diff --git a/backend/src/main/java/com/rit/portal/entity/CollabRequest.java b/backend/src/main/java/com/rit/portal/entity/CollabRequest.java index 9733994..d0800db 100644 --- a/backend/src/main/java/com/rit/portal/entity/CollabRequest.java +++ b/backend/src/main/java/com/rit/portal/entity/CollabRequest.java @@ -54,6 +54,14 @@ public class CollabRequest { @Builder.Default private Integer applicationsCount = 0; + @Column(name = "collaborators_needed") + @Builder.Default + private Integer collaboratorsNeeded = 1; + + @Column(name = "accepted_count") + @Builder.Default + private Integer acceptedCount = 0; + @Column(name = "created_at") @Builder.Default private LocalDateTime createdAt = LocalDateTime.now(); diff --git a/backend/src/main/java/com/rit/portal/entity/LeetcodeProfile.java b/backend/src/main/java/com/rit/portal/entity/LeetcodeProfile.java new file mode 100644 index 0000000..6cab468 --- /dev/null +++ b/backend/src/main/java/com/rit/portal/entity/LeetcodeProfile.java @@ -0,0 +1,67 @@ +package com.rit.portal.entity; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "leetcode_profiles") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class LeetcodeProfile { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(name = "student_name", nullable = false) + private String studentName; + + @Column(name = "leetcode_username", nullable = false, unique = true) + private String leetcodeUsername; + + @Column(nullable = false) + private String department; + + @Column(nullable = false) + private String year; + + @Column(name = "total_solved") + @Builder.Default + private Integer totalSolved = 0; + + @Column(name = "easy_solved") + @Builder.Default + private Integer easySolved = 0; + + @Column(name = "medium_solved") + @Builder.Default + private Integer mediumSolved = 0; + + @Column(name = "hard_solved") + @Builder.Default + private Integer hardSolved = 0; + + @Column(name = "ranking") + @Builder.Default + private Integer ranking = 0; + + @Column(name = "reputation") + @Builder.Default + private Integer reputation = 0; + + @Column(name = "last_updated") + private LocalDateTime lastUpdated; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @PrePersist + protected void onCreate() { + if (createdAt == null) createdAt = LocalDateTime.now(); + if (lastUpdated == null) lastUpdated = LocalDateTime.now(); + } +} diff --git a/backend/src/main/java/com/rit/portal/repository/CollabRequestRepository.java b/backend/src/main/java/com/rit/portal/repository/CollabRequestRepository.java index c7dc321..7e382c6 100644 --- a/backend/src/main/java/com/rit/portal/repository/CollabRequestRepository.java +++ b/backend/src/main/java/com/rit/portal/repository/CollabRequestRepository.java @@ -8,4 +8,6 @@ import java.util.List; @Repository public interface CollabRequestRepository extends JpaRepository { List findAllByOrderByCreatedAtDesc(); + List findByTelegramChatIdAndStatusOrderByCreatedAtDesc(Long telegramChatId, String status); + List findByDiscordUserIdAndStatusOrderByCreatedAtDesc(String discordUserId, String status); } diff --git a/backend/src/main/java/com/rit/portal/repository/LeetcodeProfileRepository.java b/backend/src/main/java/com/rit/portal/repository/LeetcodeProfileRepository.java new file mode 100644 index 0000000..3699c77 --- /dev/null +++ b/backend/src/main/java/com/rit/portal/repository/LeetcodeProfileRepository.java @@ -0,0 +1,15 @@ +package com.rit.portal.repository; + +import com.rit.portal.entity.LeetcodeProfile; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface LeetcodeProfileRepository extends JpaRepository { + Optional findByLeetcodeUsernameIgnoreCase(String leetcodeUsername); + List findAllByOrderByTotalSolvedDescRankingAsc(); + boolean existsByLeetcodeUsernameIgnoreCase(String leetcodeUsername); +} diff --git a/backend/src/main/java/com/rit/portal/service/LeetcodeService.java b/backend/src/main/java/com/rit/portal/service/LeetcodeService.java new file mode 100644 index 0000000..c8adbcc --- /dev/null +++ b/backend/src/main/java/com/rit/portal/service/LeetcodeService.java @@ -0,0 +1,153 @@ +package com.rit.portal.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.rit.portal.entity.LeetcodeProfile; +import com.rit.portal.repository.LeetcodeProfileRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.time.LocalDateTime; +import java.util.*; + +@Service +@RequiredArgsConstructor +@Slf4j +public class LeetcodeService { + + private final LeetcodeProfileRepository repository; + private final ObjectMapper objectMapper = new ObjectMapper(); + private final RestTemplate restTemplate = new RestTemplate(); + + private static final String LEETCODE_GRAPHQL_URL = "https://leetcode.com/graphql"; + + public List getAllRankedProfiles() { + return repository.findAllByOrderByTotalSolvedDescRankingAsc(); + } + + public LeetcodeProfile registerOrUpdateStudent(String studentName, String leetcodeUsername, String department, String year) { + String cleanUsername = leetcodeUsername.trim(); + + LeetcodeProfile profile = repository.findByLeetcodeUsernameIgnoreCase(cleanUsername) + .orElse(LeetcodeProfile.builder() + .studentName(studentName.trim()) + .leetcodeUsername(cleanUsername) + .department(department.trim()) + .year(year.trim()) + .build()); + + profile.setStudentName(studentName.trim()); + profile.setDepartment(department.trim()); + profile.setYear(year.trim()); + + // Fetch fresh stats immediately for registration + boolean success = fetchAndUpdateProfile(profile); + if (!success) { + throw new IllegalArgumentException("LeetCode username '" + cleanUsername + "' could not be found on LeetCode."); + } + return repository.save(profile); + } + + public boolean fetchAndUpdateProfile(LeetcodeProfile profile) { + try { + log.info("Fetching LeetCode GraphQL stats for: {}", profile.getLeetcodeUsername()); + + String graphqlQuery = "{\"query\":\"query getUserProfile($username: String!) { matchedUser(username: $username) { username submitStats: submitStatsGlobal { acSubmissionNum { difficulty count } } profile { ranking reputation } } }\",\"variables\":{\"username\":\"" + profile.getLeetcodeUsername() + "\"}}"; + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"); + headers.set("Referer", "https://leetcode.com/"); + + HttpEntity request = new HttpEntity<>(graphqlQuery, headers); + ResponseEntity response = restTemplate.postForEntity(LEETCODE_GRAPHQL_URL, request, String.class); + + if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { + JsonNode root = objectMapper.readTree(response.getBody()); + JsonNode matchedUser = root.path("data").path("matchedUser"); + + if (matchedUser.isMissingNode() || matchedUser.isNull()) { + log.warn("LeetCode username not found: {}", profile.getLeetcodeUsername()); + return false; + } + + // Extract ranking and reputation + JsonNode profileNode = matchedUser.path("profile"); + if (!profileNode.isMissingNode()) { + profile.setRanking(profileNode.path("ranking").asInt(0)); + profile.setReputation(profileNode.path("reputation").asInt(0)); + } + + // Extract problem submission counts + JsonNode acSubmissions = matchedUser.path("submitStats").path("acSubmissionNum"); + if (acSubmissions.isArray()) { + for (JsonNode item : acSubmissions) { + String diff = item.path("difficulty").asText(""); + int count = item.path("count").asInt(0); + if ("All".equalsIgnoreCase(diff)) { + profile.setTotalSolved(count); + } else if ("Easy".equalsIgnoreCase(diff)) { + profile.setEasySolved(count); + } else if ("Medium".equalsIgnoreCase(diff)) { + profile.setMediumSolved(count); + } else if ("Hard".equalsIgnoreCase(diff)) { + profile.setHardSolved(count); + } + } + } + + profile.setLastUpdated(LocalDateTime.now()); + log.info("Successfully updated LeetCode profile for {}: Total Solved = {}", profile.getLeetcodeUsername(), profile.getTotalSolved()); + return true; + } + } catch (Exception e) { + log.error("Failed to fetch LeetCode data for {}: {}", profile.getLeetcodeUsername(), e.getMessage()); + } + return false; + } + + /** + * Automatic 24-hour scheduled job running at 2:00 AM every day. + * Uses spaced requests (3-second delay between each user) to ensure ZERO IP blocking. + */ + @Scheduled(cron = "0 0 2 * * ?") + public void scheduledDailySyncWithSpacedRequests() { + log.info("Starting scheduled 24h LeetCode sync job..."); + syncAllProfilesWithSpacing(); + } + + /** + * Manual or scheduled trigger to sync all profiles with a 3-second delay between requests. + */ + public void syncAllProfilesWithSpacing() { + List profiles = repository.findAll(); + log.info("Queued {} LeetCode profiles for spaced synchronization.", profiles.size()); + + new Thread(() -> { + for (int i = 0; i < profiles.size(); i++) { + LeetcodeProfile profile = profiles.get(i); + try { + fetchAndUpdateProfile(profile); + repository.save(profile); + + // Space out requests by 3 seconds if not the last item + if (i < profiles.size() - 1) { + log.info("Waiting 3 seconds before fetching next profile to space out requests..."); + Thread.sleep(5000); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("Spaced sync job interrupted."); + break; + } catch (Exception e) { + log.error("Error during spaced sync for {}: {}", profile.getLeetcodeUsername(), e.getMessage()); + } + } + log.info("Finished spaced sync for all LeetCode profiles."); + }).start(); + } +} diff --git a/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst deleted file mode 100644 index 3c9dc21..0000000 --- a/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +++ /dev/null @@ -1,26 +0,0 @@ -com\rit\portal\controller\BusLocationController.class -com\rit\portal\config\DataInitializer$1.class -com\rit\portal\repository\CommunityAnswerRepository.class -com\rit\portal\entity\CommunityAnswer$CommunityAnswerBuilder.class -com\rit\portal\entity\CommunityQuestion.class -com\rit\portal\controller\BusRouteController.class -com\rit\portal\entity\NotePyq.class -com\rit\portal\entity\CommunityAnswer.class -com\rit\portal\controller\CommunityQuestionController.class -com\rit\portal\dto\DriverLocationUpdate.class -com\rit\portal\config\WebConfig.class -com\rit\portal\repository\BusStopRepository.class -com\rit\portal\config\DataInitializer.class -com\rit\portal\entity\BusStop.class -com\rit\portal\entity\BusStop$BusStopBuilder.class -com\rit\portal\repository\BusRouteRepository.class -com\rit\portal\entity\CommunityQuestion$CommunityQuestionBuilder.class -com\rit\portal\entity\NotePyq$NotePyqBuilder.class -com\rit\portal\entity\BusRoute$BusRouteBuilder.class -com\rit\portal\controller\NotePyqController.class -com\rit\portal\service\BusLocationService.class -com\rit\portal\repository\NotePyqRepository.class -com\rit\portal\model\BusLocation.class -com\rit\portal\entity\BusRoute.class -com\rit\portal\repository\CommunityQuestionRepository.class -com\rit\portal\PortalApplication.class diff --git a/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst deleted file mode 100644 index 1177e4f..0000000 --- a/backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +++ /dev/null @@ -1,20 +0,0 @@ -F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\config\DataInitializer.java -F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\config\WebConfig.java -F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\controller\BusLocationController.java -F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\controller\BusRouteController.java -F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\controller\CommunityQuestionController.java -F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\controller\NotePyqController.java -F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\dto\DriverLocationUpdate.java -F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\entity\BusRoute.java -F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\entity\BusStop.java -F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\entity\CommunityAnswer.java -F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\entity\CommunityQuestion.java -F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\entity\NotePyq.java -F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\model\BusLocation.java -F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\PortalApplication.java -F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\repository\BusRouteRepository.java -F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\repository\BusStopRepository.java -F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\repository\CommunityAnswerRepository.java -F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\repository\CommunityQuestionRepository.java -F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\repository\NotePyqRepository.java -F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\service\BusLocationService.java diff --git a/rit-driver-app/app/src/main/java/com/rit/driver/Config.java b/rit-driver-app/app/src/main/java/com/rit/driver/Config.java index 6bc95e9..8f75eaa 100644 --- a/rit-driver-app/app/src/main/java/com/rit/driver/Config.java +++ b/rit-driver-app/app/src/main/java/com/rit/driver/Config.java @@ -1,9 +1,9 @@ package com.rit.driver; public class Config { - // Local testing backend URL (Your PC Wi-Fi IP) - public static final String BACKEND_URL = "http://10.10.15.91:8085"; - // Default driver security PIN + public static final String BACKEND_URL = "http://10.43.158.201:8085"; + + public static final String DEFAULT_PIN = "RITDRIVER"; } diff --git a/src/App.tsx b/src/App.tsx index 29e4597..4be763f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,6 +11,7 @@ import Community from '@/pages/Community/Community'; import BusRoutes from '@/pages/BusRoutes/BusRoutes'; import Faculty from '@/pages/Faculty/Faculty'; import DevCollab from '@/pages/DevCollab/DevCollab'; +import LeetcodeLeaderboard from '@/pages/LeetcodeLeaderboard/LeetcodeLeaderboard'; const queryClient = new QueryClient({ defaultOptions: { @@ -47,6 +48,7 @@ function App() { } /> } /> } /> + } /> diff --git a/src/components/Navbar/Navbar.tsx b/src/components/Navbar/Navbar.tsx index ebfa4a1..a8cf7c5 100644 --- a/src/components/Navbar/Navbar.tsx +++ b/src/components/Navbar/Navbar.tsx @@ -92,10 +92,10 @@ export default function Navbar() { setIsMobileOpen(!isMobileOpen)} - className="md:hidden w-9 h-9 rounded-xl flex items-center justify-center text-slate-300 hover:text-white hover:bg-white/10 transition-all" + className="md:hidden w-11 h-11 rounded-xl flex items-center justify-center text-slate-200 hover:text-white bg-slate-800/60 hover:bg-slate-700/60 border border-slate-700/50 transition-all active:scale-95" aria-label="Toggle menu" > - {isMobileOpen ? : } + {isMobileOpen ? : } @@ -108,20 +108,22 @@ export default function Navbar() { animate={{ height: 'auto', opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.25 }} - className="overflow-hidden md:hidden" + className="overflow-hidden md:hidden max-h-[80vh] overflow-y-auto" > -
+
{NAV_LINKS.map((link) => { const isActive = location.pathname === link.path; return ( setIsMobileOpen(false)} + className="px-3.5 py-3 rounded-xl text-xs font-semibold uppercase tracking-wider flex items-center justify-center text-center transition-all min-h-[44px]" style={{ - fontFamily: 'Inter, sans-serif', + fontFamily: 'Poppins, sans-serif', color: isActive ? '#FFFFFF' : '#CBD5E1', - backgroundColor: isActive ? '#F97316' : 'transparent', + backgroundColor: isActive ? '#F97316' : 'rgba(255, 255, 255, 0.05)', + border: isActive ? '1px solid #FB923C' : '1px solid rgba(255, 255, 255, 0.08)', }} > {link.label} diff --git a/src/constants/index.ts b/src/constants/index.ts index 3a0167f..76b2212 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -11,6 +11,7 @@ export const NAV_LINKS = [ { label: 'Clubs', path: '/events' }, { label: 'Community', path: '/community' }, { label: 'Dev Collab', path: '/collab' }, + { label: 'LeetCode', path: '/leetcode' }, ]; // ─── Stats ──────────────────────────────────────────────────────────────────── diff --git a/src/pages/DevCollab/DevCollab.tsx b/src/pages/DevCollab/DevCollab.tsx index 0db84f2..c7f346c 100644 --- a/src/pages/DevCollab/DevCollab.tsx +++ b/src/pages/DevCollab/DevCollab.tsx @@ -16,8 +16,11 @@ export interface CollabRequestItem { projectIdea: string; githubLink?: string; tag: string; + collaboratorsNeeded?: number; + acceptedCount?: number; contactInfo?: string; telegramChatId?: number; + discordUserId?: string; status: string; applicationsCount: number; createdAt: string; @@ -41,6 +44,8 @@ const INITIAL_MOCK_REQUESTS: CollabRequestItem[] = [ 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', + collaboratorsNeeded: 2, + acceptedCount: 0, contactInfo: '@rohan_sharma_rit', status: 'OPEN', applicationsCount: 3, @@ -54,6 +59,8 @@ const INITIAL_MOCK_REQUESTS: CollabRequestItem[] = [ 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', + collaboratorsNeeded: 5, + acceptedCount: 2, contactInfo: '@ananya_rit_dev', status: 'OPEN', applicationsCount: 7, @@ -67,6 +74,8 @@ const INITIAL_MOCK_REQUESTS: CollabRequestItem[] = [ 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', + collaboratorsNeeded: 3, + acceptedCount: 1, contactInfo: '@karthik_ece_rit', status: 'OPEN', applicationsCount: 5, @@ -90,6 +99,7 @@ export default function DevCollab() { const [newDept, setNewDept] = useState('CSE'); const [newYear, setNewYear] = useState('1st Year'); const [newTag, setNewTag] = useState(TAG_OPTIONS[0]); + const [newCollaboratorsNeeded, setNewCollaboratorsNeeded] = useState(1); const [newIdea, setNewIdea] = useState(''); const [newGithub, setNewGithub] = useState(''); const [newContact, setNewContact] = useState(''); @@ -140,6 +150,7 @@ export default function DevCollab() { department: newDept, year: newYear, tag: newTag, + collaboratorsNeeded: Number(newCollaboratorsNeeded) || 1, projectIdea: newIdea.trim(), githubLink: newGithub.trim() || null, contactInfo: newContact.trim() || undefined, @@ -159,6 +170,7 @@ export default function DevCollab() { setNewIdea(''); setNewGithub(''); setNewContact(''); + setNewCollaboratorsNeeded(1); }) .catch(() => { // Local state fallback @@ -168,6 +180,8 @@ export default function DevCollab() { department: payload.department, year: payload.year, tag: payload.tag, + collaboratorsNeeded: payload.collaboratorsNeeded, + acceptedCount: 0, projectIdea: payload.projectIdea, githubLink: payload.githubLink || undefined, contactInfo: payload.contactInfo || '@student_rit', @@ -202,8 +216,7 @@ export default function DevCollab() { }) .then((res) => res.json()) .then(() => { - showToast(`🚀 Collaboration request sent to ${selectedCollab.authorName} via Telegram!`); - // Increment count locally + showToast(`🚀 Collaboration request sent to ${selectedCollab.authorName}!`); setRequests((prev) => prev.map((item) => item.id === selectedCollab.id ? { ...item, applicationsCount: item.applicationsCount + 1 } : item @@ -226,7 +239,6 @@ export default function DevCollab() { .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]' }; @@ -315,10 +327,10 @@ export default function DevCollab() {

- 🤖 Post directly via Telegram or Discord Bot! + 🤖 Post or remove requests via Telegram or Discord!

- Send /collab to our 24/7 Telegram bot or Discord bot to submit project requests directly from your chat! + Send /collab to post requests or /remove to cancel active requests anytime!

@@ -377,23 +389,37 @@ export default function DevCollab() {
{filteredRequests.map((item) => { const tagStyle = getTagColor(item.tag); + const needed = item.collaboratorsNeeded || 1; + const accepted = item.acceptedCount || 0; + const isClosed = item.status === 'CLOSED' || item.status === 'CANCELLED' || accepted >= needed; + return (
- {/* Top Bar: Tag Badge */} -
+ {/* Top Bar: Tag Badge & Status */} +
{item.tag} + + {isClosed ? ( + + CLOSED • SPOTS FILLED + + ) : ( + + {accepted} / {needed} Spots Filled + + )}
{/* Project Idea */} -

+

{item.projectIdea}

@@ -429,20 +455,30 @@ export default function DevCollab() {
- {item.applicationsCount} Requests + {item.applicationsCount} Applications
{/* Primary Action Button */} - + {isClosed ? ( + + ) : ( + + )}
); @@ -576,10 +612,26 @@ export default function DevCollab() { - {/* 5. Project Idea */} + {/* 5. Collaborators Needed */}
+ setNewCollaboratorsNeeded(Math.max(1, parseInt(e.target.value) || 1))} + 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" + /> +
+ + {/* 6. Project Idea */} +
+