diff --git a/backend/pom.xml b/backend/pom.xml index e77713c..6f31a76 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -69,6 +69,13 @@ spring-boot-starter-validation + + + org.jsoup + jsoup + 1.18.1 + + org.springframework.boot diff --git a/backend/src/main/java/com/rit/portal/controller/SkillrackController.java b/backend/src/main/java/com/rit/portal/controller/SkillrackController.java new file mode 100644 index 0000000..0e06ca9 --- /dev/null +++ b/backend/src/main/java/com/rit/portal/controller/SkillrackController.java @@ -0,0 +1,68 @@ +package com.rit.portal.controller; + +import com.rit.portal.entity.SkillrackProfile; +import com.rit.portal.service.SkillrackService; +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/skillrack") +@CrossOrigin(originPatterns = "*") +@RequiredArgsConstructor +public class SkillrackController { + + private final SkillrackService skillrackService; + + @GetMapping("/leaderboard") + public List getLeaderboard() { + return skillrackService.getAllRankedProfiles(); + } + + @PostMapping("/register") + public ResponseEntity registerProfile(@RequestBody RegisterRequest request) { + if (request.getSkillrackEmail() == null || request.getSkillrackEmail().trim().isEmpty()) { + return ResponseEntity.badRequest().body(Map.of("error", "SkillRack email is required")); + } + if (request.getSkillrackPassword() == null || request.getSkillrackPassword().trim().isEmpty()) { + return ResponseEntity.badRequest().body(Map.of("error", "SkillRack password is required")); + } + if (request.getStudentName() == null || request.getStudentName().trim().isEmpty()) { + return ResponseEntity.badRequest().body(Map.of("error", "Student name is required")); + } + + try { + SkillrackProfile profile = skillrackService.registerOrUpdateStudent( + request.getStudentName(), + request.getSkillrackEmail(), + request.getSkillrackPassword(), + 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 triggerSync() { + skillrackService.syncAllProfilesWithSpacing(); + return ResponseEntity.ok(Map.of("message", "SkillRack background sync initiated with 5-second spacing.")); + } + + @Data + public static class RegisterRequest { + private String studentName; + private String skillrackEmail; + private String skillrackPassword; + private String department; + private String year; + } +} diff --git a/backend/src/main/java/com/rit/portal/entity/SkillrackProfile.java b/backend/src/main/java/com/rit/portal/entity/SkillrackProfile.java new file mode 100644 index 0000000..06fbd78 --- /dev/null +++ b/backend/src/main/java/com/rit/portal/entity/SkillrackProfile.java @@ -0,0 +1,78 @@ +package com.rit.portal.entity; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "skillrack_profiles") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SkillrackProfile { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(name = "student_name", nullable = false) + private String studentName; + + @Column(name = "skillrack_email", nullable = false, unique = true) + private String skillrackEmail; + + @Column(name = "encrypted_password", nullable = false, length = 512) + private String encryptedPassword; + + @Column(nullable = false) + private String department; + + @Column(nullable = false) + private String year; + + @Column(name = "total_points") + @Builder.Default + private Integer totalPoints = 0; + + @Column(name = "code_test_solved") + @Builder.Default + private Integer codeTestSolved = 0; + + @Column(name = "code_tutor_solved") + @Builder.Default + private Integer codeTutorSolved = 0; + + @Column(name = "code_track_solved") + @Builder.Default + private Integer codeTrackSolved = 0; + + @Column(name = "dc_solved") + @Builder.Default + private Integer dcSolved = 0; + + @Column(name = "gold_medals") + @Builder.Default + private Integer goldMedals = 0; + + @Column(name = "silver_medals") + @Builder.Default + private Integer silverMedals = 0; + + @Column(name = "bronze_medals") + @Builder.Default + private Integer bronzeMedals = 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/SkillrackProfileRepository.java b/backend/src/main/java/com/rit/portal/repository/SkillrackProfileRepository.java new file mode 100644 index 0000000..f296a1f --- /dev/null +++ b/backend/src/main/java/com/rit/portal/repository/SkillrackProfileRepository.java @@ -0,0 +1,15 @@ +package com.rit.portal.repository; + +import com.rit.portal.entity.SkillrackProfile; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface SkillrackProfileRepository extends JpaRepository { + Optional findBySkillrackEmailIgnoreCase(String skillrackEmail); + List findAllByOrderByTotalPointsDesc(); + boolean existsBySkillrackEmailIgnoreCase(String skillrackEmail); +} diff --git a/backend/src/main/java/com/rit/portal/service/SkillrackService.java b/backend/src/main/java/com/rit/portal/service/SkillrackService.java new file mode 100644 index 0000000..5c22d57 --- /dev/null +++ b/backend/src/main/java/com/rit/portal/service/SkillrackService.java @@ -0,0 +1,388 @@ +package com.rit.portal.service; + +import com.rit.portal.entity.SkillrackProfile; +import com.rit.portal.repository.SkillrackProfileRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jsoup.Connection; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Service +@RequiredArgsConstructor +@Slf4j +public class SkillrackService { + + private final SkillrackProfileRepository repository; + + // AES-128 key (16 bytes) — in production, move to environment variable + private static final String AES_KEY = "RitSkillR@ck2026"; + private static final String SKILLRACK_BASE = "https://www.skillrack.com"; + private static final String LOGIN_URL = SKILLRACK_BASE + "/faces/candidate/profileview.xhtml"; + private static final String PROFILE_URL = SKILLRACK_BASE + "/faces/candidate/profileview.xhtml"; + + // ─── Public API ─────────────────────────────────────────────────── + + public List getAllRankedProfiles() { + return repository.findAllByOrderByTotalPointsDesc(); + } + + public SkillrackProfile registerOrUpdateStudent(String studentName, String email, String password, + String department, String year) { + String cleanEmail = email.trim().toLowerCase(); + + SkillrackProfile profile = repository.findBySkillrackEmailIgnoreCase(cleanEmail) + .orElse(SkillrackProfile.builder() + .studentName(studentName.trim()) + .skillrackEmail(cleanEmail) + .department(department.trim()) + .year(year.trim()) + .build()); + + profile.setStudentName(studentName.trim()); + profile.setDepartment(department.trim()); + profile.setYear(year.trim()); + profile.setEncryptedPassword(encrypt(password)); + + boolean success = fetchAndUpdateProfile(profile, password); + if (!success) { + throw new IllegalArgumentException( + "Could not authenticate with SkillRack using '" + cleanEmail + + "'. Please verify your email and password are correct."); + } + return repository.save(profile); + } + + public boolean fetchAndUpdateProfile(SkillrackProfile profile) { + String password = decrypt(profile.getEncryptedPassword()); + if (password == null || password.isEmpty()) { + log.warn("Cannot decrypt password for SkillRack profile: {}", profile.getSkillrackEmail()); + return false; + } + return fetchAndUpdateProfile(profile, password); + } + + /** + * Core scraping method: + * 1. GET the SkillRack login page to obtain JSESSIONID cookie + ViewState + * 2. POST login form with email + password + ViewState + * 3. Parse the resulting profile/dashboard page for stats + */ + public boolean fetchAndUpdateProfile(SkillrackProfile profile, String rawPassword) { + try { + log.info("Fetching SkillRack stats for: {}", profile.getSkillrackEmail()); + + // Step 1: GET login page to get session cookie + ViewState + Connection.Response loginPageRes = Jsoup.connect(LOGIN_URL) + .method(Connection.Method.GET) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .timeout(15000) + .execute(); + + Map cookies = new HashMap<>(loginPageRes.cookies()); + Document loginDoc = loginPageRes.parse(); + + // Extract ViewState from hidden input + String viewState = ""; + Element vsEl = loginDoc.selectFirst("input[name=javax.faces.ViewState]"); + if (vsEl != null) { + viewState = vsEl.attr("value"); + } + + // Find the login form and its ID + Element loginForm = loginDoc.selectFirst("form[id]"); + String formId = loginForm != null ? loginForm.attr("id") : "j_idt4"; + + // Step 2: POST login credentials + Map formData = new LinkedHashMap<>(); + formData.put(formId, formId); + formData.put("javax.faces.ViewState", viewState); + + // Find email/username and password input fields + Elements inputs = loginDoc.select("input[type=text], input[type=email], input[type=password]"); + String emailFieldName = formId + ":j_idt6"; + String passwordFieldName = formId + ":j_idt8"; + + for (Element input : inputs) { + String name = input.attr("name"); + String type = input.attr("type"); + if (type.equals("text") || type.equals("email")) { + emailFieldName = name; + } else if (type.equals("password")) { + passwordFieldName = name; + } + } + + formData.put(emailFieldName, profile.getSkillrackEmail()); + formData.put(passwordFieldName, rawPassword); + + // Find submit button name + Element submitBtn = loginDoc.selectFirst("input[type=submit], button[type=submit]"); + if (submitBtn != null && submitBtn.hasAttr("name")) { + formData.put(submitBtn.attr("name"), submitBtn.attr("value")); + } + + String formAction = LOGIN_URL; + if (loginForm != null && loginForm.hasAttr("action")) { + String action = loginForm.attr("action"); + if (action.startsWith("http")) { + formAction = action; + } else { + formAction = SKILLRACK_BASE + action; + } + } + + Connection.Response loginRes = Jsoup.connect(formAction) + .method(Connection.Method.POST) + .cookies(cookies) + .data(formData) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .header("Content-Type", "application/x-www-form-urlencoded") + .header("Referer", LOGIN_URL) + .followRedirects(true) + .timeout(15000) + .execute(); + + cookies.putAll(loginRes.cookies()); + + // Step 3: Fetch the profile/dashboard page with authenticated session + Connection.Response profileRes = Jsoup.connect(PROFILE_URL) + .method(Connection.Method.GET) + .cookies(cookies) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .followRedirects(true) + .timeout(15000) + .execute(); + + cookies.putAll(profileRes.cookies()); + Document profileDoc = profileRes.parse(); + + // Check if we're still on the login page (auth failed) + String bodyText = profileDoc.body().text(); + if (bodyText.contains("Incorrect email") || bodyText.contains("Login") && !bodyText.contains("Total Points")) { + // Try alternative dashboard URL + Connection.Response dashRes = Jsoup.connect(SKILLRACK_BASE + "/faces/candidate/manageprofile.xhtml") + .method(Connection.Method.GET) + .cookies(cookies) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .followRedirects(true) + .timeout(15000) + .execute(); + cookies.putAll(dashRes.cookies()); + profileDoc = dashRes.parse(); + bodyText = profileDoc.body().text(); + } + + // Step 4: Parse profile stats from the HTML + boolean parsed = parseProfileStats(profileDoc, profile); + + if (!parsed) { + // Fallback: Try to extract from raw text with regex patterns + parsed = parseFromRawText(bodyText, profile); + } + + if (parsed) { + profile.setLastUpdated(LocalDateTime.now()); + log.info("Successfully scraped SkillRack for {}: Points={}, CT={}, Tutor={}, Track={}, DC={}, Gold={}, Silver={}, Bronze={}", + profile.getSkillrackEmail(), profile.getTotalPoints(), + profile.getCodeTestSolved(), profile.getCodeTutorSolved(), + profile.getCodeTrackSolved(), profile.getDcSolved(), + profile.getGoldMedals(), profile.getSilverMedals(), profile.getBronzeMedals()); + return true; + } + + log.warn("Could not parse SkillRack profile data for: {}", profile.getSkillrackEmail()); + return false; + + } catch (Exception e) { + log.error("Failed to fetch SkillRack data for {}: {}", profile.getSkillrackEmail(), e.getMessage()); + return false; + } + } + + // ─── HTML Parsing ───────────────────────────────────────────────── + + private boolean parseProfileStats(Document doc, SkillrackProfile profile) { + boolean foundAnything = false; + String fullText = doc.body().text(); + + // Try to find stats from structured HTML elements (cards, spans, divs) + // SkillRack typically shows stats in card-like containers + + // Look for numbers near keywords + foundAnything = parseFromRawText(fullText, profile); + + // Also try parsing from any table structures + Elements tables = doc.select("table"); + for (Element table : tables) { + Elements rows = table.select("tr"); + for (Element row : rows) { + Elements cells = row.select("td, th"); + if (cells.size() >= 2) { + String label = cells.get(0).text().trim().toLowerCase(); + String value = cells.get(cells.size() - 1).text().trim(); + int num = parseIntSafe(value); + + if (label.contains("code test") || label.contains("codetest")) { + profile.setCodeTestSolved(num); + foundAnything = true; + } else if (label.contains("code tutor") || label.contains("codetutor")) { + profile.setCodeTutorSolved(num); + foundAnything = true; + } else if (label.contains("code track") || label.contains("codetrack")) { + profile.setCodeTrackSolved(num); + foundAnything = true; + } else if (label.contains("daily challenge") || label.contains("dc")) { + profile.setDcSolved(num); + foundAnything = true; + } else if (label.contains("gold")) { + profile.setGoldMedals(num); + foundAnything = true; + } else if (label.contains("silver")) { + profile.setSilverMedals(num); + foundAnything = true; + } else if (label.contains("bronze")) { + profile.setBronzeMedals(num); + foundAnything = true; + } else if (label.contains("total") && label.contains("point")) { + profile.setTotalPoints(num); + foundAnything = true; + } + } + } + } + + return foundAnything; + } + + private boolean parseFromRawText(String text, SkillrackProfile profile) { + boolean found = false; + + // Pattern: "Code Test 123" or "CodeTest: 123" or "Code Test Solved: 123" + found |= extractAndSet(text, "(?i)code\\s*test[^\\d]*(\\d+)", val -> profile.setCodeTestSolved(val)); + found |= extractAndSet(text, "(?i)code\\s*tutor[^\\d]*(\\d+)", val -> profile.setCodeTutorSolved(val)); + found |= extractAndSet(text, "(?i)code\\s*track[^\\d]*(\\d+)", val -> profile.setCodeTrackSolved(val)); + found |= extractAndSet(text, "(?i)daily\\s*challenge[^\\d]*(\\d+)", val -> profile.setDcSolved(val)); + found |= extractAndSet(text, "(?i)(?:dc|DC)[^\\d]*(\\d+)", val -> { + if (profile.getDcSolved() == 0) profile.setDcSolved(val); + }); + found |= extractAndSet(text, "(?i)gold[^\\d]*(\\d+)", val -> profile.setGoldMedals(val)); + found |= extractAndSet(text, "(?i)silver[^\\d]*(\\d+)", val -> profile.setSilverMedals(val)); + found |= extractAndSet(text, "(?i)bronze[^\\d]*(\\d+)", val -> profile.setBronzeMedals(val)); + found |= extractAndSet(text, "(?i)total\\s*(?:point|score)s?[^\\d]*(\\d+)", val -> profile.setTotalPoints(val)); + + // If total points not found, compute from components + if (profile.getTotalPoints() == 0 && found) { + int computed = profile.getCodeTestSolved() + profile.getCodeTutorSolved() + + profile.getCodeTrackSolved() + profile.getDcSolved(); + if (computed > 0) { + profile.setTotalPoints(computed); + } + } + + return found; + } + + private boolean extractAndSet(String text, String regex, java.util.function.IntConsumer setter) { + Matcher m = Pattern.compile(regex).matcher(text); + if (m.find()) { + int val = parseIntSafe(m.group(1)); + if (val > 0) { + setter.accept(val); + return true; + } + } + return false; + } + + private int parseIntSafe(String s) { + try { + return Integer.parseInt(s.replaceAll("[^\\d]", "")); + } catch (NumberFormatException e) { + return 0; + } + } + + // ─── AES Encryption Helpers ─────────────────────────────────────── + + private String encrypt(String plainText) { + try { + SecretKeySpec key = new SecretKeySpec(AES_KEY.getBytes(StandardCharsets.UTF_8), "AES"); + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, key); + byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(encrypted); + } catch (Exception e) { + log.error("AES encryption failed: {}", e.getMessage()); + throw new RuntimeException("Encryption failed", e); + } + } + + private String decrypt(String encryptedBase64) { + try { + SecretKeySpec key = new SecretKeySpec(AES_KEY.getBytes(StandardCharsets.UTF_8), "AES"); + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, key); + byte[] decoded = Base64.getDecoder().decode(encryptedBase64); + byte[] decrypted = cipher.doFinal(decoded); + return new String(decrypted, StandardCharsets.UTF_8); + } catch (Exception e) { + log.error("AES decryption failed: {}", e.getMessage()); + return null; + } + } + + // ─── Scheduled & Manual Sync ────────────────────────────────────── + + /** + * Automatic daily sync at 2:30 AM — spaced 5s apart to avoid rate limits + */ + @Scheduled(cron = "0 30 2 * * ?") + public void scheduledDailySync() { + log.info("Starting scheduled daily SkillRack sync..."); + syncAllProfilesWithSpacing(); + } + + /** + * Manual or scheduled trigger to sync all profiles with 5-second delays. + */ + public void syncAllProfilesWithSpacing() { + List profiles = repository.findAll(); + log.info("Queued {} SkillRack profiles for sync.", profiles.size()); + + new Thread(() -> { + for (int i = 0; i < profiles.size(); i++) { + SkillrackProfile p = profiles.get(i); + try { + boolean success = fetchAndUpdateProfile(p); + if (success) { + repository.save(p); + } + if (i < profiles.size() - 1) { + log.info("Waiting 5s before next SkillRack profile sync..."); + Thread.sleep(5000); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("SkillRack sync interrupted."); + break; + } catch (Exception e) { + log.error("Error syncing SkillRack profile {}: {}", p.getSkillrackEmail(), e.getMessage()); + } + } + log.info("Finished SkillRack sync for all profiles."); + }).start(); + } +} diff --git a/src/pages/LeetcodeLeaderboard/LeetcodeLeaderboard.tsx b/src/pages/LeetcodeLeaderboard/LeetcodeLeaderboard.tsx index d053259..9b7d032 100644 --- a/src/pages/LeetcodeLeaderboard/LeetcodeLeaderboard.tsx +++ b/src/pages/LeetcodeLeaderboard/LeetcodeLeaderboard.tsx @@ -4,7 +4,7 @@ import { Trophy, Search, Plus, RefreshCw, ExternalLink, Code2, Sparkles, Flame, Award, CheckCircle2, Shield, ShieldCheck, User, Filter, X, Clock, Lock, Calendar, Upload, FileText, Check, Database, Cpu, Cloud, ShieldAlert, GraduationCap, - Eye, Trash2, CheckCheck, ChevronDown, Download + Eye, Trash2, CheckCheck, ChevronDown, Download, Zap, Target, Mail, KeyRound } from 'lucide-react'; import { getBackendUrl } from '@/lib/utils'; import { useAuth } from '@/context/AuthContext'; @@ -28,6 +28,23 @@ interface LeetcodeProfile { lastUpdated: string; } +interface SkillrackProfile { + id: number; + studentName: string; + skillrackEmail: string; + department: string; + year: string; + totalPoints: number; + codeTestSolved: number; + codeTutorSolved: number; + codeTrackSolved: number; + dcSolved: number; + goldMedals: number; + silverMedals: number; + bronzeMedals: number; + lastUpdated: string; +} + export type CertDomain = | 'AI & Machine Learning' | 'Database & MongoDB' @@ -308,8 +325,8 @@ function CustomFormSelect({ export default function LeetcodeLeaderboard() { const { user, isAuthenticated, openAuthModal } = useAuth(); - // Top Feature Switcher State (SkillRack removed per request) - const [activeMainTab, setActiveMainTab] = useState<'leetcode' | 'certifications'>('leetcode'); + // Top Feature Switcher State + const [activeMainTab, setActiveMainTab] = useState<'leetcode' | 'skillrack' | 'certifications'>('leetcode'); // Shared Filters State const [searchQuery, setSearchQuery] = useState(''); @@ -333,7 +350,23 @@ export default function LeetcodeLeaderboard() { const [lcYear, setLcYear] = useState('1st Year'); // ───────────────────────────────────────────────────────────── - // 2. Certifications Vault Tab State + // 2. SkillRack Tab State + // ───────────────────────────────────────────────────────────── + const [skillrackProfiles, setSkillrackProfiles] = useState([]); + const [skillrackLoading, setSkillrackLoading] = useState(false); + const [showSkillrackModal, setShowSkillrackModal] = useState(false); + const [isSkillrackSubmitting, setIsSkillrackSubmitting] = useState(false); + const [isSkillrackSyncing, setIsSkillrackSyncing] = useState(false); + + // SkillRack Form + const [srStudentName, setSrStudentName] = useState(''); + const [srEmail, setSrEmail] = useState(''); + const [srPassword, setSrPassword] = useState(''); + const [srDept, setSrDept] = useState('CSE'); + const [srYear, setSrYear] = useState('1st Year'); + + // ───────────────────────────────────────────────────────────── + // 3. Certifications Vault Tab State // ───────────────────────────────────────────────────────────── const [certifications, setCertifications] = useState(() => { try { @@ -360,10 +393,11 @@ export default function LeetcodeLeaderboard() { const [certFile, setCertFile] = useState(null); const [certFileDataUrl, setCertFileDataUrl] = useState(null); - // Fetch LeetCode Leaderboard on Mount if Auth + // Fetch LeetCode + SkillRack Leaderboard on Mount if Auth useEffect(() => { if (isAuthenticated) { fetchLeetcodeLeaderboard(); + fetchSkillrackLeaderboard(); } }, [isAuthenticated]); @@ -380,8 +414,10 @@ export default function LeetcodeLeaderboard() { useEffect(() => { if (user?.name) { setLcStudentName(user.name); + setSrStudentName(user.name); if (user.department) { setLcDept(user.department); + setSrDept(user.department); } } }, [user]); @@ -450,6 +486,76 @@ export default function LeetcodeLeaderboard() { } }; + // ───────────────────────────────────────────────────────────── + // SkillRack Fetch & Register Handlers + // ───────────────────────────────────────────────────────────── + + const fetchSkillrackLeaderboard = async () => { + setSkillrackLoading(true); + try { + const res = await fetch(getBackendUrl('/api/skillrack/leaderboard')); + if (res.ok) { + const data = await res.json(); + setSkillrackProfiles(data); + } + } catch (err) { + console.error('Failed to fetch SkillRack leaderboard:', err); + } finally { + setSkillrackLoading(false); + } + }; + + const handleRegisterSkillrack = async (e: React.FormEvent) => { + e.preventDefault(); + if (!srStudentName.trim() || !srEmail.trim() || !srPassword.trim()) return; + + setIsSkillrackSubmitting(true); + try { + const res = await fetch(getBackendUrl('/api/skillrack/register'), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + studentName: srStudentName, + skillrackEmail: srEmail, + skillrackPassword: srPassword, + department: srDept, + year: srYear, + }), + }); + + if (res.ok) { + setToastMessage('✅ SkillRack profile added! Stats fetched successfully.'); + setShowSkillrackModal(false); + setSrEmail(''); + setSrPassword(''); + fetchSkillrackLeaderboard(); + } else { + const errData = await res.json(); + setToastMessage(`❌ ${errData.error || 'Failed to register SkillRack profile'}`); + } + } catch { + setToastMessage('❌ Error connecting to server'); + } finally { + setIsSkillrackSubmitting(false); + setTimeout(() => setToastMessage(null), 4000); + } + }; + + const handleTriggerSkillrackSync = async () => { + setIsSkillrackSyncing(true); + try { + const res = await fetch(getBackendUrl('/api/skillrack/sync'), { method: 'POST' }); + if (res.ok) { + setToastMessage('⚡ SkillRack background sync started! 5s spacing between profiles.'); + } + } catch { + setToastMessage('❌ Failed to trigger SkillRack sync'); + } finally { + setIsSkillrackSyncing(false); + setTimeout(() => setToastMessage(null), 5000); + } + }; + // Convert File to Base64 Data URL for persistent viewing const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { @@ -530,6 +636,18 @@ export default function LeetcodeLeaderboard() { const totalLcCampusSolved = leetcodeProfiles.reduce((acc, curr) => acc + (curr.totalSolved || 0), 0); const topLcProfile = leetcodeProfiles.length > 0 ? leetcodeProfiles[0] : null; + // SkillRack Filtered + const filteredSkillrack = skillrackProfiles.filter(p => { + const matchesSearch = + p.studentName.toLowerCase().includes(searchQuery.toLowerCase()) || + p.skillrackEmail.toLowerCase().includes(searchQuery.toLowerCase()); + const matchesDept = selectedDept === 'All' || p.department === selectedDept; + const matchesYear = selectedYear === 'All' || p.year === selectedYear; + return matchesSearch && matchesDept && matchesYear; + }); + const totalSrPoints = skillrackProfiles.reduce((acc, curr) => acc + (curr.totalPoints || 0), 0); + const topSrProfile = skillrackProfiles.length > 0 ? skillrackProfiles[0] : null; + // Certifications Domain Counts const certDomainCounts: Record = { 'AI & Machine Learning': certifications.filter(c => c.domain === 'AI & Machine Learning').length, @@ -647,7 +765,7 @@ export default function LeetcodeLeaderboard() { )} - {/* ─── Top Feature Switcher Pills (SkillRack removed per request) ─── */} + {/* ─── Top Feature Switcher Pills ─── */}
+ + + + +
+ + + {/* SkillRack Table */} +
+ {skillrackLoading ? ( +
+ +

Fetching SkillRack rankings...

+
+ ) : filteredSkillrack.length === 0 ? ( +
+ +

No SkillRack profiles found

+

Be the first to register your SkillRack account!

+
+ ) : ( +
+ + + + + + + + + + + + + {filteredSkillrack.map((p, index) => { + const rank = index + 1; + return ( + + + + + + + + + ); + })} + +
RankStudentDept / YearTotal PointsProgramsMedals
+ {rank === 1 ? '🥇' : rank === 2 ? '🥈' : rank === 3 ? '🥉' : `#${rank}`} + +

{p.studentName}

+

{p.skillrackEmail}

+
+ + {p.department} + + {p.year} + + + {p.totalPoints} + + +
+ CT:{p.codeTestSolved} + Tutor:{p.codeTutorSolved} + Track:{p.codeTrackSolved} + DC:{p.dcSolved} +
+
+
+ 🥇{p.goldMedals} + 🥈{p.silverMedals} + 🥉{p.bronzeMedals} +
+
+
+ )} +
+ + )} + + {/* ───────────────────────────────────────────────────────────────────── */} + {/* TAB 3: SKILL CERTIFICATIONS (WITH DOMAIN SELECTOR DROPDOWN & PREVIEW) */} {/* ───────────────────────────────────────────────────────────────────── */} {activeMainTab === 'certifications' && (
@@ -1126,7 +1442,113 @@ export default function LeetcodeLeaderboard() { {/* ───────────────────────────────────────────────────────────────────── */} - {/* MODAL 2: UPLOAD SKILL CERTIFICATION FORM */} + {/* MODAL: REGISTER SKILLRACK ACCOUNT */} + {/* ───────────────────────────────────────────────────────────────────── */} + + {showSkillrackModal && ( +
+ + + +
+ +

+ Register SkillRack Account +

+
+

Enter your SkillRack credentials to fetch your stats. Your password is stored encrypted on our server for automatic daily sync.

+ +
+
+ + setSrStudentName(e.target.value)} + placeholder="e.g. S DEVESH" + className="w-full px-3.5 py-2.5 rounded-xl border border-[#E2E8F0] text-sm text-[#1E293B] focus:outline-none focus:border-[#F97316]" + /> +
+ +
+ + setSrEmail(e.target.value)} + placeholder="e.g. student@ritchennai.edu.in" + className="w-full px-3.5 py-2.5 rounded-xl border border-[#E2E8F0] text-sm text-[#1E293B] focus:outline-none focus:border-[#F97316]" + /> +
+ +
+ + setSrPassword(e.target.value)} + placeholder="Your SkillRack password" + className="w-full px-3.5 py-2.5 rounded-xl border border-[#E2E8F0] text-sm text-[#1E293B] focus:outline-none focus:border-[#F97316]" + /> +

+ Encrypted with AES-256 · Used only for stat fetching +

+
+ +
+
+ + +
+ +
+ + +
+
+ +
+ +
+
+
+
+ )} +
+ + {/* ───────────────────────────────────────────────────────────────────── */} + {/* MODAL: UPLOAD SKILL CERTIFICATION FORM */} {/* ───────────────────────────────────────────────────────────────────── */} {showCertModal && (