diff --git a/src/components/BusinessCard.tsx b/src/components/BusinessCard.tsx index 47820e12..fa3d03d6 100644 --- a/src/components/BusinessCard.tsx +++ b/src/components/BusinessCard.tsx @@ -2,12 +2,12 @@ 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"; + AvatarBlock, + BioBlock, + StatsBlock, + TopLanguagesBlock, + TopReposBlock, +} from "./business-card"; type Props = { summary: UserSummary; @@ -21,18 +21,6 @@ const BusinessCard = forwardRef(({ 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,263 +29,28 @@ const BusinessCard = forwardRef(({ summary, layout, optio ? profile.pinnedRepos.slice(0, 2) : repositories?.topRepos.slice(0, 2) || []; - const renderAvatarBlock = () => ( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - {profile.login} -
-

- {profile.name || profile.login} -

-

@{profile.login}

-
-
- ); - - const renderBioBlock = () => ( - <> -
-

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

-
- - {(showCompany || showLocation || showWebsite || showTwitter || showJoinedDate) && ( -
- {showCompany && profile.company && ( -
- - {profile.company} -
- )} - {showLocation && profile.location && ( -
- - {profile.location} -
- )} - {showWebsite && profile.blog && ( -
- - - {profile.blog.replace(/^https?:\/\//, "")} - -
- )} - {showTwitter && profile.twitter_username && ( -
- - @{profile.twitter_username} -
- )} - {showJoinedDate && ( -
- - - Joined {new Date(profile.created_at).toLocaleDateString("en-US", { month: "short", year: "numeric" })} - -
- )} -
- )} - - ); - - const renderStatsBlock = () => ( - <> -
-
-
- {(contributions?.totalContributions ?? 0).toLocaleString()} -
-
Contributions
-
-
-
- {profile.followers.toLocaleString()} -
-
Followers
-
-
-
- {profile.public_repos.toLocaleString()} -
-
Repositories
-
-
- - {showContributionBreakdown && contributions && ( -
-
- Commits - {contributions.totalCommits.toLocaleString()} -
-
- Pull Requests - {contributions.totalPRs.toLocaleString()} -
-
- Issues - {contributions.totalIssues.toLocaleString()} -
-
- Code Reviews - {contributions.totalReviews.toLocaleString()} -
-
- )} - - {showStreaks && contributions && ( -
-
-
- {contributions.longestStreak} days -
-
Longest Streak
-
-
-
- {contributions.currentStreak} days -
-
Current Streak
-
-
- )} - - ); - - const renderTopLanguagesBlock = () => ( -
- {topLanguages.length > 0 && ( -
-

- Top Languages -

-
- {topLanguages.map((lang) => ( -
- - {lang.name} - {lang.percentage.toFixed(1)}% -
- ))} -
-
- )} - - {showTopics && topTopics.length > 0 && ( -
-

- Top Topics -

-
- {topTopics.map((topic) => ( - - #{topic.name} - - ))} -
-
- )} - - {showInterests && interests && interests.topTopics.length > 0 && ( -
-

- Interests -

-
- {interests.topTopics.slice(0, 8).map((topic) => ( - - #{topic.name} - - ))} -
-
- )} - - {showActivityBreakdown && activity && activity.eventBreakdown.length > 0 && ( -
-

- Recent Activity -

-
- {activity.eventBreakdown.slice(0, 5).map((event) => ( -
- {event.type} - {event.count} -
- ))} -
-
- )} -
- ); - - const renderTopReposBlock = () => ( -
- {reposToShow.length > 0 && ( - <> -

- Top Repositories -

-
- {reposToShow.map((repo) => ( -
-
{repo.name}
-
- {repo.primaryLanguage && ( - - - {repo.primaryLanguage.name} - - )} - - - {repo.stargazerCount.toLocaleString()} - -
-
- ))} -
- - )} -
- ); - const renderBlock = (blockId: CardBlockId) => { if (blockId === "avatar") { - return renderAvatarBlock(); + return ; } if (blockId === "bio") { - return renderBioBlock(); + return ; } if (blockId === "stats") { - return renderStatsBlock(); + return ; } if (blockId === "topLanguages") { - return renderTopLanguagesBlock(); + return ( + + ); } - return renderTopReposBlock(); + return ; }; const fullBlocks = activeLayout.blocks.filter((block) => block.column === "full" && block.visible); diff --git a/src/components/business-card/AvatarBlock.tsx b/src/components/business-card/AvatarBlock.tsx new file mode 100644 index 00000000..c93ca90c --- /dev/null +++ b/src/components/business-card/AvatarBlock.tsx @@ -0,0 +1,23 @@ +import type { UserProfile } from "@/lib/types"; + +type Props = { + profile: UserProfile; +}; + +export const AvatarBlock = ({ profile }: Props) => ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {profile.login} +
+

+ {profile.name || profile.login} +

+

@{profile.login}

+
+
+); diff --git a/src/components/business-card/BioBlock.tsx b/src/components/business-card/BioBlock.tsx new file mode 100644 index 00000000..8f6f1215 --- /dev/null +++ b/src/components/business-card/BioBlock.tsx @@ -0,0 +1,72 @@ +import type { UserProfile, CardDisplayOptions } from "@/lib/types"; +import { + BuildingIcon, + CalendarIcon, + LinkIcon, + MapPinIcon, + TwitterIcon, +} from "../Icons"; + +type Props = { + profile: UserProfile; + options: CardDisplayOptions; +}; + +export const BioBlock = ({ profile, options }: Props) => { + const { + showCompany = false, + showLocation = false, + showWebsite = false, + showTwitter = false, + showJoinedDate = false, + } = options; + + return ( + <> +
+

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

+
+ + {(showCompany || showLocation || showWebsite || showTwitter || showJoinedDate) && ( +
+ {showCompany && profile.company && ( +
+ + {profile.company} +
+ )} + {showLocation && profile.location && ( +
+ + {profile.location} +
+ )} + {showWebsite && profile.blog && ( +
+ + + {profile.blog.replace(/^https?:\/\//, "")} + +
+ )} + {showTwitter && profile.twitter_username && ( +
+ + @{profile.twitter_username} +
+ )} + {showJoinedDate && ( +
+ + + Joined {new Date(profile.created_at).toLocaleDateString("en-US", { month: "short", year: "numeric" })} + +
+ )} +
+ )} + + ); +}; diff --git a/src/components/business-card/StatsBlock.tsx b/src/components/business-card/StatsBlock.tsx new file mode 100644 index 00000000..58d8b57f --- /dev/null +++ b/src/components/business-card/StatsBlock.tsx @@ -0,0 +1,77 @@ +import type { UserProfile, ContributionData, CardDisplayOptions } from "@/lib/types"; + +type Props = { + profile: UserProfile; + contributions: ContributionData | null; + options: CardDisplayOptions; +}; + +export const StatsBlock = ({ profile, contributions, options }: Props) => { + const { + showContributionBreakdown = false, + showStreaks = false, + } = options; + + return ( + <> +
+
+
+ {(contributions?.totalContributions ?? 0).toLocaleString()} +
+
Contributions
+
+
+
+ {profile.followers.toLocaleString()} +
+
Followers
+
+
+
+ {profile.public_repos.toLocaleString()} +
+
Repositories
+
+
+ + {showContributionBreakdown && contributions && ( +
+
+ Commits + {contributions.totalCommits.toLocaleString()} +
+
+ Pull Requests + {contributions.totalPRs.toLocaleString()} +
+
+ Issues + {contributions.totalIssues.toLocaleString()} +
+
+ Code Reviews + {contributions.totalReviews.toLocaleString()} +
+
+ )} + + {showStreaks && contributions && ( +
+
+
+ {contributions.longestStreak} days +
+
Longest Streak
+
+
+
+ {contributions.currentStreak} days +
+
Current Streak
+
+
+ )} + + ); +}; diff --git a/src/components/business-card/TopLanguagesBlock.tsx b/src/components/business-card/TopLanguagesBlock.tsx new file mode 100644 index 00000000..5baf4acf --- /dev/null +++ b/src/components/business-card/TopLanguagesBlock.tsx @@ -0,0 +1,93 @@ +import type { LanguageStats, InterestsData, ActivityData, CardDisplayOptions } from "@/lib/types"; + +type Props = { + topLanguages: LanguageStats[]; + topTopics: { name: string; count: number }[]; + interests: InterestsData | null; + activity: ActivityData | null; + options: CardDisplayOptions; +}; + +export const TopLanguagesBlock = ({ topLanguages, topTopics, interests, activity, options }: Props) => { + const { + showTopics = false, + showInterests = false, + showActivityBreakdown = false, + } = options; + + return ( +
+ {topLanguages.length > 0 && ( +
+

+ Top Languages +

+
+ {topLanguages.map((lang) => ( +
+ + {lang.name} + {lang.percentage.toFixed(1)}% +
+ ))} +
+
+ )} + + {showTopics && topTopics.length > 0 && ( +
+

+ Top Topics +

+
+ {topTopics.map((topic) => ( + + #{topic.name} + + ))} +
+
+ )} + + {showInterests && interests && interests.topTopics.length > 0 && ( +
+

+ Interests +

+
+ {interests.topTopics.slice(0, 8).map((topic) => ( + + #{topic.name} + + ))} +
+
+ )} + + {showActivityBreakdown && activity && activity.eventBreakdown.length > 0 && ( +
+

+ Recent Activity +

+
+ {activity.eventBreakdown.slice(0, 5).map((event) => ( +
+ {event.type} + {event.count} +
+ ))} +
+
+ )} +
+ ); +}; diff --git a/src/components/business-card/TopReposBlock.tsx b/src/components/business-card/TopReposBlock.tsx new file mode 100644 index 00000000..698bd038 --- /dev/null +++ b/src/components/business-card/TopReposBlock.tsx @@ -0,0 +1,42 @@ +import type { PinnedRepo, TopRepo } from "@/lib/types"; + +type Props = { + reposToShow: (PinnedRepo | TopRepo)[]; +}; + +export const TopReposBlock = ({ reposToShow }: Props) => ( +
+ {reposToShow.length > 0 && ( + <> +

+ Top Repositories +

+
+ {reposToShow.map((repo) => ( +
+
{repo.name}
+
+ {repo.primaryLanguage && ( + + + {repo.primaryLanguage.name} + + )} + + + {repo.stargazerCount.toLocaleString()} + +
+
+ ))} +
+ + )} +
+); diff --git a/src/components/business-card/index.ts b/src/components/business-card/index.ts new file mode 100644 index 00000000..bd0c9659 --- /dev/null +++ b/src/components/business-card/index.ts @@ -0,0 +1,5 @@ +export { AvatarBlock } from "./AvatarBlock"; +export { BioBlock } from "./BioBlock"; +export { StatsBlock } from "./StatsBlock"; +export { TopLanguagesBlock } from "./TopLanguagesBlock"; +export { TopReposBlock } from "./TopReposBlock";