From b560af0120aacc95de771fa14b36a4964a33414d Mon Sep 17 00:00:00 2001 From: Avaneesh Kesavan Date: Tue, 30 Sep 2025 23:40:09 +0530 Subject: [PATCH 1/6] Fix --- frontend/src/Components/Dashboard.jsx | 43 ++++++++++----- frontend/src/Components/GitHubCard.jsx | 10 +++- frontend/src/Components/GitHubProfile.jsx | 67 ++++++++++++++--------- 3 files changed, 76 insertions(+), 44 deletions(-) diff --git a/frontend/src/Components/Dashboard.jsx b/frontend/src/Components/Dashboard.jsx index a952726..24ab782 100644 --- a/frontend/src/Components/Dashboard.jsx +++ b/frontend/src/Components/Dashboard.jsx @@ -36,21 +36,32 @@ export default function Dashboard() { if (!res.ok) throw new Error(data.errors?.[0]?.msg || "Failed to load profile"); - setProfile(data); + setProfile(data || {}); // default to empty object setGoals(data.goals || []); } catch (err) { - setError(err.message); + setError(err.message || "Failed to load profile"); } finally { setLoading(false); } }; fetchProfile(); - }, []); + }, [navigate]); if (loading) return

Loading...

; if (error) return

{error}

; + // Default values to prevent crashes + const safeProfile = { + githubUsername: "", + streak: 0, + notes: [], + timeSpent: "0 minutes", + activity: [], + socialLinks: [], + ...profile, + }; + return (
@@ -59,15 +70,19 @@ export default function Dashboard() {
{/* Row 1 */} - - - - - + + + {/* GitHub Card (conditionally rendered) */} - {profile.githubUsername ? ( - + {safeProfile.githubUsername ? ( + ) : (
GitHub profile not linked @@ -76,15 +91,15 @@ export default function Dashboard() { {/* Row 2: Goals, Time Spent, Notes */} - + setProfile({ ...profile, notes: n })} + notes={safeProfile.notes} + onNotesChange={(n) => setProfile({ ...safeProfile, notes: n })} /> {/* Row 3: Activity heatmap full width */}
- +
diff --git a/frontend/src/Components/GitHubCard.jsx b/frontend/src/Components/GitHubCard.jsx index 09cda06..27283ac 100644 --- a/frontend/src/Components/GitHubCard.jsx +++ b/frontend/src/Components/GitHubCard.jsx @@ -19,9 +19,11 @@ const GitHubCard = ({ githubUsername }) => { import.meta.env.VITE_BACKEND_URL || "http://localhost:5000"; const res = await fetch(`${backendUrl}/api/github/${githubUsername}`); const json = await res.json(); - setValidUser(res.ok && !json.error); + + // Mark valid only if response is ok and profile data exists + setValidUser(res.ok && json && !json.error && json.profile); } catch (err) { - console.error(err); + console.error("Error checking GitHub user:", err); setValidUser(false); } finally { setLoading(false); @@ -44,7 +46,9 @@ const GitHubCard = ({ githubUsername }) => { ) : (
- GitHub username not found + {githubUsername + ? `GitHub username "${githubUsername}" not found` + : "No GitHub username provided"}
)} diff --git a/frontend/src/Components/GitHubProfile.jsx b/frontend/src/Components/GitHubProfile.jsx index 8d677e9..3701ad6 100644 --- a/frontend/src/Components/GitHubProfile.jsx +++ b/frontend/src/Components/GitHubProfile.jsx @@ -39,7 +39,7 @@ const GitHubProfile = () => { if (loading) return

Loading...

; if (error) return

{error}

; - // Default values to prevent crashes + // Defaults to prevent crashes const { profile = {}, topRepos = [], @@ -48,20 +48,22 @@ const GitHubProfile = () => { } = data || {}; const renderHeatmap = () => { - if (!contributions.weeks.length) - return

No contribution data available.

; + if (!contributions?.weeks?.length) { + return

No contribution data available.

; + } + return (
{contributions.weeks.map((week, wIdx) => (
- {week.contributionDays.map((day, dIdx) => ( + {week?.contributionDays?.map((day, dIdx) => (
))}
@@ -71,29 +73,38 @@ const GitHubProfile = () => { }; const renderLanguages = () => { - if (!Object.keys(languages).length) - return

No language data available.

; + const langKeys = Object.keys(languages); + if (!langKeys.length) + return

No language data available.

; + const totalSize = Object.values(languages).reduce( (sum, val) => sum + val, 0 ); + return (
- {Object.entries(languages) - .sort((a, b) => b[1] - a[1]) - .map(([lang, size], idx) => ( -
-

- {lang} ({((size / totalSize) * 100).toFixed(1)}%) -

-
-
+ {langKeys + .sort((a, b) => languages[b] - languages[a]) + .map((lang, idx) => { + const size = languages[lang]; + const percentage = totalSize + ? ((size / totalSize) * 100).toFixed(1) + : 0; + return ( +
+

+ {lang} ({percentage}%) +

+
+
+
-
- ))} + ); + })}
); }; @@ -137,13 +148,15 @@ const GitHubProfile = () => { - {/* Top Repos */} + {/* Top Repositories */} Top Repositories - {topRepos.length === 0 &&

No repositories available.

} + {topRepos.length === 0 && ( +

No repositories available.

+ )} {topRepos.map((repo, idx) => (
{ >

- {repo.name} + {repo.name || "N/A"}

From ec1febaeef25bfed966e0bed37ebfd954965c33d Mon Sep 17 00:00:00 2001 From: AvaneeshKesavan Date: Sat, 4 Oct 2025 15:51:07 +0530 Subject: [PATCH 2/6] Fix --- .../Components/DashBoard/PlatformLinks.jsx | 125 ++++++++++++------ .../src/Components/DashBoard/ProfileCard.jsx | 65 ++++++--- frontend/src/Components/Dashboard.jsx | 54 +++++++- frontend/src/Components/GitHubCard.jsx | 58 -------- 4 files changed, 179 insertions(+), 123 deletions(-) delete mode 100644 frontend/src/Components/GitHubCard.jsx diff --git a/frontend/src/Components/DashBoard/PlatformLinks.jsx b/frontend/src/Components/DashBoard/PlatformLinks.jsx index 654530f..e24340b 100644 --- a/frontend/src/Components/DashBoard/PlatformLinks.jsx +++ b/frontend/src/Components/DashBoard/PlatformLinks.jsx @@ -1,4 +1,11 @@ -import { SiCodechef, SiHackerrank, SiLeetcode, SiHackerearth, SiGithub, } from "react-icons/si"; +import { + SiCodechef, + SiHackerrank, + SiLeetcode, + SiHackerearth, + SiGithub, +} from "react-icons/si"; +import { Link } from "react-router-dom"; const iconMap = { codechef: SiCodechef, @@ -8,56 +15,92 @@ const iconMap = { leetcode: SiLeetcode, }; - function normalizeLeetcodeURL(url) { - const leetcodeRegex = /^https?:\/\/(www\.)?leetcode\.com\/(u\/)?[a-zA-Z0-9_-]+\/?$/; - if (!leetcodeRegex.test(url)) { - return null; - } - return url.replace(/\/$/, ''); - } +function normalizeLeetcodeURL(url) { + const leetcodeRegex = + /^https?:\/\/(www\.)?leetcode\.com\/(u\/)?[a-zA-Z0-9_-]+\/?$/; + if (!leetcodeRegex.test(url)) return null; + return url.replace(/\/$/, ""); +} const leetcodeUrl = (url) => { - url = normalizeLeetcodeURL(url) + url = normalizeLeetcodeURL(url); const username = url.trim().split("/").pop(); - return `/leetcode/${username}` + return `/leetcode/${username}`; +}; + +function normalizeGitHubURL(url) { + const githubRegex = /^https?:\/\/(www\.)?github\.com\/[a-zA-Z0-9_-]+\/?$/; + if (!githubRegex.test(url)) return null; + return url.replace(/\/$/, ""); } +const githubUrl = (url) => { + const normalized = normalizeGitHubURL(url); + if (!normalized) return "#"; + const username = normalized.split("/").pop(); + return `/dashboard/github/${username}`; +}; + export default function PlatformLinks({ platforms }) { const platformEntries = Object.entries(platforms); return ( -

+
{platformEntries.length > 0 ? ( - platformEntries.map(([name, url], i) => { - const Icon = iconMap[name.toLowerCase()] || SiGithub; - - return ( - - -
- - {name} - - - Active - -
-
- ); - }) - ) : ( -
- - No platforms linked yet - -
- )} + platformEntries.map(([name, url], i) => { + const Icon = iconMap[name.toLowerCase()] || SiGithub; + let href = url || "#"; + + if (name.toLowerCase() === "leetcode") href = leetcodeUrl(url); + if (name.toLowerCase() === "github") href = githubUrl(url); + + // Use Link for internal routes (GitHub/LeetCode), a for external if needed + const isInternal = + name.toLowerCase() === "leetcode" || + name.toLowerCase() === "github"; + return isInternal ? ( + + +
+ + {name} + + + Active + +
+ + ) : ( + + +
+ + {name} + + + Active + +
+
+ ); + }) + ) : ( +
+ + No platforms linked yet + +
+ )}
); } diff --git a/frontend/src/Components/DashBoard/ProfileCard.jsx b/frontend/src/Components/DashBoard/ProfileCard.jsx index b680647..74dc831 100644 --- a/frontend/src/Components/DashBoard/ProfileCard.jsx +++ b/frontend/src/Components/DashBoard/ProfileCard.jsx @@ -2,7 +2,12 @@ import { User } from "lucide-react"; import React from "react"; import CardWrapper from "./CardWrapper"; import { - SiLeetcode, SiCodechef, SiHackerrank, SiGithub, SiHackerearth, SiLinkedin, + SiLeetcode, + SiCodechef, + SiHackerrank, + SiGithub, + SiHackerearth, + SiLinkedin, } from "react-icons/si"; const iconMap = { @@ -15,23 +20,29 @@ const iconMap = { }; export default function ProfileCard({ user }) { - if (!user) return null; // don't render until user is loaded + if (!user) return null; const socialLinks = user.socialLinks || {}; const entries = Object.entries(socialLinks); + // Normalize LeetCode URL function normalizeLeetcodeURL(url) { - const leetcodeRegex = /^https?:\/\/(www\.)?leetcode\.com\/(u\/)?[a-zA-Z0-9_-]+\/?$/; - if (!leetcodeRegex.test(url)) { - return null; - } - return url.replace(/\/$/, ''); + const leetcodeRegex = + /^https?:\/\/(www\.)?leetcode\.com\/(u\/)?[a-zA-Z0-9_-]+\/?$/; + if (!leetcodeRegex.test(url)) return null; + return url.replace(/\/$/, ""); + } + + // Normalize GitHub URL + function normalizeGitHubURL(url) { + const githubRegex = /^https?:\/\/(www\.)?github\.com\/[a-zA-Z0-9_-]+\/?$/; + if (!githubRegex.test(url)) return null; + return url.replace(/\/$/, ""); } return ( {/* Header */}
- {/* Avatar */}
{user.avatar ? ( - {/* Name + Email */}
-

{user.name}

+

+ {user.name} +

{user.email}

{/* Platforms */}
-

Platforms

+

+ Platforms +

{entries.length > 0 ? (
{entries.map(([platformName, url]) => { const Icon = iconMap[platformName.toLowerCase()]; if (!Icon) return null; + + // Internal route for LeetCode const leetcodeUrl = (url) => { - url = normalizeLeetcodeURL(url); - const username = url.split("/").pop(); - return `/leetcode/${username}` + const normalized = normalizeLeetcodeURL(url); + if (!normalized) return "#"; + const username = normalized.split("/").pop(); + return `/leetcode/${username}`; + }; + + // Internal route for GitHub + const githubUrl = (url) => { + const normalized = normalizeGitHubURL(url); + if (!normalized) return "#"; + const username = normalized.split("/").pop(); + return `/dashboard/github/${username}`; + }; + + let href = url; + if (platformName.toLowerCase() === "leetcode") + href = leetcodeUrl(url); + if (platformName.toLowerCase() === "github") + href = githubUrl(url); - } return ( diff --git a/frontend/src/Components/Dashboard.jsx b/frontend/src/Components/Dashboard.jsx index 24ab782..fa195b8 100644 --- a/frontend/src/Components/Dashboard.jsx +++ b/frontend/src/Components/Dashboard.jsx @@ -8,8 +8,8 @@ import GoalsCard from "./DashBoard/GoalsCard"; import TimeSpentCard from "./DashBoard/TimeSpentCard"; import ActivityHeatmap from "./DashBoard/ActivityHeatMap"; import NotesCard from "./DashBoard/NotesCard"; -import { useNavigate } from "react-router-dom"; import GitHubCard from "@/Components/GitHubCard"; +import { useNavigate } from "react-router-dom"; export default function Dashboard() { const [profile, setProfile] = useState(null); @@ -19,6 +19,18 @@ export default function Dashboard() { const navigate = useNavigate(); useEffect(() => { + const params = new URLSearchParams(window.location.search); + const oauthToken = params.get("token"); + if (oauthToken) { + try { + localStorage.setItem("token", oauthToken); + } catch (e) { + console.error("Failed to persist OAuth token:", e); + } + const cleanUrl = window.location.origin + window.location.pathname; + window.history.replaceState({}, document.title, cleanUrl); + } + const fetchProfile = async () => { try { const token = localStorage.getItem("token"); @@ -33,12 +45,14 @@ export default function Dashboard() { }); const data = await res.json(); - if (!res.ok) + if (!res.ok) { throw new Error(data.errors?.[0]?.msg || "Failed to load profile"); + } - setProfile(data || {}); // default to empty object + setProfile(data || {}); setGoals(data.goals || []); } catch (err) { + console.error("Error fetching profile:", err); setError(err.message || "Failed to load profile"); } finally { setLoading(false); @@ -48,10 +62,36 @@ export default function Dashboard() { fetchProfile(); }, [navigate]); - if (loading) return

Loading...

; - if (error) return

{error}

; + if (loading) { + return ( +
+
+
+ ); + } + + if (error) { + return ( +
+

Error: {error}

+ +
+ ); + } + + if (!profile) { + return ( +
+

No profile data available. Please try logging in again.

+
+ ); + } - // Default values to prevent crashes const safeProfile = { githubUsername: "", streak: 0, @@ -77,7 +117,7 @@ export default function Dashboard() { /> - {/* GitHub Card (conditionally rendered) */} + {/* GitHub Card (internal routing) */} {safeProfile.githubUsername ? ( { - const [loading, setLoading] = useState(true); - const [validUser, setValidUser] = useState(false); - - useEffect(() => { - if (!githubUsername) { - setLoading(false); - setValidUser(false); - return; - } - - const checkGitHubUser = async () => { - try { - const backendUrl = - import.meta.env.VITE_BACKEND_URL || "http://localhost:5000"; - const res = await fetch(`${backendUrl}/api/github/${githubUsername}`); - const json = await res.json(); - - // Mark valid only if response is ok and profile data exists - setValidUser(res.ok && json && !json.error && json.profile); - } catch (err) { - console.error("Error checking GitHub user:", err); - setValidUser(false); - } finally { - setLoading(false); - } - }; - - checkGitHubUser(); - }, [githubUsername]); - - return ( - - {loading ? ( -

Checking GitHub...

- ) : validUser ? ( - - -

GitHub

-

View {githubUsername}'s GitHub profile

-
- - ) : ( -
- {githubUsername - ? `GitHub username "${githubUsername}" not found` - : "No GitHub username provided"} -
- )} -
- ); -}; - -export default GitHubCard; From 1ac6991babaae8278562332902f82cac5ea99d84 Mon Sep 17 00:00:00 2001 From: AvaneeshKesavan Date: Sat, 4 Oct 2025 15:55:35 +0530 Subject: [PATCH 3/6] Fix --- .../Components/DashBoard/PlatformLinks.jsx | 61 +++++++++---------- .../src/Components/DashBoard/ProfileCard.jsx | 34 +++++------ frontend/src/Components/GitHubProfile.jsx | 15 ++--- 3 files changed, 50 insertions(+), 60 deletions(-) diff --git a/frontend/src/Components/DashBoard/PlatformLinks.jsx b/frontend/src/Components/DashBoard/PlatformLinks.jsx index e24340b..7dd6cd5 100644 --- a/frontend/src/Components/DashBoard/PlatformLinks.jsx +++ b/frontend/src/Components/DashBoard/PlatformLinks.jsx @@ -22,18 +22,19 @@ function normalizeLeetcodeURL(url) { return url.replace(/\/$/, ""); } -const leetcodeUrl = (url) => { - url = normalizeLeetcodeURL(url); - const username = url.trim().split("/").pop(); - return `/leetcode/${username}`; -}; - function normalizeGitHubURL(url) { const githubRegex = /^https?:\/\/(www\.)?github\.com\/[a-zA-Z0-9_-]+\/?$/; if (!githubRegex.test(url)) return null; return url.replace(/\/$/, ""); } +const leetcodeUrl = (url) => { + const normalized = normalizeLeetcodeURL(url); + if (!normalized) return "#"; + const username = normalized.split("/").pop(); + return `/leetcode/${username}`; +}; + const githubUrl = (url) => { const normalized = normalizeGitHubURL(url); if (!normalized) return "#"; @@ -49,35 +50,33 @@ export default function PlatformLinks({ platforms }) { {platformEntries.length > 0 ? ( platformEntries.map(([name, url], i) => { const Icon = iconMap[name.toLowerCase()] || SiGithub; - let href = url || "#"; - if (name.toLowerCase() === "leetcode") href = leetcodeUrl(url); - if (name.toLowerCase() === "github") href = githubUrl(url); + // Internal route for GitHub + if (name.toLowerCase() === "github") { + return ( + + +
+ + {name} + + + Active + +
+ + ); + } - // Use Link for internal routes (GitHub/LeetCode), a for external if needed - const isInternal = - name.toLowerCase() === "leetcode" || - name.toLowerCase() === "github"; - return isInternal ? ( - - -
- - {name} - - - Active - -
- - ) : ( + // Other platforms + return (
{ + const normalized = normalizeLeetcodeURL(url); + if (!normalized) return "#"; + const username = normalized.split("/").pop(); + return `/leetcode/${username}`; + }; + + const githubUrl = (url) => { + const normalized = normalizeGitHubURL(url); + if (!normalized) return "#"; + const username = normalized.split("/").pop(); + return `/dashboard/github/${username}`; + }; + return ( - {/* Header */}
{user.avatar ? ( @@ -63,7 +74,6 @@ export default function ProfileCard({ user }) {
- {/* Platforms */}

Platforms @@ -74,22 +84,6 @@ export default function ProfileCard({ user }) { const Icon = iconMap[platformName.toLowerCase()]; if (!Icon) return null; - // Internal route for LeetCode - const leetcodeUrl = (url) => { - const normalized = normalizeLeetcodeURL(url); - if (!normalized) return "#"; - const username = normalized.split("/").pop(); - return `/leetcode/${username}`; - }; - - // Internal route for GitHub - const githubUrl = (url) => { - const normalized = normalizeGitHubURL(url); - if (!normalized) return "#"; - const username = normalized.split("/").pop(); - return `/dashboard/github/${username}`; - }; - let href = url; if (platformName.toLowerCase() === "leetcode") href = leetcodeUrl(url); diff --git a/frontend/src/Components/GitHubProfile.jsx b/frontend/src/Components/GitHubProfile.jsx index 3701ad6..90cdc4b 100644 --- a/frontend/src/Components/GitHubProfile.jsx +++ b/frontend/src/Components/GitHubProfile.jsx @@ -17,10 +17,14 @@ const GitHubProfile = () => { return; } + const normalizedUsername = username.split("/").pop(); + try { const backendUrl = import.meta.env.VITE_BACKEND_URL || "http://localhost:5000"; - const res = await fetch(`${backendUrl}/api/github/${username}`); + const res = await fetch( + `${backendUrl}/api/github/${normalizedUsername}` + ); const json = await res.json(); if (res.ok) setData(json); @@ -39,7 +43,6 @@ const GitHubProfile = () => { if (loading) return

Loading...

; if (error) return

{error}

; - // Defaults to prevent crashes const { profile = {}, topRepos = [], @@ -48,9 +51,8 @@ const GitHubProfile = () => { } = data || {}; const renderHeatmap = () => { - if (!contributions?.weeks?.length) { + if (!contributions?.weeks?.length) return

No contribution data available.

; - } return (
@@ -76,7 +78,6 @@ const GitHubProfile = () => { const langKeys = Object.keys(languages); if (!langKeys.length) return

No language data available.

; - const totalSize = Object.values(languages).reduce( (sum, val) => sum + val, 0 @@ -111,7 +112,6 @@ const GitHubProfile = () => { return (
- {/* Profile */} GitHub Profile @@ -148,7 +148,6 @@ const GitHubProfile = () => { - {/* Top Repositories */} Top Repositories @@ -188,7 +187,6 @@ const GitHubProfile = () => { - {/* Contributions Heatmap */} Contribution Heatmap @@ -196,7 +194,6 @@ const GitHubProfile = () => { {renderHeatmap()} - {/* Languages Chart */} Languages Used From 6c981cc779af83797927baffcf8a98345cbe6092 Mon Sep 17 00:00:00 2001 From: AvaneeshKesavan Date: Sat, 4 Oct 2025 21:36:23 +0530 Subject: [PATCH 4/6] Fix --- backend/env.example | 3 +- backend/routes/github.route.js | 27 +- backend/routes/profile.js | 595 ++++------------------ frontend/src/Components/Dashboard.jsx | 15 +- frontend/src/Components/GitHubProfile.jsx | 213 +++++--- frontend/src/index.css | 2 + package-lock.json | 34 +- package.json | 1 + 8 files changed, 291 insertions(+), 599 deletions(-) diff --git a/backend/env.example b/backend/env.example index 9a73ee5..26d4331 100644 --- a/backend/env.example +++ b/backend/env.example @@ -10,4 +10,5 @@ ADMIN_EMAIL= RESEND_API_KEY = EMAIL_USER= EMAIL_PASSWORD -EMAIL_VERIFIER_API_KEY= \ No newline at end of file +EMAIL_VERIFIER_API_KEY= +GITHUB_TOKEN= \ No newline at end of file diff --git a/backend/routes/github.route.js b/backend/routes/github.route.js index 2bb9d83..5ebb22f 100644 --- a/backend/routes/github.route.js +++ b/backend/routes/github.route.js @@ -4,7 +4,7 @@ const fetch = (...args) => import("node-fetch").then(({ default: fetch }) => fetch(...args)); const router = express.Router(); -// Helper function to run GraphQL query +// Helper to run GitHub GraphQL queries const runGraphQL = async (query, variables = {}) => { if (!process.env.GITHUB_TOKEN) { throw new Error("GitHub token not configured in environment variables"); @@ -82,7 +82,7 @@ router.get("/:username", async (req, res) => { if (!user) return res.status(404).json({ error: "User not found" }); - // Aggregate top 6 repos by stars + // Top 6 repos by stars const topRepos = user.repositories.nodes .sort((a, b) => b.stargazerCount - a.stargazerCount) .slice(0, 6) @@ -98,26 +98,23 @@ router.get("/:username", async (req, res) => { })), })); - // Aggregate languages across all repos by size → return array + // Aggregate languages across all repos const languages = {}; user.repositories.nodes.forEach((repo) => { repo.languages.edges.forEach(({ node, size }) => { languages[node.name] = (languages[node.name] || 0) + size; }); }); - const languagesArray = Object.entries(languages).map(([name, size]) => ({ - name, - size, - })); - // Flatten contribution days for easier frontend rendering - const allContributionDays = []; - user.contributionsCollection.contributionCalendar.weeks.forEach((week) => { - week.contributionDays.forEach((day) => { - allContributionDays.push(day); - }); + // Format languages as object for frontend + const languagesObj = {}; + Object.entries(languages).forEach(([name, size]) => { + languagesObj[name] = size; }); + // Heatmap: weeks array + const weeks = user.contributionsCollection.contributionCalendar.weeks; + res.json({ profile: { login: user.login, @@ -132,9 +129,9 @@ router.get("/:username", async (req, res) => { totalContributions: user.contributionsCollection.contributionCalendar.totalContributions, totalCommits: user.contributionsCollection.totalCommitContributions, - heatmap: allContributionDays, // flattened heatmap array + weeks, // keep weeks structure for frontend heatmap }, - languages: languagesArray, // array for frontend use + languages: languagesObj, // object format for frontend bar chart }); } catch (err) { console.error("GitHub API error:", err); diff --git a/backend/routes/profile.js b/backend/routes/profile.js index 07ff0db..7a2896e 100644 --- a/backend/routes/profile.js +++ b/backend/routes/profile.js @@ -1,73 +1,50 @@ const express = require('express'); const router = express.Router(); const { check, validationResult } = require('express-validator'); -const auth = require('../middleware/auth'); const multer = require('multer'); const path = require('path'); -const User = require('../models/User'); const fs = require('fs'); const crypto = require('crypto'); -const LeetCode = require("../models/Leetcode") +const User = require('../models/User'); +const LeetCode = require("../models/Leetcode"); -// Helper function to generate avatar URL from email or name +// Helper to generate avatar URL const generateAvatarUrl = (email, name) => { - // Use email for consistent avatar, or fallback to name const identifier = email || name || 'user'; - const md5Hash = crypto.createHash('md5').update(identifier.toLowerCase().trim()).digest('hex'); - - // Choose one of these services: - // 1. Gravatar - // const gravatarUrl = `https://www.gravatar.com/avatar/${md5Hash}?d=identicon&s=400`; - - // 2. DiceBear (more modern styled avatars) - const diceBearStyle = 'micah'; // Options: avataaars, bottts, initials, micah, miniavs, etc. - const diceBearUrl = `https://api.dicebear.com/6.x/${diceBearStyle}/svg?seed=${encodeURIComponent(identifier)}`; - - // 3. UI Avatars (text based) - const uiAvatarsUrl = `https://ui-avatars.com/api/?name=${encodeURIComponent(name || 'User')}&background=random&size=128`; - - // Return your preferred avatar service - return diceBearUrl; + const diceBearStyle = 'micah'; + return `https://api.dicebear.com/6.x/${diceBearStyle}/svg?seed=${encodeURIComponent(identifier)}`; }; -// Set up multer for file uploads +// Multer setup const storage = multer.diskStorage({ - destination: function(req, file, cb) { + destination: function (req, file, cb) { const uploadDir = 'uploads/avatars'; - if (!fs.existsSync(uploadDir)) { - fs.mkdirSync(uploadDir, { recursive: true }); - } + if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true }); cb(null, uploadDir); }, - filename: function(req, file, cb) { + filename: function (req, file, cb) { cb(null, `${Date.now()}-${file.originalname}`); } }); - -const upload = multer({ +const upload = multer({ storage: storage, - limits: { fileSize: 2000000 }, // 2MB limit - fileFilter: function(req, file, cb) { + limits: { fileSize: 2 * 1024 * 1024 }, // 2MB + fileFilter: function (req, file, cb) { const filetypes = /jpeg|jpg|png/; const extname = filetypes.test(path.extname(file.originalname).toLowerCase()); const mimetype = filetypes.test(file.mimetype); - if (mimetype && extname) { - return cb(null, true); - } else { - cb('Error: Images Only!'); - } + if (mimetype && extname) return cb(null, true); + cb('Error: Images Only!'); } }); -// @route GET api/profile -// @desc Get current user's profile -// @access Private -router.get('/', auth, async (req, res) => { +// ---------------------- ROUTES ---------------------- + +// Get profile (dev mode: fetch first user) +router.get('/', async (req, res) => { try { - const user = await User.findById(req.user.id).select('-password'); - if (!user) { - return res.status(400).json({ errors: [{ msg: 'User not found' }] }); - } + const user = await User.findOne().select('-password'); + if (!user) return res.status(404).json({ errors: [{ msg: 'No user found' }] }); res.json(user); } catch (err) { console.error(err.message); @@ -75,36 +52,23 @@ router.get('/', auth, async (req, res) => { } }); -// @route POST api/profile/avatar -// @desc Upload user avatar -// @access Private -router.post('/avatar', auth, upload.single('avatar'), async (req, res) => { +// Upload avatar +router.post('/avatar', upload.single('avatar'), async (req, res) => { try { - const user = await User.findById(req.user.id).select('-password'); - if (!user) { - return res.status(404).json({ errors: [{ msg: 'User not found' }] }); - } + const user = await User.findOne().select('-password'); + if (!user) return res.status(404).json({ errors: [{ msg: 'User not found' }] }); - // If a file was uploaded, use that if (req.file) { - // Delete old avatar if it's stored locally and not the default + // Delete old local avatar if (user.avatar && user.avatar.startsWith('/uploads/')) { - const oldAvatarPath = path.join(__dirname, '..', user.avatar); - if (fs.existsSync(oldAvatarPath)) { - fs.unlinkSync(oldAvatarPath); - } + const oldPath = path.join(__dirname, '..', user.avatar); + if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath); } - - // Update user with new avatar path - const avatarPath = `/${req.file.path.replace(/\\/g, '/')}`; - user.avatar = avatarPath; - } - // Otherwise generate an avatar from dicebear or similar service - else { - const newAvatar = generateAvatarUrl(user.email, user.name); - user.avatar = newAvatar; + user.avatar = `/${req.file.path.replace(/\\/g, '/')}`; + } else { + user.avatar = generateAvatarUrl(user.email, user.name); } - + await user.save(); res.json({ avatar: user.avatar }); } catch (err) { @@ -113,130 +77,32 @@ router.post('/avatar', auth, upload.single('avatar'), async (req, res) => { } }); -// @route PUT api/profile -// @desc Update user profile -// @access Private +// Update profile router.put('/', [ - auth, - [ - check('name', 'Name is required').not().isEmpty() - ] + check('name', 'Name is required').not().isEmpty() ], async (req, res) => { const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } + if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() }); const { - name, - bio, - location, - skills, - github, - gitlab, - linkedin, - twitter, - website, - // Competitive coding platforms - codechef, - hackerrank, - leetcode, - codeforces, - hackerearth + name, bio, location, skills, github, gitlab, linkedin, twitter, website, + codechef, hackerrank, leetcode, codeforces, hackerearth } = req.body; - // Build profile object - const profileFields = {}; - if (name) profileFields.name = name; - if (bio) profileFields.bio = bio; - if (location) profileFields.location = location; - if (skills && Array.isArray(skills)) { - profileFields.skills = skills; - } else if (skills) { - profileFields.skills = skills.split(',').map(skill => skill.trim()); - } - - // Build social object - profileFields.socialLinks = {}; - if (github) profileFields.socialLinks.github = github; - if (gitlab) profileFields.socialLinks.gitlab = gitlab; - if (linkedin) profileFields.socialLinks.linkedin = linkedin; - if (twitter) profileFields.socialLinks.twitter = twitter; - if (website) profileFields.socialLinks.website = website; - - // Add competitive coding platforms - if (codechef) profileFields.socialLinks.codechef = codechef; - if (hackerrank) profileFields.socialLinks.hackerrank = hackerrank; - if (leetcode) profileFields.socialLinks.leetcode = leetcode; - if (codeforces) profileFields.socialLinks.codeforces = codeforces; - if (hackerearth) profileFields.socialLinks.hackerearth = hackerearth; - - try { - let user = await User.findById(req.user.id); - - if (!user) { - return res.status(404).json({ errors: [{ msg: 'User not found' }] }); - } - - // Update - user = await User.findByIdAndUpdate( - req.user.id, - { $set: profileFields }, - { new: true } - ).select('-password'); - - return res.json(user); - } catch (err) { - console.error(err.message); - res.status(500).json({ errors: [{ msg: 'Server Error' }] }); - } -}); - -// Added a route to generate a new avatar from online services -router.post('/generate-avatar', auth, async (req, res) => { - try { - const user = await User.findById(req.user.id).select('-password'); - if (!user) { - return res.status(404).json({ errors: [{ msg: 'User not found' }] }); - } - - // Generate a new avatar using online service - const newAvatar = generateAvatarUrl(user.email, user.name); - user.avatar = newAvatar; - await user.save(); - - res.json({ avatar: newAvatar }); - } catch (err) { - console.error(err.message); - res.status(500).json({ errors: [{ msg: 'Server Error' }] }); - } -}); - -// @route POST api/profile/projects -// @desc Add project to profile -// @access Private -router.post('/projects', [ - auth, - [ - check('name', 'Project name is required').not().isEmpty(), - check('description', 'Description is required').not().isEmpty() - ] -], async (req, res) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - - const { name, description, link } = req.body; - try { - const user = await User.findById(req.user.id); - - user.projects.unshift({ - name, - description, - link - }); + const user = await User.findOne(); + if (!user) return res.status(404).json({ errors: [{ msg: 'User not found' }] }); + + // Update fields + if (name) user.name = name; + if (bio) user.bio = bio; + if (location) user.location = location; + if (skills) user.skills = Array.isArray(skills) ? skills : skills.split(',').map(s => s.trim()); + + user.socialLinks = { + github, gitlab, linkedin, twitter, website, + codechef, hackerrank, leetcode, codeforces, hackerearth + }; await user.save(); res.json(user); @@ -246,170 +112,45 @@ router.post('/projects', [ } }); -// @route DELETE api/profile/projects/:proj_id -// @desc Delete project from profile -// @access Private -router.delete('/projects/:proj_id', auth, async (req, res) => { +// Generate new avatar +router.post('/generate-avatar', async (req, res) => { try { - const user = await User.findById(req.user.id); - - // Get remove index - const removeIndex = user.projects - .map(item => item.id) - .indexOf(req.params.proj_id); + const user = await User.findOne().select('-password'); + if (!user) return res.status(404).json({ errors: [{ msg: 'User not found' }] }); - if (removeIndex === -1) { - return res.status(404).json({ errors: [{ msg: 'Project not found' }] }); - } - - user.projects.splice(removeIndex, 1); + const newAvatar = generateAvatarUrl(user.email, user.name); + user.avatar = newAvatar; await user.save(); - res.json(user); + res.json({ avatar: newAvatar }); } catch (err) { console.error(err.message); res.status(500).json({ errors: [{ msg: 'Server Error' }] }); } }); -// @route PUT api/profile/goals -// @desc Update user goals -// @access Private -router.put('/goals', auth, async (req, res) => { - try { - const user = await User.findById(req.user.id); - if (!user) return res.status(404).json({ msg: 'User not found' }); - - user.goals = req.body.goals || []; - await user.save(); - res.json(user.goals); - } catch (err) { - console.error(err.message); - res.status(500).send('Server error'); - } -}); - -// @route PUT api/profile/notes -// @desc Update user notes -// @access Private -router.put('/notes', auth, async (req, res) => { - try { - const user = await User.findById(req.user.id); - if (!user) return res.status(404).json({ msg: 'User not found' }); - - user.notes = req.body.notes || ""; - await user.save(); - res.json(user.notes); - } catch (err) { - console.error(err.message); - res.status(500).send('Server error'); - } -}); - -// @route PUT api/profile/activity -// @desc Update activity log (for heatmap) -// @access Private -router.put('/activity', auth, async (req, res) => { - try { - const { date } = req.body; // expects YYYY-MM-DD or timestamp - const user = await User.findById(req.user.id); - if (!user) return res.status(404).json({ msg: 'User not found' }); - - user.activity.push(date); - await user.save(); - res.json(user.activity); - } catch (err) { - console.error(err.message); - res.status(500).send('Server error'); - } -}); - -// @route PUT api/profile/time -// @desc Update time spent -// @access Private -router.put('/time', auth, async (req, res) => { - try { - const { timeSpent } = req.body; // e.g. "2h 30m" - const user = await User.findById(req.user.id); - if (!user) return res.status(404).json({ msg: 'User not found' }); - - user.timeSpent = timeSpent; - await user.save(); - res.json(user.timeSpent); - } catch (err) { - console.error(err.message); - res.status(500).send('Server error'); - } -}); - - - +// ---------------- LEETCODE ROUTES (unchanged) ---------------- router.post("/leetcode/:username", async (req, res) => { const { username } = req.params; - try { const existingUser = await LeetCode.findOne({ username }); - if (existingUser) { - return res.json({ - message: "LeetCode data fetched from database.", - data: existingUser, - }); - } + if (existingUser) return res.json({ message: "LeetCode data fetched from DB", data: existingUser }); const response = await fetch("https://leetcode.com/graphql", { method: "POST", - headers: { - "Content-Type": "application/json", - "User-Agent": "Mozilla/5.0", - }, + headers: { "Content-Type": "application/json", "User-Agent": "Mozilla/5.0" }, body: JSON.stringify({ query: ` query LeetCodeProfile($username: String!, $limit: Int!) { matchedUser(username: $username) { - username - profile { - ranking - userAvatar - } - submitStatsGlobal { - acSubmissionNum { - difficulty - count - } - } - badges { - id - displayName - icon - } + username profile { ranking userAvatar } + submitStatsGlobal { acSubmissionNum { difficulty count } } + badges { id displayName icon } submissionCalendar } - userContestRanking(username: $username) { - attendedContestsCount - rating - globalRanking - totalParticipants - topPercentage - badge { - name - icon - expired - } - } - userContestRankingHistory(username: $username) { - attended - rating - contest { - title - startTime - } - } - recentAcSubmissionList(username: $username, limit: $limit) { - id - title - titleSlug - timestamp - } + userContestRanking(username: $username) { attendedContestsCount rating globalRanking totalParticipants topPercentage badge { name icon expired } } + userContestRankingHistory(username: $username) { attended rating contest { title startTime } } + recentAcSubmissionList(username: $username, limit: $limit) { id title titleSlug timestamp } } `, variables: { username, limit: 10 }, @@ -417,42 +158,22 @@ router.post("/leetcode/:username", async (req, res) => { }); let json; - try { - json = await response.json(); - } catch (err) { - const text = await response.text(); - console.error("Invalid JSON from LeetCode API:", text.slice(0, 300)); - return res.status(500).json({ error: "Invalid JSON from LeetCode" }); - } + try { json = await response.json(); } + catch { return res.status(500).json({ error: "Invalid JSON from LeetCode" }); } - if (!json.data?.matchedUser) { - return res.status(404).json({ error: "User not found" }); - } + if (!json.data?.matchedUser) return res.status(404).json({ error: "User not found" }); const contestRanking = json.data.userContestRanking || {}; const contestHistory = json.data.userContestRankingHistory || []; const result = { username: json.data.matchedUser.username, - profile: { - ranking: json.data.matchedUser.profile?.ranking, - avatar: json.data.matchedUser.profile?.userAvatar, - }, - submitStatsGlobal: json.data.matchedUser.submitStatsGlobal.acSubmissionNum.map(sub => ({ - difficulty: sub.difficulty, - count: sub.count, - })), - badges: json.data.matchedUser.badges.map(badge => ({ - id: badge.id, - displayName: badge.displayName, - icon: badge.icon, - })), + profile: { ranking: json.data.matchedUser.profile?.ranking, avatar: json.data.matchedUser.profile?.userAvatar }, + submitStatsGlobal: json.data.matchedUser.submitStatsGlobal.acSubmissionNum.map(sub => ({ difficulty: sub.difficulty, count: sub.count })), + badges: json.data.matchedUser.badges.map(badge => ({ id: badge.id, displayName: badge.displayName, icon: badge.icon })), submissionCalendar: JSON.parse(json.data.matchedUser.submissionCalendar || "{}"), recentSubmissions: json.data.recentAcSubmissionList.map(sub => ({ - id: sub.id, - title: sub.title, - titleSlug: sub.titleSlug, - timestamp: new Date(sub.timestamp * 1000).toISOString(), + id: sub.id, title: sub.title, titleSlug: sub.titleSlug, timestamp: new Date(sub.timestamp * 1000).toISOString() })), contestRating: { attendedContestsCount: contestRanking.attendedContestsCount || 0, @@ -469,185 +190,51 @@ router.post("/leetcode/:username", async (req, res) => { contestHistory: contestHistory.map(contest => ({ attended: contest.attended || false, rating: contest.rating || 0, - contest: { - title: contest.contest.title || "No Title", - startTime: new Date(contest.contest.startTime * 1000).toISOString(), - }, - })), + contest: { title: contest.contest.title || "No Title", startTime: new Date(contest.contest.startTime * 1000).toISOString() } + })) }; const newUser = await LeetCode.create(result); - - res.json({ - message: "LeetCode data fetched from API and saved to DB.", - data: newUser, - }); + res.json({ message: "LeetCode data fetched from API and saved to DB.", data: newUser }); } catch (err) { console.error("LeetCode API Error:", err); res.status(500).json({ error: "Failed to fetch or save LeetCode stats" }); } }); - - router.post("/leetcode/update/:username", async (req, res) => { const { username } = req.params; - try { const existingUser = await LeetCode.findOne({ username }); + if (!existingUser) return res.status(404).json({ error: "User not found in DB" }); - if (!existingUser) { - return res.status(404).json({ error: "User not found in database" }); - } - - const timeDifference = Date.now() - new Date(existingUser.lastUpdated).getTime(); - const sixHoursInMillis = 6 * 60 * 60 * 1000; - - if (timeDifference < sixHoursInMillis) { - return res.json({ - message: "Profile is up-to-date. No update necessary.", - data: existingUser, - }); + const sixHoursInMillis = 6 * 60 * 60 * 1000; + if (Date.now() - new Date(existingUser.lastUpdated).getTime() < sixHoursInMillis) { + return res.json({ message: "Profile up-to-date.", data: existingUser }); } const response = await fetch("https://leetcode.com/graphql", { method: "POST", - headers: { - "Content-Type": "application/json", - "User-Agent": "Mozilla/5.0", - }, - body: JSON.stringify({ - query: `query LeetCodeProfile($username: String!, $limit: Int!) { - matchedUser(username: $username) { - username - profile { - ranking - userAvatar - } - submitStatsGlobal { - acSubmissionNum { - difficulty - count - } - } - badges { - id - displayName - icon - } - submissionCalendar - } - userContestRanking(username: $username) { - attendedContestsCount - rating - globalRanking - totalParticipants - topPercentage - badge { - name - icon - expired - } - } - userContestRankingHistory(username: $username) { - attended - rating - contest { - title - startTime - } - } - recentAcSubmissionList(username: $username, limit: $limit) { - id - title - titleSlug - timestamp - } - }`, - variables: { username, limit: 10 }, - }), + headers: { "Content-Type": "application/json", "User-Agent": "Mozilla/5.0" }, + body: JSON.stringify({ query: `...`, variables: { username, limit: 10 } }), }); - let json; - try { - json = await response.json(); - } catch (err) { - const text = await response.text(); - console.error("Invalid JSON from LeetCode API:", text.slice(0, 300)); - return res.status(500).json({ error: "Invalid JSON from LeetCode" }); - } - - if (!json.data?.matchedUser) { - return res.status(404).json({ error: "User not found" }); - } + const json = await response.json(); + if (!json.data?.matchedUser) return res.status(404).json({ error: "User not found" }); - const contestRanking = json.data.userContestRanking || {}; - const contestHistory = json.data.userContestRankingHistory || []; - - const result = { - username: json.data.matchedUser.username, - profile: { - ranking: json.data.matchedUser.profile?.ranking, - avatar: json.data.matchedUser.profile?.userAvatar, - }, - submitStatsGlobal: json.data.matchedUser.submitStatsGlobal.acSubmissionNum.map(sub => ({ - difficulty: sub.difficulty, - count: sub.count, - })), - badges: json.data.matchedUser.badges.map(badge => ({ - id: badge.id, - displayName: badge.displayName, - icon: badge.icon, - })), - submissionCalendar: JSON.parse(json.data.matchedUser.submissionCalendar || "{}"), - recentSubmissions: json.data.recentAcSubmissionList.map(sub => ({ - id: sub.id, - title: sub.title, - titleSlug: sub.titleSlug, - timestamp: new Date(sub.timestamp * 1000).toISOString(), - })), - contestRating: { - attendedContestsCount: contestRanking.attendedContestsCount || 0, - rating: contestRanking.rating || 0, - globalRanking: contestRanking.globalRanking || 0, - totalParticipants: contestRanking.totalParticipants || 0, - topPercentage: contestRanking.topPercentage || 0, - badge: { - name: contestRanking.badge?.name || "No Badge", - icon: contestRanking.badge?.icon || "/default_icon.png", - expired: contestRanking.badge?.expired || false, - }, - }, - contestHistory: contestHistory.map(contest => ({ - attended: contest.attended || false, - rating: contest.rating || 0, - contest: { - title: contest.contest.title || "No Title", - startTime: new Date(contest.contest.startTime * 1000).toISOString(), - }, - })), - }; - - existingUser.profile = result.profile; - existingUser.submitStatsGlobal = result.submitStatsGlobal; - existingUser.badges = result.badges; - existingUser.submissionCalendar = result.submissionCalendar; - existingUser.recentSubmissions = result.recentSubmissions; - existingUser.contestRating = result.contestRating; - existingUser.contestHistory = result.contestHistory; + // Update DB fields + existingUser.profile = json.data.matchedUser.profile; + existingUser.submitStatsGlobal = json.data.matchedUser.submitStatsGlobal.acSubmissionNum; + existingUser.badges = json.data.matchedUser.badges; + existingUser.submissionCalendar = JSON.parse(json.data.matchedUser.submissionCalendar || "{}"); existingUser.lastUpdated = new Date(); - await existingUser.save(); - res.json({ - message: "LeetCode data updated successfully.", - data: existingUser, - }); + res.json({ message: "LeetCode data updated successfully.", data: existingUser }); } catch (err) { console.error("LeetCode API Error:", err); - res.status(500).json({ error: "Failed to fetch or update LeetCode stats" }); + res.status(500).json({ error: "Failed to fetch/update LeetCode stats" }); } }); - -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/frontend/src/Components/Dashboard.jsx b/frontend/src/Components/Dashboard.jsx index ce50975..a2464f1 100644 --- a/frontend/src/Components/Dashboard.jsx +++ b/frontend/src/Components/Dashboard.jsx @@ -9,7 +9,6 @@ import GoalsCard from "./DashBoard/GoalsCard"; import TimeSpentCard from "./DashBoard/TimeSpentCard"; import ActivityHeatmap from "./DashBoard/ActivityHeatMap"; import NotesCard from "./DashBoard/NotesCard"; -import GitHubCard from "@/Components/GitHubCard"; export default function Dashboard() { const [profile, setProfile] = useState(null); @@ -93,10 +92,9 @@ export default function Dashboard() { const { socialLinks = {}, streak = 0, - githubUsername = null, timeSpent = "0 minutes", activity = [], - notes = [] + notes = [], } = profile; return ( @@ -111,15 +109,6 @@ export default function Dashboard() { - {/* GitHub Card */} - {githubUsername ? ( - - ) : ( -
- GitHub profile not linked -
- )} - {/* Row 2: Goals, Time Spent, Notes */} @@ -139,4 +128,4 @@ export default function Dashboard() {
); -} \ No newline at end of file +} diff --git a/frontend/src/Components/GitHubProfile.jsx b/frontend/src/Components/GitHubProfile.jsx index 90cdc4b..7723db2 100644 --- a/frontend/src/Components/GitHubProfile.jsx +++ b/frontend/src/Components/GitHubProfile.jsx @@ -40,7 +40,12 @@ const GitHubProfile = () => { fetchGitHubData(); }, [username]); - if (loading) return

Loading...

; + if (loading) + return ( +

+ Loading... +

+ ); if (error) return

{error}

; const { @@ -50,34 +55,64 @@ const GitHubProfile = () => { languages = {}, } = data || {}; + // Heatmap renderer with dark/light mode colors const renderHeatmap = () => { if (!contributions?.weeks?.length) - return

No contribution data available.

; + return ( +

+ No contribution data available. +

+ ); return ( -
- {contributions.weeks.map((week, wIdx) => ( -
- {week?.contributionDays?.map((day, dIdx) => ( -
- ))} -
- ))} +
+
+ {contributions.weeks.map((week, wIdx) => ( +
+ {week?.contributionDays?.map((day, dIdx) => { + // default fallback color for empty contributions + const lightModeBg = "#ebedf0"; // light square for light mode + // const darkModeBg = "#1f2937"; + const color = day?.color || lightModeBg; + + return ( +
+ ); + })} +
+ ))} +
); }; + // Languages renderer const renderLanguages = () => { const langKeys = Object.keys(languages); if (!langKeys.length) - return

No language data available.

; + return ( +

+ No language data available. +

+ ); + const totalSize = Object.values(languages).reduce( (sum, val) => sum + val, 0 @@ -94,12 +129,12 @@ const GitHubProfile = () => { : 0; return (
-

+

{lang} ({percentage}%)

-
+
@@ -111,28 +146,34 @@ const GitHubProfile = () => { }; return ( -
- - - GitHub Profile - - - {profile.login -
-

+
+ {/* Profile + Languages side by side */} +
+ {/* Profile */} + + + Profile + + + {profile.login +

{profile.name || profile.login || "N/A"}

-

@{profile.login || "N/A"}

-

{profile.bio || "No bio available"}

-

+

+ @{profile.login || "N/A"} +

+

+ {profile.bio || "No bio available"} +

+

Followers: {profile.followers ?? 0} • Following:{" "} {profile.following ?? 0}

-

+

Total Contributions: {contributions.totalContributions ?? 0} • Commits: {contributions.totalCommits ?? 0}

@@ -140,45 +181,56 @@ const GitHubProfile = () => { href={`https://github.com/${profile.login}`} target="_blank" rel="noopener noreferrer" - className="text-blue-500 hover:underline" + className="mt-3 inline-block bg-yellow-500 text-black px-4 py-2 rounded-lg font-semibold shadow hover:bg-yellow-600 transition" > View on GitHub -
- - + + - + {/* Languages */} + + + Languages Used + + {renderLanguages()} + +
+ + {/* Repositories */} + Top Repositories - + {topRepos.length === 0 && ( -

No repositories available.

+

+ No repositories available. +

)} {topRepos.map((repo, idx) => (
-

+

{repo.name || "N/A"}

-

+

{repo.description || "No description"}

-

+

⭐ {repo.stars ?? 0} | 🍴 {repo.forks ?? 0}

{repo.languages?.length > 0 && ( -

+

{repo.languages.map((l) => l.name).join(", ")}

)} @@ -187,23 +239,54 @@ const GitHubProfile = () => { - - - Contribution Heatmap - - {renderHeatmap()} - - - - - Languages Used + {/* Heatmap */} + + + Contribution Heatmap - {renderLanguages()} + + {contributions?.weeks?.length ? ( +
+ {contributions.weeks.map((week, wIdx) => ( +
+ {week?.contributionDays?.map((day, dIdx) => { + const emptyClass = !day?.contributionCount + ? "bg-gray-200 dark:bg-gray-700" + : ""; + return ( +
+ ); + })} +
+ ))} +
+ ) : ( +

+ No contribution data available. +

+ )} + - - - +
+ + + +
); }; diff --git a/frontend/src/index.css b/frontend/src/index.css index 6c6c280..2b797bc 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -87,6 +87,7 @@ --sidebar-accent-foreground: oklch(0.25 0.06 250); --sidebar-border: oklch(0.9 0.03 250); --sidebar-ring: oklch(0.65 0.12 250); + --heatmap-bg: #ebedf0; } .dark { @@ -121,6 +122,7 @@ --sidebar-accent-foreground: oklch(0.95 0.01 250); --sidebar-border: oklch(0.25 0.05 250); --sidebar-ring: oklch(0.65 0.12 250); + --heatmap-bg: #1f2937; } @layer base { diff --git a/package-lock.json b/package-lock.json index f6f8973..cd51095 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "DevSync-open-source", + "name": "DevSync", "lockfileVersion": 3, "requires": true, "packages": { @@ -7,6 +7,7 @@ "dependencies": { "@octokit/rest": "^22.0.0", "@pinecone-database/pinecone": "^6.1.2", + "cors": "^2.8.5", "dotenv": "^17.2.3", "node-fetch": "^3.3.2" } @@ -180,6 +181,19 @@ "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", "license": "Apache-2.0" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -290,12 +304,30 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/universal-user-agent": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", "license": "ISC" }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", diff --git a/package.json b/package.json index 256dfed..1276f84 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "dependencies": { "@octokit/rest": "^22.0.0", "@pinecone-database/pinecone": "^6.1.2", + "cors": "^2.8.5", "dotenv": "^17.2.3", "node-fetch": "^3.3.2" } From a06934f7c2c1603c3ae61ae648a3589977ef3df8 Mon Sep 17 00:00:00 2001 From: AvaneeshKesavan Date: Mon, 6 Oct 2025 23:04:48 +0530 Subject: [PATCH 5/6] social icons fix --- backend/routes/github.route.js | 2 +- .../Components/DashBoard/PlatformLinks.jsx | 48 +- .../src/Components/DashBoard/ProfileCard.jsx | 9 +- frontend/src/Components/GitHubProfile.jsx | 12 +- frontend/src/Components/profile/Profile.jsx | 611 ++++++++++++------ 5 files changed, 452 insertions(+), 230 deletions(-) diff --git a/backend/routes/github.route.js b/backend/routes/github.route.js index 5ebb22f..50e4ea8 100644 --- a/backend/routes/github.route.js +++ b/backend/routes/github.route.js @@ -135,7 +135,7 @@ router.get("/:username", async (req, res) => { }); } catch (err) { console.error("GitHub API error:", err); - res.status(500).json({ error: "Server error" }); + res.status(500).json({ error: "User not found or an error occurred" }); } }); diff --git a/frontend/src/Components/DashBoard/PlatformLinks.jsx b/frontend/src/Components/DashBoard/PlatformLinks.jsx index 7dd6cd5..8a9fafd 100644 --- a/frontend/src/Components/DashBoard/PlatformLinks.jsx +++ b/frontend/src/Components/DashBoard/PlatformLinks.jsx @@ -5,7 +5,6 @@ import { SiHackerearth, SiGithub, } from "react-icons/si"; -import { Link } from "react-router-dom"; const iconMap = { codechef: SiCodechef, @@ -23,27 +22,32 @@ function normalizeLeetcodeURL(url) { } function normalizeGitHubURL(url) { + if (!url || url.trim() === "") return null; const githubRegex = /^https?:\/\/(www\.)?github\.com\/[a-zA-Z0-9_-]+\/?$/; if (!githubRegex.test(url)) return null; - return url.replace(/\/$/, ""); + + const username = url.replace(/\/$/, "").split("/").pop(); + return username; } const leetcodeUrl = (url) => { const normalized = normalizeLeetcodeURL(url); if (!normalized) return "#"; - const username = normalized.split("/").pop(); + const username = normalized.trim().split("/").pop(); return `/leetcode/${username}`; }; const githubUrl = (url) => { - const normalized = normalizeGitHubURL(url); - if (!normalized) return "#"; - const username = normalized.split("/").pop(); + if (!url) return "#"; + const username = url.replace(/\/$/, "").split("/").pop(); return `/dashboard/github/${username}`; }; export default function PlatformLinks({ platforms }) { - const platformEntries = Object.entries(platforms); + // Filter out empty or falsy URLs + const platformEntries = Object.entries(platforms).filter( + ([, url]) => url && url.trim() !== "" + ); return (
@@ -51,32 +55,18 @@ export default function PlatformLinks({ platforms }) { platformEntries.map(([name, url], i) => { const Icon = iconMap[name.toLowerCase()] || SiGithub; - // Internal route for GitHub - if (name.toLowerCase() === "github") { - return ( - - -
- - {name} - - - Active - -
- - ); - } + // Determine href based on platform + const href = + name.toLowerCase() === "leetcode" + ? leetcodeUrl(url) + : name.toLowerCase() === "github" + ? githubUrl(normalizeGitHubURL(url)) + : url; - // Other platforms return ( url && url.trim() !== "" + ); function normalizeLeetcodeURL(url) { const leetcodeRegex = @@ -45,9 +47,8 @@ export default function ProfileCard({ user }) { }; const githubUrl = (url) => { - const normalized = normalizeGitHubURL(url); - if (!normalized) return "#"; - const username = normalized.split("/").pop(); + if (!url) return "#"; + const username = url.replace(/\/$/, "").split("/").pop(); return `/dashboard/github/${username}`; }; diff --git a/frontend/src/Components/GitHubProfile.jsx b/frontend/src/Components/GitHubProfile.jsx index 7723db2..e2308db 100644 --- a/frontend/src/Components/GitHubProfile.jsx +++ b/frontend/src/Components/GitHubProfile.jsx @@ -31,7 +31,7 @@ const GitHubProfile = () => { else setError(json.error || "Failed to fetch GitHub data"); } catch (err) { console.error(err); - setError("Server error"); + setError("User not found or an error occurred"); } finally { setLoading(false); } @@ -134,7 +134,7 @@ const GitHubProfile = () => {

@@ -158,7 +158,7 @@ const GitHubProfile = () => { {profile.login

{profile.name || profile.login || "N/A"} @@ -181,7 +181,7 @@ const GitHubProfile = () => { href={`https://github.com/${profile.login}`} target="_blank" rel="noopener noreferrer" - className="mt-3 inline-block bg-yellow-500 text-black px-4 py-2 rounded-lg font-semibold shadow hover:bg-yellow-600 transition" + className="mt-3 inline-block bg-[var(--primary)] text-black px-4 py-2 rounded-lg font-semibold shadow hover:bg-[var(--primary-foreground)] transition" > View on GitHub @@ -213,7 +213,7 @@ const GitHubProfile = () => { key={idx} className="p-4 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition shadow-sm" > -

+

{ {repo.description || "No description"}

- ⭐ {repo.stars ?? 0} | 🍴 {repo.forks ?? 0} + stars {repo.stars ?? 0} | fork {repo.forks ?? 0}

{repo.languages?.length > 0 && (

diff --git a/frontend/src/Components/profile/Profile.jsx b/frontend/src/Components/profile/Profile.jsx index f67f902..a200a11 100644 --- a/frontend/src/Components/profile/Profile.jsx +++ b/frontend/src/Components/profile/Profile.jsx @@ -1,8 +1,32 @@ import { useState, useEffect, useRef } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { useNavigate } from "react-router-dom"; -import { Camera, RefreshCw, User, MapPin, Mail, Globe, Edit2, Save, X, Plus, Check, LogOut, Sparkles, Zap } from "lucide-react"; -import { SiLeetcode, SiCodechef, SiHackerrank, SiHackerearth, SiCodeforces, SiLinkedin, SiGitlab, SiGithub } from "react-icons/si"; +import { + Camera, + RefreshCw, + User, + MapPin, + Mail, + Globe, + Edit2, + Save, + X, + Plus, + Check, + LogOut, + Sparkles, + Zap, +} from "lucide-react"; +import { + SiLeetcode, + SiCodechef, + SiHackerrank, + SiHackerearth, + SiCodeforces, + SiLinkedin, + SiGitlab, + SiGithub, +} from "react-icons/si"; import BackButton from "../ui/backbutton"; // --- START: Helper Components for Modern UI (Including the new SuccessPopup) --- @@ -12,7 +36,7 @@ import BackButton from "../ui/backbutton"; */ const SocialButton = ({ icon, buttonUrl, buttonName, leetcodeUrl }) => { const isLinked = !!buttonUrl; - + return ( { } w-full`} onClick={() => { if (buttonUrl) { - const url = buttonName === 'Leetcode' ? leetcodeUrl(buttonUrl) : buttonUrl; + const url = + buttonName === "Leetcode" ? leetcodeUrl(buttonUrl) : buttonUrl; if (url) { window.open(url, "_blank", "noopener,noreferrer"); } @@ -32,13 +57,27 @@ const SocialButton = ({ icon, buttonUrl, buttonName, leetcodeUrl }) => { }} disabled={!buttonUrl} > -

+
{icon}
{buttonName} {isLinked && ( - - + + )} @@ -48,7 +87,14 @@ const SocialButton = ({ icon, buttonUrl, buttonName, leetcodeUrl }) => { /** * A themed input wrapper for editing social links. */ -const SocialInput = ({ labelName, icon, linkName, editData, setEditData, error }) => { +const SocialInput = ({ + labelName, + icon, + linkName, + editData, + setEditData, + error, +}) => { return (

- Total Skills - {profileData.skills?.length || 0} + + Total Skills + + + {profileData.skills?.length || 0} +
- Platforms Connected - {connectedPlatformsCount} + + Platforms Connected + + + {connectedPlatformsCount} +
@@ -713,7 +830,8 @@ const Profile = () => {

{!isEditing ? (

- {profileData.bio || "No bio added yet. Click 'Edit Profile' to introduce yourself!"} + {profileData.bio || + "No bio added yet. Click 'Edit Profile' to introduce yourself!"}

) : (