From d67721bfd14171425b615fcfdfae66088fc2231f Mon Sep 17 00:00:00 2001 From: SunilKumarKV Date: Thu, 18 Jun 2026 23:54:51 +0530 Subject: [PATCH 1/3] feat: add coding profile page --- src/App.css | 155 +++++++++ src/App.jsx | 2 + src/coding/CodingProfilePage.jsx | 537 +++++++++++++++++++++++++++++++ src/components/Navbar.jsx | 4 + 4 files changed, 698 insertions(+) create mode 100644 src/coding/CodingProfilePage.jsx diff --git a/src/App.css b/src/App.css index 7681516..dbaa332 100644 --- a/src/App.css +++ b/src/App.css @@ -1357,6 +1357,133 @@ textarea:focus-visible { background: linear-gradient(90deg, var(--accent-color), color-mix(in srgb, var(--accent-color) 70%, #22c55e)); } +.coding-hero-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 18px; +} + +.coding-hero-grid .glass-card { + display: grid; + gap: 12px; + align-content: start; + min-height: 154px; +} + +.coding-hero-grid .glass-card h3 { + font-size: clamp(2rem, 4vw, 3rem); + line-height: 0.96; + letter-spacing: -0.05em; + margin: 0; +} + +.coding-profile-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; +} + +.coding-profile-grid > * { + min-width: 0; + min-height: 100%; +} + +.coding-card-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + min-width: 0; +} + +.coding-card-row span:first-child { + color: var(--accent-color); + font-weight: 700; +} + +.coding-card-row span:last-child { + color: var(--text-secondary); + font-size: 0.92rem; + font-weight: 600; +} + +.coding-card-meta { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 12px; +} + +.coding-tag { + display: inline-flex; + align-items: center; + min-height: 30px; + padding: 6px 10px; + border-radius: 999px; + background: color-mix(in srgb, var(--accent-color) 10%, transparent); + border: 1px solid color-mix(in srgb, var(--accent-color) 16%, transparent); + color: var(--text-color); + font-size: 0.82rem; + font-weight: 700; + overflow-wrap: anywhere; +} + +.contribution-heatmap-container { + margin-top: 22px; + overflow-x: auto; + padding-bottom: 8px; +} + +.contribution-heatmap-container::-webkit-scrollbar { + height: 8px; +} + +.contribution-heatmap-grid { + display: inline-flex; + gap: 7px; + min-width: max-content; +} + +.contribution-week { + display: grid; + grid-template-rows: repeat(7, 12px); + gap: 7px; +} + +.contribution-cell { + width: 12px; + height: 12px; + border-radius: 4px; + background: color-mix(in srgb, var(--accent-color) 8%, var(--card-bg)); + border: 1px solid color-mix(in srgb, var(--input-border) 75%, transparent); + transition: transform 0.16s ease, background-color 0.16s ease, border-color 0.16s ease; +} + +.contribution-cell:hover { + transform: scale(1.15); +} + +.contribution-cell.level-1 { + background: color-mix(in srgb, var(--accent-color) 20%, var(--card-bg)); + border-color: color-mix(in srgb, var(--accent-color) 24%, transparent); +} + +.contribution-cell.level-2 { + background: color-mix(in srgb, var(--accent-color) 36%, var(--card-bg)); + border-color: color-mix(in srgb, var(--accent-color) 32%, transparent); +} + +.contribution-cell.level-3 { + background: color-mix(in srgb, var(--accent-color) 54%, var(--card-bg)); + border-color: color-mix(in srgb, var(--accent-color) 42%, transparent); +} + +.contribution-cell.level-4 { + background: color-mix(in srgb, var(--accent-color) 74%, white 8%); + border-color: color-mix(in srgb, var(--accent-color) 58%, transparent); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent-color) 18%, transparent); +} + .tag-chip-list { display: flex; flex-wrap: wrap; @@ -1531,6 +1658,10 @@ textarea:focus-visible { /* mobile */ @media (max-width: 768px) { + .coding-hero-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .solution-tabs { gap: 8px; } @@ -1549,6 +1680,30 @@ textarea:focus-visible { } } +@media (max-width: 640px) { + .coding-hero-grid { + grid-template-columns: 1fr; + } + + .coding-hero-grid .glass-card { + min-height: 0; + } + + .contribution-heatmap-grid { + gap: 5px; + } + + .contribution-week { + grid-template-rows: repeat(7, 10px); + gap: 5px; + } + + .contribution-cell { + width: 10px; + height: 10px; + } +} + @media (max-width: 430px) { .solution-tab { flex: 1 1 100%; diff --git a/src/App.jsx b/src/App.jsx index 7774394..f688f76 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -24,6 +24,7 @@ const CodebaseDetailPage = lazy(() => import("./codebase/CodebaseDetailPage")); const DashboardPage = lazy(() => import("./dashboard/DashboardPage")); const JourneyPage = lazy(() => import("./journey/JourneyPage")); const AchievementsPage = lazy(() => import("./achievements/AchievementsPage")); +const CodingProfilePage = lazy(() => import("./coding/CodingProfilePage")); function RouteFallback() { return ( @@ -64,6 +65,7 @@ function AppRoutes() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/coding/CodingProfilePage.jsx b/src/coding/CodingProfilePage.jsx new file mode 100644 index 0000000..64706bc --- /dev/null +++ b/src/coding/CodingProfilePage.jsx @@ -0,0 +1,537 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { Link } from "react-router-dom"; +import { + formatDate, + getJournalProblems, + getJournalProjects, + getJournalStats, + getProblemLanguages, + getProblemSolvedAt, + toPlatformSegment, + uniqueValues, +} from "../lib/codingJournal"; +import PageHeader from "../components/ui/PageHeader"; +import SectionPanel from "../components/ui/SectionPanel"; +import LoadingState from "../components/ui/LoadingState"; +import ErrorState from "../components/ui/ErrorState"; +import EmptyState from "../components/ui/EmptyState"; + +function percent(value, total) { + if (!total) return "0%"; + return `${Math.round((value / total) * 100)}%`; +} + +function getDateKey(date) { + return date.toISOString().slice(0, 10); +} + +function getContributionLevel(count, maxCount) { + if (!count || !maxCount) return 0; + return Math.min(4, Math.ceil((count / maxCount) * 4)); +} + +function normalizeProfileLanguage(value) { + if (!value) return ""; + const normalized = String(value).trim().toLowerCase(); + if (/^javascript?$/.test(normalized) || /^typescript?$/.test(normalized)) return "JavaScript"; + if (/^java$/.test(normalized)) return "Java"; + if (/^c($|[^a-z])/.test(normalized) || normalized === "c") return "C"; + return "Others"; +} + +function normalizePlatform(value) { + const normalized = String(value || "").trim(); + if (!normalized) return "Others"; + if (/leetcode/i.test(normalized)) return "LeetCode"; + if (/codeforces/i.test(normalized)) return "Codeforces"; + if (/codechef/i.test(normalized)) return "CodeChef"; + if (/hackerrank/i.test(normalized)) return "HackerRank"; + return "Others"; +} + +function useCodingProfileData() { + const [state, setState] = useState({ + stats: null, + problems: [], + projects: [], + loading: true, + error: "", + }); + + useEffect(() => { + let ignore = false; + + Promise.all([getJournalStats(), getJournalProblems(), getJournalProjects()]) + .then(([statsData, problemsData, projectsData]) => { + if (ignore) return; + setState({ + stats: statsData ?? null, + problems: Array.isArray(problemsData) ? problemsData : [], + projects: Array.isArray(projectsData) ? projectsData : [], + loading: false, + error: "", + }); + }) + .catch((fetchError) => { + if (ignore) return; + setState({ + stats: null, + problems: [], + projects: [], + loading: false, + error: + fetchError.message || "Unable to load coding profile data from coding-journal.", + }); + }); + + return () => { + ignore = true; + }; + }, []); + + return state; +} + +export default function CodingProfilePage() { + const { stats, problems, projects, loading, error } = useCodingProfileData(); + + const analytics = useMemo(() => { + const totalProblems = stats?.totalProblems ?? problems.length; + const verifiedSolutions = stats?.verifiedProblems ?? problems.filter((problem) => problem.verified).length; + + const problemLanguages = problems.flatMap((problem) => { + const languages = getProblemLanguages(problem); + if (languages.length) return languages; + return problem.language ? [problem.language] : []; + }); + + const languageUsedSet = uniqueValues(problemLanguages.map((language) => normalizeProfileLanguage(language))); + const languagesUsed = languageUsedSet.filter(Boolean).length; + const platformUsedSet = uniqueValues(problems.map((problem) => normalizePlatform(problem.platform))); + const platformsUsed = platformUsedSet.filter(Boolean).length; + + const difficultyCounts = problems.reduce( + (acc, problem) => { + const key = problem.difficulty || "Unknown"; + acc[key] = (acc[key] || 0) + 1; + return acc; + }, + {} + ); + + const difficultyCards = ["Easy", "Medium", "Hard"].map((name) => ({ + name, + count: difficultyCounts[name] || 0, + percentage: percent(difficultyCounts[name] || 0, totalProblems), + })); + + const languageCounts = problemLanguages.reduce((acc, language) => { + const key = normalizeProfileLanguage(language); + acc[key] = (acc[key] || 0) + 1; + return acc; + }, {}); + + const languageCards = [ + { name: "JavaScript", count: languageCounts["JavaScript"] || 0 }, + { name: "Java", count: languageCounts["Java"] || 0 }, + { name: "C", count: languageCounts["C"] || 0 }, + { name: "Others", count: languageCounts["Others"] || 0 }, + ]; + + const platformCounts = problems.reduce((acc, problem) => { + const key = normalizePlatform(problem.platform); + acc[key] = (acc[key] || 0) + 1; + return acc; + }, {}); + + const platformCards = [ + { name: "LeetCode", count: platformCounts["LeetCode"] || 0 }, + { name: "Codeforces", count: platformCounts["Codeforces"] || 0 }, + { name: "CodeChef", count: platformCounts["CodeChef"] || 0 }, + { name: "HackerRank", count: platformCounts["HackerRank"] || 0 }, + { name: "Others", count: platformCounts["Others"] || 0 }, + ]; + + const contributions = {}; + const today = new Date(); + today.setHours(0, 0, 0, 0); + const daysCount = 365; + const startDate = new Date(today); + startDate.setDate(startDate.getDate() - (daysCount - 1)); + + const recordEvent = (rawDate) => { + if (!rawDate) return; + const eventDate = new Date(rawDate); + if (Number.isNaN(eventDate.getTime())) return; + eventDate.setHours(0, 0, 0, 0); + if (eventDate < startDate || eventDate > today) return; + const key = getDateKey(eventDate); + contributions[key] = (contributions[key] || 0) + 1; + }; + + problems.forEach((problem) => recordEvent(getProblemSolvedAt(problem))); + projects.forEach((project) => recordEvent(project.updatedAt)); + + const heatmapDays = Array.from({ length: daysCount }, (_, index) => { + const date = new Date(startDate); + date.setDate(startDate.getDate() + index); + const key = getDateKey(date); + return { + key, + date, + count: contributions[key] || 0, + }; + }); + + const maxHeatCount = Math.max(...heatmapDays.map((day) => day.count), 1); + const contributionWeeks = Array.from( + { length: Math.ceil(heatmapDays.length / 7) }, + (_, weekIndex) => heatmapDays.slice(weekIndex * 7, weekIndex * 7 + 7) + ); + + const totalActivityEvents = heatmapDays.reduce((total, day) => total + day.count, 0); + const activeDays = heatmapDays.filter((day) => day.count > 0).length; + const currentStreak = (() => { + let streak = 0; + for (let i = heatmapDays.length - 1; i >= 0; i -= 1) { + if (heatmapDays[i].count > 0) streak += 1; + else break; + } + return streak; + })(); + + const longestStreak = heatmapDays.reduce( + (state, day) => { + if (day.count > 0) { + const current = state.current + 1; + return { + current, + longest: Math.max(state.longest, current), + }; + } + return { current: 0, longest: state.longest }; + }, + { current: 0, longest: 0 } + ).longest; + + const recentSolves = [...problems] + .filter((problem) => getProblemSolvedAt(problem)) + .sort((a, b) => new Date(getProblemSolvedAt(b)) - new Date(getProblemSolvedAt(a))) + .slice(0, 8) + .map((problem) => ({ + ...problem, + solvedAt: getProblemSolvedAt(problem), + solutionLanguages: getProblemLanguages(problem).length + ? getProblemLanguages(problem) + : problem.language + ? [problem.language] + : [], + })); + + const multiLanguageProblems = [...problems] + .map((problem) => { + const solutionLanguages = getProblemLanguages(problem).length + ? getProblemLanguages(problem) + : problem.language + ? [problem.language] + : []; + return { + ...problem, + solutionLanguages: uniqueValues(solutionLanguages), + activityDate: getProblemSolvedAt(problem) || problem.updatedAt || problem.createdAt || problem.addedAt, + }; + }) + .filter((problem) => problem.solutionLanguages.length > 1) + .sort((a, b) => new Date(b.activityDate) - new Date(a.activityDate)) + .slice(0, 6); + + const timelineEvents = [...problems] + .flatMap((problem) => { + const solvedDate = problem.solvedAt ? new Date(problem.solvedAt) : null; + const updatedDate = problem.updatedAt ? new Date(problem.updatedAt) : null; + const events = []; + + if (solvedDate && !Number.isNaN(solvedDate.getTime())) { + events.push({ + date: solvedDate, + title: `${problem.title} solved`, + description: `${normalizePlatform(problem.platform)} problem solved (${problem.difficulty || "Unknown"}).`, + verified: Boolean(problem.verified), + link: `/problems/${toPlatformSegment(problem.platform)}/${problem.slug}`, + }); + } + + if (!problem.solvedAt && updatedDate && !Number.isNaN(updatedDate.getTime())) { + events.push({ + date: updatedDate, + title: `${problem.title} updated`, + description: `Problem record updated in coding-journal.`, + verified: Boolean(problem.verified), + link: `/problems/${toPlatformSegment(problem.platform)}/${problem.slug}`, + }); + } + + return events; + }) + .sort((a, b) => b.date - a.date) + .slice(0, 10); + + return { + heroStats: [ + { label: "Total Problems", value: totalProblems }, + { label: "Verified Solutions", value: verifiedSolutions }, + { label: "Languages Used", value: languagesUsed }, + { label: "Platforms", value: platformsUsed }, + ], + difficultyCards, + languageCards, + platformCards, + recentSolves, + multiLanguageProblems, + timelineEvents, + heatmap: { + weeks: contributionWeeks, + totalActivityEvents, + activeDays, + currentStreak, + longestStreak, + maxCount: maxHeatCount, + }, + }; + }, [problems, projects, stats]); + + const hasProblemData = problems.length > 0; + + return ( +
+ + + {!loading && !error && hasProblemData ? ( + <> + +
+ {analytics.heroStats.map((item) => ( +
+ {item.label} +

{item.value}

+
+ ))} +
+
+ + +
+
+
+
+

{analytics.heatmap.totalActivityEvents}

+

Total activity events

+
+
+

{analytics.heatmap.activeDays}

+

Active days

+
+
+

{analytics.heatmap.currentStreak}

+

Current streak

+
+
+

{analytics.heatmap.longestStreak}

+

Longest streak

+
+
+ +
+
+ {analytics.heatmap.weeks.map((week, weekIndex) => ( +
+ {week.map((day) => ( +
+ ))} +
+ ))} +
+
+
+
+
+ + +
+
+
+ {analytics.difficultyCards.map((item) => ( +
+
+ {item.name} + {item.count} +
+
+ +
+
+ ))} +
+
+
+
+ + +
+ {analytics.languageCards.map((item) => ( +
+

{item.name}

+

{item.count} references

+
+ ))} +
+
+ + +
+ {analytics.platformCards.map((item) => ( +
+

{item.name}

+

{item.count} problems

+
+ ))} +
+
+ + + {analytics.recentSolves.length ? ( +
+ {analytics.recentSolves.map((problem) => ( +
+
+ {normalizePlatform(problem.platform)} + {problem.difficulty || "Unknown"} +
+

{problem.title}

+

{problem.solutionLanguages.length} language{problem.solutionLanguages.length === 1 ? "" : "s"}

+
+ {problem.solutionLanguages.slice(0, 3).map((language) => ( + {language} + ))} +
+
+ + {problem.verified ? "Verified" : "Captured"} + + {formatDate(problem.solvedAt)} +
+
+ + View Problem + + + View Codebase + +
+
+ ))} +
+ ) : ( + + )} +
+ + + {analytics.multiLanguageProblems.length ? ( +
+ {analytics.multiLanguageProblems.map((problem) => ( +
+

{problem.title}

+

{problem.solutionLanguages.join(", ")}

+

{problem.solutionLanguages.length} solution language{problem.solutionLanguages.length === 1 ? "" : "s"}

+
+ + View Codebase + +
+
+ ))} +
+ ) : ( + + )} +
+ + +
+ {analytics.timelineEvents.length ? ( + analytics.timelineEvents.map((event) => ( +
+
+

{event.title}

+ + {event.verified ? "Verified" : "Recorded"} + +
+

{event.description}

+ {formatDate(event.date)} +
+ + View Problem + +
+
+ )) + ) : ( +
+

No timeline events available

+

+ The live coding-journal feed does not contain enough dated problem events to build a profile timeline. +

+
+ )} +
+
+ + ) : !loading && !error ? ( + + + + ) : null} + + {loading ? ( + + + + ) : error ? ( + + + + ) : null} +
+ ); +} diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index df9bffc..340f8f9 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -24,6 +24,7 @@ export default function Navbar() { if (id === "Work") return location.pathname.startsWith("/projects"); if (id === "Problems") return location.pathname.startsWith("/problems"); if (id === "Codebase") return location.pathname.startsWith("/codebase"); + if (id === "Coding") return location.pathname.startsWith("/coding"); if (id === "About") { return ( location.pathname.startsWith("/about") || @@ -50,6 +51,8 @@ export default function Navbar() { navigate("/codebase"); } else if (id === "About") { navigate("/about"); + } else if (id === "Coding") { + navigate("/coding"); } else if (id === "Contact") { navigate("/contact"); } else { @@ -98,6 +101,7 @@ export default function Navbar() { "Work", "Problems", "Codebase", + "Coding", "About", "Contact", ].map((id) => ( From 006213de135d035fcb47c75a0429c49f5779bd37 Mon Sep 17 00:00:00 2001 From: SunilKumarKV Date: Fri, 19 Jun 2026 00:07:53 +0530 Subject: [PATCH 2/3] refactor: clarify coding navigation structure --- src/codebase/CodebasePage.jsx | 10 +++--- src/coding/CodingProfilePage.jsx | 61 ++++++++++++++++++++++++++------ src/components/Navbar.jsx | 27 +++++++------- src/dashboard/DashboardPage.jsx | 6 ++-- src/problems/ProblemsHome.jsx | 14 ++++---- 5 files changed, 78 insertions(+), 40 deletions(-) diff --git a/src/codebase/CodebasePage.jsx b/src/codebase/CodebasePage.jsx index dcdf5da..4751153 100644 --- a/src/codebase/CodebasePage.jsx +++ b/src/codebase/CodebasePage.jsx @@ -136,14 +136,14 @@ export default function CodebasePage() { {loading ? ( @@ -184,8 +184,8 @@ export default function CodebasePage() { {!loading && !error && hasProblemData ? ( <> + +
+
+ Problems Tracker +

Problem history and progress

+

Open the tracker for solved lists, difficulty coverage, platforms, tags, and recent progress.

+
+ + Open Problems Tracker + +
+
+
+ Codebase Library +

Solution articles and code

+

Go deeper into explanations, language tabs, complexity notes, and verified implementation detail.

+
+ + Open Codebase Library + +
+
+
+ Developer Dashboard +

Analytics and activity center

+

See broader engineering signals across projects, coding activity, verification, and portfolio metrics.

+
+ + Open Developer Dashboard + +
+
+
+
+
{analytics.heroStats.map((item) => ( @@ -330,8 +369,8 @@ export default function CodingProfilePage() {
@@ -374,7 +413,7 @@ export default function CodingProfilePage() {
- +
@@ -394,7 +433,7 @@ export default function CodingProfilePage() {
- +
{analytics.languageCards.map((item) => (
@@ -405,7 +444,7 @@ export default function CodingProfilePage() {
- +
{analytics.platformCards.map((item) => (
@@ -416,7 +455,7 @@ export default function CodingProfilePage() {
- + {analytics.recentSolves.length ? (
{analytics.recentSolves.map((problem) => ( @@ -458,7 +497,7 @@ export default function CodingProfilePage() { )} - + {analytics.multiLanguageProblems.length ? (
{analytics.multiLanguageProblems.map((problem) => ( @@ -483,7 +522,7 @@ export default function CodingProfilePage() { )} - +
{analytics.timelineEvents.length ? ( analytics.timelineEvents.map((event) => ( diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index 340f8f9..fa2ca1f 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -21,19 +21,21 @@ export default function Navbar() { const isNavActive = (id) => { if (id === "Home") return location.pathname === "/"; - if (id === "Work") return location.pathname.startsWith("/projects"); - if (id === "Problems") return location.pathname.startsWith("/problems"); - if (id === "Codebase") return location.pathname.startsWith("/codebase"); + if (id === "Projects") return location.pathname.startsWith("/projects"); if (id === "Coding") return location.pathname.startsWith("/coding"); - if (id === "About") { + if (id === "Dashboard") { return ( - location.pathname.startsWith("/about") || - location.pathname.startsWith("/rewards") || location.pathname.startsWith("/dashboard") || location.pathname.startsWith("/journey") || location.pathname.startsWith("/achievements") ); } + if (id === "About") { + return ( + location.pathname.startsWith("/about") || + location.pathname.startsWith("/rewards") + ); + } if (id === "Contact") return location.pathname.startsWith("/contact"); return false; }; @@ -43,16 +45,14 @@ export default function Navbar() { if (id === "Home") { navigate("/"); - } else if (id === "Work") { + } else if (id === "Projects") { navigate("/projects"); - } else if (id === "Problems") { - navigate("/problems"); - } else if (id === "Codebase") { - navigate("/codebase"); } else if (id === "About") { navigate("/about"); } else if (id === "Coding") { navigate("/coding"); + } else if (id === "Dashboard") { + navigate("/dashboard"); } else if (id === "Contact") { navigate("/contact"); } else { @@ -98,10 +98,9 @@ export default function Navbar() {
    {[ "Home", - "Work", - "Problems", - "Codebase", + "Projects", "Coding", + "Dashboard", "About", "Contact", ].map((id) => ( diff --git a/src/dashboard/DashboardPage.jsx b/src/dashboard/DashboardPage.jsx index a3aafad..f06bc71 100644 --- a/src/dashboard/DashboardPage.jsx +++ b/src/dashboard/DashboardPage.jsx @@ -378,7 +378,7 @@ export default function DashboardPage() { @@ -387,7 +387,7 @@ export default function DashboardPage() {
    {analytics.overview.map((metric) => ( @@ -403,7 +403,7 @@ export default function DashboardPage() { className="dashboard-progress" eyebrow="Coding Progress" title="Coding Progress" - description="Problem solving performance and platform, difficulty, and topic distributions from live coding-journal problem data." + description="Analytics across problem-solving performance, platform spread, difficulty mix, and topic distribution." >
    diff --git a/src/problems/ProblemsHome.jsx b/src/problems/ProblemsHome.jsx index 46bef15..920346f 100644 --- a/src/problems/ProblemsHome.jsx +++ b/src/problems/ProblemsHome.jsx @@ -154,14 +154,14 @@ export default function ProblemsHome() { {loading ? ( @@ -202,8 +202,8 @@ export default function ProblemsHome() { {loading ? ( @@ -266,8 +266,8 @@ export default function ProblemsHome() { Date: Fri, 19 Jun 2026 00:18:25 +0530 Subject: [PATCH 3/3] feat: add live coding social proof --- src/App.css | 6 ++ src/coding/CodingProfilePage.jsx | 50 ++++++++++++ src/components/HeroSection.jsx | 130 +++++++++++++++++++++++++++++++ src/components/ProjectDetail.jsx | 70 ++++++++++++++++- 4 files changed, 254 insertions(+), 2 deletions(-) diff --git a/src/App.css b/src/App.css index dbaa332..690d3b2 100644 --- a/src/App.css +++ b/src/App.css @@ -1564,6 +1564,12 @@ textarea:focus-visible { .project-detail-hero h1 { font-size: clamp(2rem, 5vw, 4rem); margin: 16px 0; } .project-detail-hero p { color: var(--text-secondary); line-height: 1.8; } +.project-proof-strip { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 14px; +} + .solution-tabs { display: flex; flex-wrap: wrap; diff --git a/src/coding/CodingProfilePage.jsx b/src/coding/CodingProfilePage.jsx index 52c7fb0..bb6879b 100644 --- a/src/coding/CodingProfilePage.jsx +++ b/src/coding/CodingProfilePage.jsx @@ -289,6 +289,12 @@ export default function CodingProfilePage() { recentSolves, multiLanguageProblems, timelineEvents, + goal: { + currentSolved: problems.length, + targetSolved: 300, + percentage: Math.min(100, Math.round((problems.length / 300) * 100)), + remaining: Math.max(0, 300 - problems.length), + }, heatmap: { weeks: contributionWeeks, totalActivityEvents, @@ -367,6 +373,50 @@ export default function CodingProfilePage() {
    + +
    +
    +
    + Target 300 + {analytics.goal.percentage}% complete +
    +

    {analytics.goal.currentSolved} solved so far

    +

    {analytics.goal.remaining} problems remaining to reach the current coding goal.

    +
    +
    +
    + Goal progress + {analytics.goal.currentSolved} / {analytics.goal.targetSolved} +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +

    {analytics.heatmap.currentStreak}

    +

    Current streak

    +
    +
    +

    {analytics.heatmap.longestStreak}

    +

    Longest streak

    +
    +
    +

    + Streaks are based on the same live activity timeline used for the heatmap, combining solved problems and project updates. +

    +
    +
    +
    + [...projects].sort(sortProjects).slice(0, 3), [projects]); const verifiedProblems = useMemo(() => problems.filter((problem) => problem.verified).slice(0, 3), [problems]); + const liveCodingActivity = useMemo(() => { + const latestSolvedProblem = [...problems] + .filter((problem) => getProblemSolvedAt(problem)) + .sort((a, b) => new Date(getProblemSolvedAt(b)) - new Date(getProblemSolvedAt(a)))[0] || null; + + const latestProjectUpdate = [...projects] + .filter((project) => project.updatedAt) + .sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))[0] || null; + + const contributions = {}; + const today = new Date(); + today.setHours(0, 0, 0, 0); + const startDate = new Date(today); + startDate.setDate(startDate.getDate() - 364); + + const addEvent = (rawDate) => { + if (!rawDate) return; + const date = new Date(rawDate); + if (Number.isNaN(date.getTime())) return; + date.setHours(0, 0, 0, 0); + if (date < startDate || date > today) return; + const key = date.toISOString().slice(0, 10); + contributions[key] = (contributions[key] || 0) + 1; + }; + + problems.forEach((problem) => addEvent(getProblemSolvedAt(problem))); + projects.forEach((project) => addEvent(project.updatedAt)); + + const heatmapDays = Array.from({ length: 365 }, (_, index) => { + const date = new Date(startDate); + date.setDate(startDate.getDate() + index); + const key = date.toISOString().slice(0, 10); + return contributions[key] || 0; + }); + + const currentStreak = (() => { + let streak = 0; + for (let i = heatmapDays.length - 1; i >= 0; i -= 1) { + if (heatmapDays[i] > 0) streak += 1; + else break; + } + return streak; + })(); + + return { + latestSolvedProblem, + latestProjectUpdate, + currentStreak, + }; + }, [problems, projects]); + const summary = useMemo(() => { const verified = problems.filter((problem) => problem.verified).length; const platforms = new Set(problems.map((problem) => problem.platform).filter(Boolean)).size; @@ -312,6 +365,83 @@ export default function HeroSection() { )} + + {loading ? ( + + ) : error ? ( + + ) : !liveCodingActivity.latestSolvedProblem && !liveCodingActivity.latestProjectUpdate ? ( + + ) : ( + <> +
    +
    +
    + Latest solved problem + {liveCodingActivity.latestSolvedProblem?.difficulty ? ( + {liveCodingActivity.latestSolvedProblem.difficulty} + ) : null} +
    +

    {liveCodingActivity.latestSolvedProblem?.title || "No solved problem yet"}

    +

    + {liveCodingActivity.latestSolvedProblem + ? `${liveCodingActivity.latestSolvedProblem.platform || "Unknown platform"} solved on ${formatDate(getProblemSolvedAt(liveCodingActivity.latestSolvedProblem)) || "Unknown date"}.` + : "The problem feed does not yet contain a dated solved entry."} +

    + {liveCodingActivity.latestSolvedProblem ? ( + + View Latest Problem + + ) : null} +
    + +
    +
    + Latest project update + {liveCodingActivity.latestProjectUpdate?.language ? ( + {liveCodingActivity.latestProjectUpdate.language} + ) : null} +
    +

    {liveCodingActivity.latestProjectUpdate?.name || "No project update yet"}

    +

    + {liveCodingActivity.latestProjectUpdate + ? `Updated on ${formatDate(liveCodingActivity.latestProjectUpdate.updatedAt) || "Unknown date"} with ${liveCodingActivity.latestProjectUpdate.stars || 0} stars and ${liveCodingActivity.latestProjectUpdate.forks || 0} forks.` + : "The project feed does not yet contain a recent update."} +

    + {liveCodingActivity.latestProjectUpdate ? ( + + View Project Update + + ) : null} +
    + +
    +
    + Current streak + Live +
    +

    {liveCodingActivity.currentStreak} day{liveCodingActivity.currentStreak === 1 ? "" : "s"}

    +

    The streak uses the same coding activity timeline as the coding profile and counts recent solved problems plus project updates.

    + + Open Coding Hub + +
    +
    + + )} +
    + - ⭐ Star Repo + ⭐ Star on GitHub + + + + 🍴 Fork Repository + + + + 👀 Watch Repository ) : null} @@ -151,6 +171,52 @@ export default function ProjectDetail() {
    +
    +
    +

    Topics

    @@ -172,4 +238,4 @@ export default function ProjectDetail() {
    ); -} \ No newline at end of file +}