-
Notifications
You must be signed in to change notification settings - Fork 0
🧹 Refactor BusinessCard component #519
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,13 +1,11 @@ | ||||||
| import { forwardRef } from "react"; | ||||||
| import type { CardBlockId, CardDisplayOptions, CardLayout, UserSummary } from "@/lib/types"; | ||||||
| import { DEFAULT_CARD_LAYOUT } from "@/lib/types"; | ||||||
| import { | ||||||
| BuildingIcon, | ||||||
| CalendarIcon, | ||||||
| LinkIcon, | ||||||
| MapPinIcon, | ||||||
| TwitterIcon, | ||||||
| } from "./Icons"; | ||||||
| import { AvatarBlock } from "./business-card/AvatarBlock"; | ||||||
| import { BioBlock } from "./business-card/BioBlock"; | ||||||
| import { StatsBlock } from "./business-card/StatsBlock"; | ||||||
| import { TopLanguagesBlock } from "./business-card/TopLanguagesBlock"; | ||||||
| import { TopReposBlock } from "./business-card/TopReposBlock"; | ||||||
|
|
||||||
| type Props = { | ||||||
| summary: UserSummary; | ||||||
|
|
@@ -21,18 +19,7 @@ const BusinessCard = forwardRef<HTMLDivElement, Props>(({ summary, layout, optio | |||||
| if (!profile) return null; | ||||||
|
|
||||||
| const activeLayout = layout ?? DEFAULT_CARD_LAYOUT; | ||||||
| const { | ||||||
| showCompany = false, | ||||||
| showLocation = false, | ||||||
| showWebsite = false, | ||||||
| showTwitter = false, | ||||||
| showJoinedDate = false, | ||||||
| showTopics = false, | ||||||
| showContributionBreakdown = false, | ||||||
| showStreaks = false, | ||||||
| showInterests = false, | ||||||
| showActivityBreakdown = false, | ||||||
| } = options || {}; | ||||||
|
|
||||||
|
|
||||||
| const topLanguages = repositories?.languages.slice(0, 5) || []; | ||||||
| const topTopics = repositories?.topics.slice(0, 10) || []; | ||||||
|
|
@@ -41,265 +28,33 @@ const BusinessCard = forwardRef<HTMLDivElement, Props>(({ summary, layout, optio | |||||
| ? profile.pinnedRepos.slice(0, 2) | ||||||
| : repositories?.topRepos.slice(0, 2) || []; | ||||||
|
|
||||||
| const renderAvatarBlock = () => ( | ||||||
| <div className="mb-10 flex items-center gap-8"> | ||||||
| {/* eslint-disable-next-line @next/next/no-img-element */} | ||||||
| <img | ||||||
| src={profile.avatar_url} | ||||||
| alt={profile.login} | ||||||
| className="h-40 w-40 rounded-full border-4 border-card-border shadow-xl" | ||||||
| crossOrigin="anonymous" | ||||||
| /> | ||||||
| <div className="min-w-0 flex-1"> | ||||||
| <h1 className="mb-2 break-words text-5xl font-bold leading-tight tracking-tight text-white"> | ||||||
| {profile.name || profile.login} | ||||||
| </h1> | ||||||
| <p className="break-all text-3xl font-medium text-gray-400">@{profile.login}</p> | ||||||
| </div> | ||||||
| </div> | ||||||
| ); | ||||||
|
|
||||||
| const renderBioBlock = () => ( | ||||||
| <> | ||||||
| <div className="mb-8"> | ||||||
| <p className="line-clamp-3 max-w-2xl text-2xl leading-relaxed text-gray-300"> | ||||||
| {profile.bio || "No bio available."} | ||||||
| </p> | ||||||
| </div> | ||||||
|
|
||||||
| {(showCompany || showLocation || showWebsite || showTwitter || showJoinedDate) && ( | ||||||
| <div className="mb-10 flex flex-wrap gap-x-8 gap-y-3 text-lg text-gray-300"> | ||||||
| {showCompany && profile.company && ( | ||||||
| <div className="flex items-center gap-2"> | ||||||
| <BuildingIcon className="text-accent" /> | ||||||
| <span>{profile.company}</span> | ||||||
| </div> | ||||||
| )} | ||||||
| {showLocation && profile.location && ( | ||||||
| <div className="flex items-center gap-2"> | ||||||
| <MapPinIcon className="text-accent" /> | ||||||
| <span>{profile.location}</span> | ||||||
| </div> | ||||||
| )} | ||||||
| {showWebsite && profile.blog && ( | ||||||
| <div className="flex items-center gap-2"> | ||||||
| <LinkIcon className="text-accent" /> | ||||||
| <span className="max-w-[200px] truncate"> | ||||||
| {profile.blog.replace(/^https?:\/\//, "")} | ||||||
| </span> | ||||||
| </div> | ||||||
| )} | ||||||
| {showTwitter && profile.twitter_username && ( | ||||||
| <div className="flex items-center gap-2"> | ||||||
| <TwitterIcon className="text-accent" /> | ||||||
| <span>@{profile.twitter_username}</span> | ||||||
| </div> | ||||||
| )} | ||||||
| {showJoinedDate && ( | ||||||
| <div className="flex items-center gap-2"> | ||||||
| <CalendarIcon className="text-accent" /> | ||||||
| <span> | ||||||
| Joined {new Date(profile.created_at).toLocaleDateString("en-US", { month: "short", year: "numeric" })} | ||||||
| </span> | ||||||
| </div> | ||||||
| )} | ||||||
| </div> | ||||||
| )} | ||||||
| </> | ||||||
| ); | ||||||
|
|
||||||
| const renderStatsBlock = () => ( | ||||||
| <> | ||||||
| <div className="grid grid-cols-3 gap-8"> | ||||||
| <div> | ||||||
| <div className="mb-1 text-4xl font-bold text-white"> | ||||||
| {(contributions?.totalContributions ?? 0).toLocaleString()} | ||||||
| </div> | ||||||
| <div className="text-lg uppercase tracking-wide text-gray-400">Contributions</div> | ||||||
| </div> | ||||||
| <div> | ||||||
| <div className="mb-1 text-4xl font-bold text-white"> | ||||||
| {profile.followers.toLocaleString()} | ||||||
| </div> | ||||||
| <div className="text-lg uppercase tracking-wide text-gray-400">Followers</div> | ||||||
| </div> | ||||||
| <div> | ||||||
| <div className="mb-1 text-4xl font-bold text-white"> | ||||||
| {profile.public_repos.toLocaleString()} | ||||||
| </div> | ||||||
| <div className="text-lg uppercase tracking-wide text-gray-400">Repositories</div> | ||||||
| </div> | ||||||
| </div> | ||||||
|
|
||||||
| {showContributionBreakdown && contributions && ( | ||||||
| <div className="mt-8 grid grid-cols-2 gap-x-8 gap-y-4"> | ||||||
| <div className="flex items-center justify-between"> | ||||||
| <span className="text-lg text-gray-400">Commits</span> | ||||||
| <span className="text-xl font-bold text-white">{contributions.totalCommits.toLocaleString()}</span> | ||||||
| </div> | ||||||
| <div className="flex items-center justify-between"> | ||||||
| <span className="text-lg text-gray-400">Pull Requests</span> | ||||||
| <span className="text-xl font-bold text-white">{contributions.totalPRs.toLocaleString()}</span> | ||||||
| </div> | ||||||
| <div className="flex items-center justify-between"> | ||||||
| <span className="text-lg text-gray-400">Issues</span> | ||||||
| <span className="text-xl font-bold text-white">{contributions.totalIssues.toLocaleString()}</span> | ||||||
| </div> | ||||||
| <div className="flex items-center justify-between"> | ||||||
| <span className="text-lg text-gray-400">Code Reviews</span> | ||||||
| <span className="text-xl font-bold text-white">{contributions.totalReviews.toLocaleString()}</span> | ||||||
| </div> | ||||||
| </div> | ||||||
| )} | ||||||
|
|
||||||
| {showStreaks && contributions && ( | ||||||
| <div className="mt-8 grid grid-cols-2 gap-8"> | ||||||
| <div> | ||||||
| <div className="mb-1 text-3xl font-bold text-white"> | ||||||
| {contributions.longestStreak} days | ||||||
| </div> | ||||||
| <div className="text-base uppercase tracking-wide text-gray-400">Longest Streak</div> | ||||||
| </div> | ||||||
| <div> | ||||||
| <div className="mb-1 text-3xl font-bold text-white"> | ||||||
| {contributions.currentStreak} days | ||||||
| </div> | ||||||
| <div className="text-base uppercase tracking-wide text-gray-400">Current Streak</div> | ||||||
| </div> | ||||||
| </div> | ||||||
| )} | ||||||
| </> | ||||||
| ); | ||||||
|
|
||||||
| const renderTopLanguagesBlock = () => ( | ||||||
| <div className="space-y-8"> | ||||||
| {topLanguages.length > 0 && ( | ||||||
| <div> | ||||||
| <h3 className="mb-4 flex items-center gap-2 text-2xl font-semibold text-accent"> | ||||||
| Top Languages | ||||||
| </h3> | ||||||
| <div className="space-y-3"> | ||||||
| {topLanguages.map((lang) => ( | ||||||
| <div key={lang.name} className="flex items-center gap-4"> | ||||||
| <span | ||||||
| className="h-4 w-4 rounded-full shadow-sm ring-2 ring-white/10" | ||||||
| style={{ backgroundColor: lang.color }} | ||||||
| /> | ||||||
| <span className="flex-1 text-xl font-medium text-gray-200">{lang.name}</span> | ||||||
| <span className="tabular-nums text-lg text-gray-500">{lang.percentage.toFixed(1)}%</span> | ||||||
| </div> | ||||||
| ))} | ||||||
| </div> | ||||||
| </div> | ||||||
| )} | ||||||
|
|
||||||
| {showTopics && topTopics.length > 0 && ( | ||||||
| <div> | ||||||
| <h3 className="mb-4 flex items-center gap-2 text-2xl font-semibold text-accent"> | ||||||
| Top Topics | ||||||
| </h3> | ||||||
| <div className="flex flex-wrap gap-2"> | ||||||
| {topTopics.map((topic) => ( | ||||||
| <span | ||||||
| key={topic.name} | ||||||
| className="break-all rounded-full bg-white/10 px-3 py-1 text-sm font-medium text-gray-200" | ||||||
| > | ||||||
| #{topic.name} | ||||||
| </span> | ||||||
| ))} | ||||||
| </div> | ||||||
| </div> | ||||||
| )} | ||||||
|
|
||||||
| {showInterests && interests && interests.topTopics.length > 0 && ( | ||||||
| <div> | ||||||
| <h3 className="mb-4 flex items-center gap-2 text-2xl font-semibold text-accent"> | ||||||
| Interests | ||||||
| </h3> | ||||||
| <div className="flex flex-wrap gap-2"> | ||||||
| {interests.topTopics.slice(0, 8).map((topic) => ( | ||||||
| <span | ||||||
| key={topic.name} | ||||||
| className="break-all rounded-full bg-accent/20 px-3 py-1 text-sm font-medium text-accent-light" | ||||||
| > | ||||||
| #{topic.name} | ||||||
| </span> | ||||||
| ))} | ||||||
| </div> | ||||||
| </div> | ||||||
| )} | ||||||
|
|
||||||
| {showActivityBreakdown && activity && activity.eventBreakdown.length > 0 && ( | ||||||
| <div> | ||||||
| <h3 className="mb-4 flex items-center gap-2 text-2xl font-semibold text-accent"> | ||||||
| Recent Activity | ||||||
| </h3> | ||||||
| <div className="space-y-2"> | ||||||
| {activity.eventBreakdown.slice(0, 5).map((event) => ( | ||||||
| <div key={event.type} className="flex items-center justify-between text-gray-300"> | ||||||
| <span>{event.type}</span> | ||||||
| <span className="font-bold">{event.count}</span> | ||||||
| </div> | ||||||
| ))} | ||||||
| </div> | ||||||
| </div> | ||||||
| )} | ||||||
| </div> | ||||||
| ); | ||||||
|
|
||||||
| const renderTopReposBlock = () => ( | ||||||
| <div> | ||||||
| {reposToShow.length > 0 && ( | ||||||
| <> | ||||||
| <h3 className="mb-4 flex items-center gap-2 text-2xl font-semibold text-accent"> | ||||||
| Top Repositories | ||||||
| </h3> | ||||||
| <div className="space-y-3"> | ||||||
| {reposToShow.map((repo) => ( | ||||||
| <div | ||||||
| key={repo.name} | ||||||
| className="rounded-xl border border-white/10 bg-white/5 p-4 shadow-lg backdrop-blur-sm" | ||||||
| > | ||||||
| <div className="mb-1 truncate text-xl font-bold text-white">{repo.name}</div> | ||||||
| <div className="flex items-center gap-6 text-base text-gray-400"> | ||||||
| {repo.primaryLanguage && ( | ||||||
| <span className="flex items-center gap-2"> | ||||||
| <span | ||||||
| className="h-3 w-3 rounded-full" | ||||||
| style={{ backgroundColor: repo.primaryLanguage.color }} | ||||||
| /> | ||||||
| {repo.primaryLanguage.name} | ||||||
| </span> | ||||||
| )} | ||||||
| <span className="flex items-center gap-1.5"> | ||||||
| <span className="text-warning">★</span> | ||||||
| {repo.stargazerCount.toLocaleString()} | ||||||
| </span> | ||||||
| </div> | ||||||
| </div> | ||||||
| ))} | ||||||
| </div> | ||||||
| </> | ||||||
| )} | ||||||
| </div> | ||||||
| ); | ||||||
|
|
||||||
| const renderBlock = (blockId: CardBlockId) => { | ||||||
| if (blockId === "avatar") { | ||||||
| return renderAvatarBlock(); | ||||||
| return <AvatarBlock profile={profile} />; | ||||||
| } | ||||||
| if (blockId === "bio") { | ||||||
| return renderBioBlock(); | ||||||
| return <BioBlock profile={profile} options={options || {}} />; | ||||||
| } | ||||||
| if (blockId === "stats") { | ||||||
| return renderStatsBlock(); | ||||||
| return <StatsBlock profile={profile} contributions={contributions} options={options || {}} />; | ||||||
| } | ||||||
| if (blockId === "topLanguages") { | ||||||
| return renderTopLanguagesBlock(); | ||||||
| return ( | ||||||
| <TopLanguagesBlock | ||||||
| topLanguages={topLanguages} | ||||||
| topTopics={topTopics} | ||||||
| interests={interests} | ||||||
| activity={activity} | ||||||
| options={options || {}} | ||||||
| /> | ||||||
| ); | ||||||
| } | ||||||
| if (blockId === "topRepos") { | ||||||
| return <TopReposBlock reposToShow={reposToShow} />; | ||||||
| } | ||||||
| return renderTopReposBlock(); | ||||||
| return null; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Knowledge Base Used: Frontend component library (src/components) Prompt To Fix With AIThis is a comment left during a code review.
Path: src/components/BusinessCard.tsx
Line: 56
Comment:
**既定ブロックの描画消失**
`DEFAULT_CARD_LAYOUT` に含まれる `profile`、`contributions`、`heatmap`、`interests`、`skills` は上の分岐で処理されないため、ここで `null` となります。設定未保存時の標準プレビューと PNG エクスポートでは、以前リポジトリ内容を表示していたこれらのスロットが空になります。
```suggestion
return <TopReposBlock reposToShow={reposToShow} />;
```
**Knowledge Base Used:** [Frontend component library (src/components)](https://app.greptile.com/hiroki-org/-/custom-context/knowledge-base/hiroki-org/github-user-summary/-/docs/frontend-components.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Comment on lines
32
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2. Unhandled blocks render nothing BusinessCard.renderBlock returns null for valid CardBlockId values like "profile", "contributions", "heatmap", "interests", and "skills", so those layout sections render no content. Since DEFAULT_CARD_LAYOUT includes these IDs as visible full-column blocks (and layout normalization re-adds them), the default/normalized BusinessCard output can silently drop entire sections. Agent Prompt
|
||||||
| }; | ||||||
|
|
||||||
| const fullBlocks = activeLayout.blocks.filter((block) => block.column === "full" && block.visible); | ||||||
| const leftBlocks = activeLayout.blocks.filter((block) => block.column === "left" && block.visible); | ||||||
| const rightBlocks = activeLayout.blocks.filter((block) => block.column === "right" && block.visible); | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import type { UserProfile } from "@/lib/types"; | ||
|
|
||
| export const AvatarBlock = ({ profile }: { profile: UserProfile }) => ( | ||
| <div className="mb-10 flex items-center gap-8"> | ||
| {/* eslint-disable-next-line @next/next/no-img-element */} | ||
| <img | ||
| src={profile.avatar_url} | ||
| alt={profile.login} | ||
| className="h-40 w-40 rounded-full border-4 border-card-border shadow-xl" | ||
| crossOrigin="anonymous" | ||
| /> | ||
| <div className="min-w-0 flex-1"> | ||
| <h1 className="mb-2 break-words text-5xl font-bold leading-tight tracking-tight text-white"> | ||
| {profile.name || profile.login} | ||
| </h1> | ||
| <p className="break-all text-3xl font-medium text-gray-400">@{profile.login}</p> | ||
| </div> | ||
| </div> | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. Relative imports in businesscard.tsx
📘 Rule violation✧ QualityAgent Prompt
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools