Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions src/components/Navbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default function Navbar() {
const lowLimit = rateLimit && rateLimit.remaining < 15

return (
<nav style={{
<nav className="app-navbar" style={{
position: 'sticky', top: 0, zIndex: 100,
background: 'var(--bg)',
backdropFilter: 'blur(10px)',
Expand All @@ -40,7 +40,7 @@ export default function Navbar() {
</span>

{/* Nav links — only visible when data is loaded */}
<div style={{ display: 'flex', gap: 2, flex: 1, overflowX: 'auto' }}>
<div className="app-navbar-links" style={{ display: 'flex', gap: 2, flex: 1, overflowX: 'auto' }}>
{hasData && LINKS.map(({ to, label }) => (
<NavLink
key={to} to={to}
Expand All @@ -58,7 +58,7 @@ export default function Navbar() {
</div>

{/* Right side */}
<div style={{ display: 'flex', alignItems: 'center', gap: 14, flexShrink: 0 }}>
<div className="app-navbar-actions" style={{ display: 'flex', alignItems: 'center', gap: 14, flexShrink: 0 }}>
{rateLimit && (
<div style={{ display: 'flex', alignItems: 'center', gap: 5, fontSize: 11, color: lowLimit ? 'var(--red)' : 'var(--text2)' }}>
<FiZap size={12} />
Expand All @@ -67,18 +67,22 @@ export default function Navbar() {
)}
<ThemeToggle />
<button
type="button"
aria-label="Settings"
onClick={() => navigate('/settings')}
style={{ background: 'none', border: '1px solid var(--border)', color: 'var(--text2)', borderRadius: 6, padding: '5px 10px', fontSize: 12, display: 'flex', alignItems: 'center', gap: 5 }}
className='h-[-webkit-fill-available]'
className="navbar-icon-action h-[-webkit-fill-available]"
>
<FiSettings size={13} /> Settings
<FiSettings size={13} /> <span className="navbar-action-label">Settings</span>
</button>
<button
type="button"
aria-label="Support Us"
onClick={() => navigate('/support-us')}
className="flex items-center gap-2 rounded-md bg-emerald-500 px-4 py-2 text-sm font-medium text-white shadow transition-all duration-200 hover:bg-emerald-600 hover:shadow-lg active:scale-95"
className="navbar-icon-action flex items-center gap-2 rounded-md bg-emerald-500 px-4 py-2 text-sm font-medium text-white shadow transition-all duration-200 hover:bg-emerald-600 hover:shadow-lg active:scale-95"
>
<FiHeart size={13} fill='white'/>
Support Us
<span className="navbar-action-label">Support Us</span>
</button>
</div>
</nav>
Expand Down
104 changes: 84 additions & 20 deletions src/pages/ContributorProfilePage.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, { useState, useEffect, useMemo } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { FiArrowLeft, FiDownload, FiExternalLink, FiCalendar, FiBriefcase, FiAlertTriangle } from 'react-icons/fi'
import { FiArrowLeft, FiDownload, FiExternalLink, FiCalendar, FiBriefcase, FiAlertTriangle, FiGithub } from 'react-icons/fi'
import { useApp } from '../context/AppContext'
import { C, PageTitle, Spinner, StatCard } from '../components/UI'
import SocialShareButton from '../components/SocialShareButton'
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'

// Reusable ContributionTable component
Expand Down Expand Up @@ -102,13 +103,14 @@ const getFullRepoFromUrl = (url) => {
export default function ContributorProfilePage() {
const { username } = useParams()
const navigate = useNavigate()
const { orgs, pat, pullsData } = useApp()
const { orgs, pat, pullsData, model } = useApp()

const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [rawContributions, setRawContributions] = useState([])
const [mergedPRKeys, setMergedPRKeys] = useState(new Set())
const [tab, setTab] = useState('prs')
const [avatarFailed, setAvatarFailed] = useState(false)

// Date Range Filters (Defaults to Last 1 Year)
const [startDate, setStartDate] = useState(() => {
Expand Down Expand Up @@ -327,6 +329,16 @@ export default function ContributorProfilePage() {
return Object.values(monthlyBuckets).sort((a, b) => a.yyyymm.localeCompare(b.yyyymm))
}, [filteredContribs])

const contributorAvatar = model?.contributors?.find(
contributor => contributor.login.toLowerCase() === username.toLowerCase()
)?.avatar_url
const contributionAvatar = rawContributions.find(item => item.user?.avatar_url)?.user.avatar_url
const avatarUrl = contributorAvatar || contributionAvatar

useEffect(() => {
setAvatarFailed(false)
}, [avatarUrl, username])
Comment on lines +332 to +340

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle a missing username before deriving the profile identity.

When username is missing, the effect at Line 153 stops loading but does not stop rendering. Line 333 then calls username.toLowerCase(). Line 454 also calls username.charAt(). Both calls throw before the page can show a fallback state.

Keep hooks unconditional. Then render a missing-profile state before these identity expressions execute.

Also applies to: 432-496

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/ContributorProfilePage.jsx` around lines 332 - 340, In
ContributorProfilePage, keep all hooks unconditional but add an early
missing-profile render before the avatar identity expressions and other
username-dependent rendering execute. Guard the contributor lookup in the
avatarUrl derivation and the username.charAt() usage by returning the existing
fallback state when username is absent, while preserving normal rendering for
valid usernames.


// Export to Markdown Report with pipe & newline escaping
const exportMarkdown = () => {
const dateStr = new Date().toLocaleDateString()
Expand Down Expand Up @@ -405,31 +417,83 @@ export default function ContributorProfilePage() {
{/* Back navigation & page header */}
<div style={{ marginBottom: 20 }}>
<button
type="button"
onClick={() => navigate('/contributors')}
style={{
background: 'none', border: 'none', color: 'var(--text2)',
display: 'flex', alignItems: 'center', gap: 6, fontSize: 13,
cursor: 'pointer', padding: '6px 0', marginBottom: 12
...C.btn('ghost'), color: 'var(--text2)', display: 'inline-flex',
alignItems: 'center', gap: 7, padding: '8px 14px', marginBottom: 12,
}}
className="hover:text-(--text) transition"
className="contributor-profile-back"
>
<FiArrowLeft size={14} /> Back to Contributor Intelligence
</button>
</div>

<PageTitle
title={`Contributor Profile: @${username}`}
subtitle={`Analyzing contributions across ${searchOrgs.join(', ')}`}
right={
<button
onClick={exportMarkdown}
disabled={!filteredContribs.length}
style={{ ...C.btn('primary'), display: 'flex', alignItems: 'center', gap: 6, fontSize: 12 }}
>
<FiDownload size={13} /> Export Contribution Report (.md)
</button>
}
/>
<div className="contributor-profile-heading">
<PageTitle
title={
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 12 }}>
{avatarUrl && !avatarFailed ? (
<img
src={avatarUrl}
alt=""
width="48"
height="48"
onError={() => setAvatarFailed(true)}
style={{ borderRadius: '50%', border: '1px solid var(--border)', objectFit: 'cover' }}
/>
) : (
<span
aria-hidden="true"
style={{
width: 48, height: 48, borderRadius: '50%', border: '1px solid var(--border)',
background: 'var(--surface2)', color: 'var(--accent)', display: 'inline-flex',
alignItems: 'center', justifyContent: 'center', fontSize: 20, fontWeight: 700,
}}
>
{username.charAt(0).toUpperCase()}
</span>
)}
<span>{username}</span>
<a
href={`https://github.com/${encodeURIComponent(username)}`}
target="_blank"
rel="noopener noreferrer"
className="contributor-profile-github"
aria-label={`View ${username} on GitHub`}
title="View GitHub profile"
>
<FiGithub size={20} />
</a>
</span>
}
right={
<div className="contributor-profile-actions" style={{ display: 'flex', alignItems: 'center', gap: 8, alignSelf: 'center' }}>
<SocialShareButton
url={`${window.location.origin}/contributors/${encodeURIComponent(username)}`}
title={`${username} — Contributor Profile | OrgExplorer`}
description={`View ${username}'s open-source contributions on OrgExplorer.`}
buttonText="Share Profile"
buttonStyle="ghost"
/>
<button
type="button"
className="contributor-profile-export"
onClick={exportMarkdown}
disabled={!filteredContribs.length}
style={{
...C.btn('primary'), height: 44, display: 'flex', alignItems: 'center',
justifyContent: 'center', gap: 6, fontSize: 12,
boxShadow: '0 4px 14px rgba(245,197,24,.18)',
transition: 'transform 0.2s ease, box-shadow 0.2s ease, opacity 0.15s ease',
}}
>
<FiDownload size={13} /> Export Contribution Report (.md)
</button>
</div>
}
/>
</div>

{error && (
<div style={{ ...C.card, display: 'flex', alignItems: 'center', gap: 12, borderColor: 'var(--red)', background: 'rgba(239,68,68,.05)', marginBottom: 20 }}>
Expand Down Expand Up @@ -487,7 +551,7 @@ export default function ContributorProfilePage() {
</div>

{/* Key Metrics Stats Grid */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12, marginBottom: 24 }}>
<div className="contributor-profile-stats" style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12, marginBottom: 24 }}>
<StatCard label="Total Contributions" value={filteredContribs.length} sub="Filtered timeframe" />
<StatCard label="Pull Requests" value={prs.length} sub={`${prs.filter(p => p.isMerged).length} Merged`} accent="var(--blue)" />
<StatCard label="Issues Opened" value={issues.length} sub={`${issues.filter(i => i.state === 'closed').length} Closed`} accent="var(--amber)" />
Expand Down
94 changes: 94 additions & 0 deletions src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,97 @@ a { color: var(--accent); text-decoration: none; }
.fade-up {
animation: fadeUp 0.22s ease;
}

.contributor-profile-actions .social-share-btn {
box-sizing: border-box;
height: 44px;
padding-top: 0;
padding-bottom: 0;
justify-content: center;
}

.contributor-profile-export {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.contributor-profile-export:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 8px 22px rgba(245,197,24,.25) !important;
}

.contributor-profile-back {
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}

.contributor-profile-back:hover {
background: var(--surface2) !important;
border-color: rgba(245, 197, 24, 0.35) !important;
color: var(--text) !important;
}

.contributor-profile-github {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
flex: 0 0 32px;
align-self: center;
line-height: 0;
color: var(--text2);
transition: color 0.2s ease, transform 0.2s ease;
}

.contributor-profile-github:hover {
color: var(--accent);
transform: translateY(-1px);
}

@media (max-width: 600px) {
.app-navbar {
padding-left: 12px !important;
padding-right: 12px !important;
gap: 8px !important;
}

.app-navbar-links {
min-width: 0;
}

.app-navbar-actions {
gap: 6px !important;
}

.navbar-action-label {
display: none;
}

.navbar-icon-action {
width: 32px;
height: 32px;
padding: 0 !important;
justify-content: center;
}

.contributor-profile-heading > div {
flex-direction: column;
align-items: stretch !important;
gap: 16px;
}

.contributor-profile-actions {
width: 100%;
flex-direction: column;
align-items: stretch !important;
}

.contributor-profile-actions > div,
.contributor-profile-actions .social-share-btn,
.contributor-profile-actions .contributor-profile-export {
width: 100%;
}

.contributor-profile-stats {
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
Comment on lines +66 to +156

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use alphabetical declaration order in the new CSS blocks.

Several blocks do not follow the Google CSS declaration-order rule. For example, .contributor-profile-github starts with display before align-items, and .navbar-icon-action starts with width before height.

Reorder declarations consistently in the changed blocks. As per path instructions, review CSS against the Google CSS style guide.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/styles/global.css` around lines 66 - 156, Reorder declarations
alphabetically within the newly added CSS blocks, especially
`.contributor-profile-github` and the mobile `.navbar-icon-action` rule, placing
properties such as `height` before `width` where applicable. Apply the same
alphabetical ordering consistently to the other changed selectors without
changing their values or behavior.

Source: Path instructions

}
}
Loading