GitHub Profile Card
Beautiful GitHub profile cards with customizable themes, activity graphs, and real-time data fetching.
GitHub Profile Card
Select Theme
MI
Mihir Jaiswal
MihirJaiswal
👨💻 Aspiring Web Developer/AI engineer | Engineering 🚀|🤖 Turning coffee ☕ into code... and occasionally, code into coffee. ☕🖥️
Indore, India
Joined Jun 2023
16followers
0following
41repos
Top Languages
TypeScript
69%JavaScript
12%HTML
11%Activity
Installation Guide
1
Install Dependencies
UI Components
npx shadcn@latest init
Utility Functions
npm install clsx tailwind-merge
2
Setup Configuration
Create file:
/lib/utils.ts
/lib/utils.ts
Configuration1import { clsx, type ClassValue } from "clsx";
2import { twMerge } from "tailwind-merge";
3
4export function cn(...inputs: ClassValue[]) {
5 return twMerge(clsx(inputs));
6}
3
Copy Component Code
GitHub Profile Card.tsx
TypeScript1"use client";
2
3import { useState, useEffect, useCallback } from "react";
4import Link from "next/link";
5import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
6import { Button } from "@/components/ui/button";
7import {
8 Card,
9 CardContent,
10 CardFooter,
11 CardHeader,
12} from "@/components/ui/card";
13import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
14import {
15 Tooltip,
16 TooltipContent,
17 TooltipProvider,
18 TooltipTrigger,
19} from "@/components/ui/tooltip";
20import {
21 AtSign,
22 BookOpen,
23 Calendar,
24 ExternalLink,
25 Github,
26 GitFork,
27 MapPin,
28 Star,
29 AlertCircle,
30} from "lucide-react";
31import { cn } from "@/lib/utils";
32
33export type ThemeOption = {
34 id: string;
35 name: string;
36 description: string;
37 cardBg: string;
38 cardBorder: string;
39 cardHoverShadow: string;
40 accentColor: string;
41 accentColorLight: string;
42 graphColor: string;
43 graphBgColor: string;
44 badgeBg: string;
45 badgeText: string;
46 textMuted: string;
47 textNormal: string;
48 tabsBg: string;
49};
50
51export const themes: ThemeOption[] = [
52 {
53 id: "github-light",
54 name: "GitHub Light",
55 description: "The classic GitHub light theme",
56 cardBg: "bg-white",
57 cardBorder: "border border-border",
58 cardHoverShadow: "hover:shadow-md",
59 accentColor: "text-[#0969da] dark:text-[#0550a0]",
60 accentColorLight: "text-[#0969da] dark:text-[#0550a0]",
61 graphColor: "text-[#0550a0] dark:text-[#0969da]",
62 graphBgColor: "text-[#0969da]/10 dark:text-[#0550a0]/10",
63 badgeBg: "bg-[#f6f8fa] dark:bg-[#24292f]",
64 badgeText: "text-[#24292f] dark:text-[#24292f]",
65 textMuted: "text-gray-700",
66 textNormal: "text-black",
67 tabsBg: "bg-[#bbd5fc] dark:bg-[#74aafc]",
68 },
69 {
70 id: "github-dark",
71 name: "GitHub Dark",
72 description: "GitHub's dark mode theme",
73 cardBg: "bg-[#0d1117]",
74 cardBorder: "border border-[#30363d]",
75 cardHoverShadow: "hover:shadow-md hover:shadow-black/20",
76 accentColor: "text-[#58a6ff]",
77 accentColorLight: "text-[#58a6ff]/10",
78 graphColor: "text-[#58a6ff]",
79 graphBgColor: "text-[#58a6ff]/10",
80 badgeBg: "bg-[#21262d]",
81 badgeText: "text-[#c9d1d9]",
82 textMuted: "text-gray-300",
83 textNormal: "text-white",
84 tabsBg: "bg-[#161b22] dark:bg-[#74aafc]",
85 },
86 {
87 id: "ocean",
88 name: "Ocean",
89 description: "A calming blue theme",
90 cardBg: "bg-[#f0f7ff] dark:bg-[#051c33]",
91 cardBorder: "border border-[#cce4ff] dark:border-[#0a3866]",
92 cardHoverShadow:
93 "hover:shadow-md hover:shadow-blue-200 dark:hover:shadow-md dark:hover:shadow-blue-900/30",
94 accentColor: "text-[#0057b7] dark:text-[#58a6ff]",
95 accentColorLight: "text-[#0057b7]/10 dark:text-[#58a6ff]/10",
96 graphColor: "text-[#0057b7] dark:text-[#58a6ff]",
97 graphBgColor: "text-[#0057b7]/10 dark:text-[#58a6ff]/10",
98 badgeBg: "bg-[#e0f0ff] dark:bg-[#0a3866]",
99 badgeText: "text-[#0057b7] dark:text-[#88bbff]",
100 textMuted: "text-gray-700 dark:text-gray-300",
101 textNormal: "text-black dark:text-white",
102 tabsBg: "bg-[#dbeaff] dark:bg-[#0a2b4d]",
103 },
104 {
105 id: "forest",
106 name: "Forest",
107 description: "A refreshing green theme",
108 cardBg: "bg-[#f0fff4] dark:bg-[#071f0e]",
109 cardBorder: "border border-[#c6f6d5] dark:border-[#1a4031]",
110 cardHoverShadow:
111 "hover:shadow-md hover:shadow-green-200 dark:hover:shadow-md dark:hover:shadow-green-900/30",
112 accentColor: "text-[#2f855a] dark:text-[#4ade80]",
113 accentColorLight: "text-[#2f855a]/10 dark:text-[#4ade80]/10",
114 graphColor: "text-[#2f855a] dark:text-[#4ade80]",
115 graphBgColor: "text-[#2f855a]/10 dark:text-[#4ade80]/10",
116 badgeBg: "bg-[#e0fff0] dark:bg-[#1a4031]",
117 badgeText: "text-[#2f855a] dark:text-[#7eeaa4]",
118 textMuted: "text-gray-700 dark:text-gray-300",
119 textNormal: "text-black dark:text-white",
120 tabsBg: "bg-[#d7f7e0] dark:bg-[#133929]",
121 },
122 {
123 id: "sunset",
124 name: "Sunset",
125 description: "A warm orange theme",
126 cardBg: "bg-[#fff7ed] dark:bg-[#271807]",
127 cardBorder: "border border-[#ffedd5] dark:border-[#4a2912]",
128 cardHoverShadow:
129 "hover:shadow-md hover:shadow-orange-200 dark:hover:shadow-md dark:hover:shadow-orange-900/30",
130 accentColor: "text-[#9a3412]",
131 accentColorLight: "text-[#c2410c]/10 dark:text-[#fb923c]/10",
132 graphColor: "text-[#9a3412]",
133 graphBgColor: "text-[#c2410c]/10 dark:text-[#fb923c]/10",
134 badgeBg: "bg-[#ffedd5] dark:bg-[#4a2912]",
135 badgeText: "text-[#9a3412] dark:text-[#fdba74]",
136 textMuted: "text-gray-700 dark:text-gray-300",
137 textNormal: "text-black dark:text-white",
138 tabsBg: "bg-[#ffe8cc] dark:bg-[#3e230f]",
139 },
140 {
141 id: "nuvyx",
142 name: "Nuvyx",
143 description: "A cyberpunk theme with purple accents",
144 cardBg: "bg-[#0f0f13] dark:bg-[#0f0f13]",
145 cardBorder: "border border-[#2a2a3a] dark:border-[#2a2a3a]",
146 cardHoverShadow:
147 "hover:shadow-md hover:shadow-purple-900/30 dark:hover:shadow-md dark:hover:shadow-purple-900/30",
148 accentColor: "text-[#b48eff] dark:text-[#b48eff]",
149 accentColorLight: "text-[#b48eff]/20 dark:text-[#b48eff]/20",
150 graphColor: "text-[#b48eff] dark:text-[#b48eff]",
151 graphBgColor: "text-[#b48eff]/10 dark:text-[#b48eff]/10",
152 badgeBg: "bg-[#2a2a3a] dark:bg-[#2a2a3a]",
153 badgeText: "text-[#c4b5fd] dark:text-[#c4b5fd]",
154 textMuted: "text-gray-300",
155 textNormal: "text-white",
156 tabsBg: "bg-[#1a1a2e] dark:bg-[#1a1a2e]",
157 },
158];
159
160export type ManualProfileData = {
161 login: string;
162 name: string;
163 avatarUrl: string;
164 bio?: string;
165 location?: string;
166 followers: number;
167 following: number;
168 publicRepos: number;
169 createdAt: string;
170 languages?: Array<{
171 name: string;
172 color: string;
173 percentage: number;
174 }>;
175 pinnedRepos?: Array<{
176 name: string;
177 description?: string;
178 language?: string;
179 languageColor?: string;
180 stars: number;
181 forks: number;
182 }>;
183 contributionData?: Array<number>;
184};
185
186// Define proper types for GitHub API responses
187interface GitHubUserProfile {
188 login: string;
189 name: string | null;
190 avatar_url: string;
191 bio: string | null;
192 location: string | null;
193 followers: number;
194 following: number;
195 public_repos: number;
196 created_at: string;
197}
198
199interface GitHubRepository {
200 name: string;
201 description: string | null;
202 language: string | null;
203 stargazers_count: number;
204 forks_count: number;
205}
206
207interface GitHubProfileCardProps {
208 username?: string;
209 githubToken?: string;
210 manualMode?: boolean;
211 profileData?: ManualProfileData;
212 themeId?: string;
213}
214
215export function GitHubProfileCard({
216 username,
217 githubToken,
218 manualMode = false,
219 profileData,
220 themeId,
221}: GitHubProfileCardProps) {
222 const currentTheme =
223 themes.find((theme) => theme.id === themeId) || themes[0];
224 const [isLoaded, setIsLoaded] = useState(manualMode);
225 const [loading, setLoading] = useState(!manualMode);
226 const [error, setError] = useState<string | null>(null);
227 const [profile, setProfile] = useState<ManualProfileData | null>(
228 manualMode ? profileData || null : null
229 );
230 const [rateLimit, setRateLimit] = useState<{
231 remaining: number;
232 limit: number;
233 } | null>(null);
234
235 const fetchProfileData = useCallback(async () => {
236 setLoading(true);
237 setError(null);
238
239 try {
240 const headers: HeadersInit = {};
241 if (githubToken) {
242 headers.Authorization = `token ${githubToken}`;
243 }
244
245 const profileResponse = await fetch(
246 `https://api.github.com/users/${username}`,
247 {
248 headers,
249 }
250 );
251
252 const rateLimitRemaining = profileResponse.headers.get(
253 "x-ratelimit-remaining"
254 );
255 const rateLimitLimit = profileResponse.headers.get("x-ratelimit-limit");
256
257 if (rateLimitRemaining && rateLimitLimit) {
258 setRateLimit({
259 remaining: Number.parseInt(rateLimitRemaining, 10),
260 limit: Number.parseInt(rateLimitLimit, 10),
261 });
262 }
263
264 if (!profileResponse.ok) {
265 if (profileResponse.status === 403 && rateLimitRemaining === "0") {
266 throw new Error(
267 "GitHub API rate limit exceeded. Please provide a GitHub token."
268 );
269 } else {
270 throw new Error(
271 `Failed to fetch profile data: ${profileResponse.status}`
272 );
273 }
274 }
275
276 const profileData: GitHubUserProfile = await profileResponse.json();
277
278 let pinnedRepos: Array<{
279 name: string;
280 description?: string;
281 language?: string;
282 languageColor?: string;
283 stars: number;
284 forks: number;
285 }> = [];
286
287 try {
288 const reposResponse = await fetch(
289 `https://api.github.com/users/${username}/repos?sort=updated&per_page=4`,
290 {
291 headers,
292 }
293 );
294
295 if (reposResponse.ok) {
296 const repos: GitHubRepository[] = await reposResponse.json();
297 pinnedRepos = repos.map((repo) => ({
298 name: repo.name,
299 description: repo.description || undefined,
300 language: repo.language || undefined,
301 languageColor: getLanguageColor(repo.language || undefined),
302 stars: repo.stargazers_count,
303 forks: repo.forks_count,
304 }));
305 }
306 } catch (err) {
307 console.error(
308 "Failed to fetch repos, continuing with profile data",
309 err
310 );
311 }
312
313 const contributionData = Array.from({ length: 12 }, () => Math.random());
314
315 const languages = [
316 { name: "JavaScript", color: "#f1e05a", percentage: 40 },
317 { name: "TypeScript", color: "#3178c6", percentage: 30 },
318 { name: "Python", color: "#3572A5", percentage: 20 },
319 ];
320
321 const transformedProfile: ManualProfileData = {
322 login: profileData.login,
323 name: profileData.name || profileData.login,
324 avatarUrl: profileData.avatar_url,
325 bio: profileData.bio || undefined,
326 location: profileData.location || undefined,
327 followers: profileData.followers,
328 following: profileData.following,
329 publicRepos: profileData.public_repos,
330 createdAt: profileData.created_at,
331 languages: languages,
332 pinnedRepos: pinnedRepos,
333 contributionData: contributionData,
334 };
335
336 setProfile(transformedProfile);
337 setLoading(false);
338 setIsLoaded(true);
339 } catch (err) {
340 setError(
341 err instanceof Error ? err.message : "An unknown error occurred"
342 );
343 setLoading(false);
344 }
345 }, [username, githubToken]);
346
347 useEffect(() => {
348 if (manualMode && profileData) {
349 setProfile(profileData);
350 setLoading(false);
351 setIsLoaded(true);
352 return;
353 }
354
355 if (!manualMode && username) {
356 fetchProfileData();
357 }
358 }, [manualMode, profileData, username, githubToken, fetchProfileData]);
359
360 const formatDate = (dateString: string) => {
361 const date = new Date(dateString);
362 return new Intl.DateTimeFormat("en-US", {
363 year: "numeric",
364 month: "short",
365 }).format(date);
366 };
367
368 const getYearsOnGitHub = (dateString: string) => {
369 const joinDate = new Date(dateString);
370 const now = new Date();
371 const diffTime = Math.abs(now.getTime() - joinDate.getTime());
372 const diffYears = diffTime / (1000 * 60 * 60 * 24 * 365.25);
373 return Math.floor(diffYears);
374 };
375
376 const getLanguageColor = (language?: string) => {
377 if (!language) return "#858585";
378
379 const languageColors: Record<string, string> = {
380 JavaScript: "#f1e05a",
381 TypeScript: "#3178c6",
382 Python: "#3572A5",
383 Java: "#b07219",
384 Go: "#00ADD8",
385 Rust: "#dea584",
386 C: "#555555",
387 "C++": "#f34b7d",
388 "C#": "#178600",
389 PHP: "#4F5D95",
390 Ruby: "#701516",
391 Swift: "#F05138",
392 Kotlin: "#A97BFF",
393 Dart: "#00B4AB",
394 HTML: "#e34c26",
395 CSS: "#563d7c",
396 Shell: "#89e051",
397 };
398
399 return languageColors[language] || "#858585";
400 };
401
402 if (loading) {
403 return (
404 <Card
405 className={cn(
406 "w-full max-w-md overflow-hidden transition-all duration-300",
407 currentTheme.cardBg,
408 currentTheme.cardBorder
409 )}
410 >
411 <CardContent className="flex h-40 items-center justify-center">
412 <div className="flex flex-col items-center gap-2">
413 <div className="h-6 w-6 animate-spin rounded-full border-2 border-current border-t-transparent" />
414 <p className="text-sm font-medium text-muted-foreground">
415 Loading profile...
416 </p>
417 </div>
418 </CardContent>
419 </Card>
420 );
421 }
422
423 if (error) {
424 return (
425 <Card
426 className={cn(
427 "w-full max-w-md overflow-hidden transition-all duration-300",
428 currentTheme.cardBg,
429 currentTheme.cardBorder
430 )}
431 >
432 <CardContent className="flex h-36 items-center justify-center">
433 <div className="flex flex-col items-center gap-2">
434 <AlertCircle className="h-8 w-8 text-red-500" />
435 <p className="text-center text-sm font-medium text-muted-foreground">
436 Failed to load profile data.
437 <br />
438 {error}
439 </p>
440 {error.includes("rate limit") && (
441 <Button
442 variant="outline"
443 size="sm"
444 className="mt-2"
445 onClick={() => setError(null)}
446 >
447 Use manual mode instead
448 </Button>
449 )}
450 </div>
451 </CardContent>
452 </Card>
453 );
454 }
455
456 if (!profile) return null;
457
458 return (
459 <>
460 {isLoaded && (
461 <div className="w-full max-w-md">
462 <Card
463 className={cn(
464 "overflow-hidden transition-all duration-300",
465 currentTheme.cardBg,
466 currentTheme.cardBorder,
467 currentTheme.cardHoverShadow,
468 currentTheme.id === "github-light" &&
469 "bg-gradient-to-br from-white to-gray-50",
470 currentTheme.id === "github-dark" &&
471 "bg-gradient-to-br from-[#0d1117] to-[#161b22]",
472 currentTheme.id === "ocean" &&
473 "bg-gradient-to-br from-[#f0f7ff] to-[#e6f0ff] dark:from-[#051c33] dark:to-[#072440]",
474 currentTheme.id === "forest" &&
475 "bg-gradient-to-br from-[#f0fff4] to-[#e6ffec] dark:from-[#071f0e] dark:to-[#0a2a13]",
476 currentTheme.id === "sunset" &&
477 "bg-gradient-to-br from-[#fff7ed] to-[#fff0e0] dark:from-[#271807] dark:to-[#33200a]",
478 currentTheme.id === "nuvyx" &&
479 "bg-gradient-to-br from-[#0f0f13] to-[#13131a]"
480 )}
481 >
482 <CardHeader className="pb-2 pt-3">
483 <div className="flex items-start gap-4">
484 <div className="relative">
485 <Avatar className="h-12 w-12 border-2 border-background shadow-sm">
486 <AvatarImage src={profile.avatarUrl} alt={profile.name} />
487 <AvatarFallback>
488 {profile.name.substring(0, 2).toUpperCase()}
489 </AvatarFallback>
490 </Avatar>
491 </div>
492
493 <div className="flex-1 space-y-1">
494 <div className="flex items-center justify-between">
495 <div>
496 <h2
497 className={cn(
498 "text-lg font-bold leading-none",
499 currentTheme.textNormal
500 )}
501 >
502 {profile.name}
503 </h2>
504 <div
505 className={cn(
506 "flex items-center text-sm text-muted-foreground",
507 currentTheme.textMuted
508 )}
509 >
510 <AtSign
511 className={cn("mr-1 h-3 w-3", currentTheme.textMuted)}
512 />
513 <span
514 className={cn("font-medium", currentTheme.textMuted)}
515 >
516 {profile.login}
517 </span>
518 </div>
519 </div>
520 <Button
521 variant="outline"
522 size="sm"
523 className={cn(
524 "h-8 gap-1 text-xs",
525 currentTheme.accentColor
526 )}
527 asChild
528 >
529 <Link
530 href={`https://github.com/${profile.login}`}
531 target="_blank"
532 >
533 <Github
534 className={cn("h-3 w-3", currentTheme.accentColor)}
535 />
536 Follow
537 </Link>
538 </Button>
539 </div>
540
541 {profile.bio && (
542 <p
543 className={cn(
544 "line-clamp-2 text-sm text-muted-foreground",
545 currentTheme.textMuted
546 )}
547 >
548 {profile.bio}
549 </p>
550 )}
551
552 <div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
553 {profile.location && (
554 <div className="flex items-center gap-1">
555 <MapPin
556 className={cn("h-3 w-3", currentTheme.textMuted)}
557 />
558 <span
559 className={cn("font-medium", currentTheme.textMuted)}
560 >
561 {profile.location}
562 </span>
563 </div>
564 )}
565 <div className="flex items-center gap-1">
566 <Calendar
567 className={cn("h-3 w-3", currentTheme.textMuted)}
568 />
569 <span
570 className={cn("font-medium", currentTheme.textMuted)}
571 >
572 Joined {formatDate(profile.createdAt)}
573 </span>
574 </div>
575 {rateLimit && !manualMode && (
576 <div className="ml-auto text-xs">
577 <Tooltip>
578 <TooltipTrigger asChild>
579 <span
580 className={cn(
581 "font-medium",
582 currentTheme.textMuted
583 )}
584 >
585 {rateLimit.remaining}/{rateLimit.limit}
586 </span>
587 </TooltipTrigger>
588 <TooltipContent>
589 <p
590 className={cn("text-sm", currentTheme.textMuted)}
591 >
592 GitHub API requests remaining
593 </p>
594 </TooltipContent>
595 </Tooltip>
596 </div>
597 )}
598 </div>
599 </div>
600 </div>
601 </CardHeader>
602
603 <CardContent className="pb-2 pt-0">
604 <div className="mb-3 grid grid-cols-3 gap-2 rounded-md border p-1.5 text-center">
605 <div className="flex flex-col">
606 <span
607 className={cn(
608 "text-base font-semibold",
609 currentTheme.textNormal
610 )}
611 >
612 {profile.followers.toLocaleString()}
613 </span>
614 <span
615 className={cn(
616 "text-xs text-muted-foreground",
617 currentTheme.textMuted
618 )}
619 >
620 followers
621 </span>
622 </div>
623 <div className="flex flex-col">
624 <span
625 className={cn(
626 "text-base font-semibold",
627 currentTheme.textNormal
628 )}
629 >
630 {profile.following.toLocaleString()}
631 </span>
632 <span
633 className={cn(
634 "text-xs text-muted-foreground",
635 currentTheme.textMuted
636 )}
637 >
638 following
639 </span>
640 </div>
641 <div className="flex flex-col">
642 <span
643 className={cn(
644 "text-base font-semibold",
645 currentTheme.textNormal
646 )}
647 >
648 {profile.publicRepos.toLocaleString()}
649 </span>
650 <span
651 className={cn(
652 "text-xs text-muted-foreground",
653 currentTheme.textMuted
654 )}
655 >
656 repos
657 </span>
658 </div>
659 </div>
660
661 <div>
662 <Tabs defaultValue="overview" className="w-full">
663 <TabsList
664 className={cn(
665 "grid w-full grid-cols-2",
666 currentTheme.tabsBg
667 )}
668 >
669 <TabsTrigger
670 value="overview"
671 className={currentTheme.accentColor}
672 >
673 Overview
674 </TabsTrigger>
675 <TabsTrigger
676 value="repositories"
677 className={currentTheme.accentColor}
678 >
679 Repositories
680 </TabsTrigger>
681 </TabsList>
682
683 <TabsContent
684 value="overview"
685 className={cn("mt-3 space-y-3", currentTheme.textMuted)}
686 >
687 {profile.languages && profile.languages.length > 0 && (
688 <div>
689 <h3
690 className={cn(
691 "mb-2 text-xs font-medium uppercase",
692 currentTheme.textMuted
693 )}
694 >
695 Top Languages
696 </h3>
697 <div
698 className={cn(
699 "mt-2 w-full space-y-2",
700 currentTheme.accentColor
701 )}
702 >
703 {profile.languages.slice(0, 3).map((item, index) => (
704 <div key={index} className="space-y-1">
705 <div className="flex items-center justify-between text-xs">
706 <div className="flex items-center gap-1.5">
707 <div
708 className="h-2 w-2 rounded-full"
709 style={{ backgroundColor: item.color }}
710 />
711 <span className="font-medium">
712 {item.name}
713 </span>
714 </div>
715 <span className="font-medium">
716 {item.percentage}%
717 </span>
718 </div>
719 <div className="relative h-2 w-full overflow-hidden rounded-full bg-gray-100/30">
720 <div
721 className="absolute h-full rounded-full"
722 style={{
723 backgroundColor: item.color,
724 width: `${item.percentage}%`,
725 }}
726 />
727 </div>
728 </div>
729 ))}
730 </div>
731 </div>
732 )}
733
734 {profile.contributionData &&
735 profile.contributionData.length > 0 && (
736 <div>
737 <h3
738 className={cn(
739 "mb-2 text-xs font-medium uppercase text-muted-foreground",
740 currentTheme.textMuted
741 )}
742 >
743 Activity
744 </h3>
745 <div
746 className={cn(
747 "h-[40px] w-full overflow-hidden rounded-md p-2",
748 currentTheme.textMuted
749 )}
750 >
751 <svg
752 className={cn(
753 "h-full w-full",
754 currentTheme.textMuted
755 )}
756 viewBox="0 0 100 30"
757 preserveAspectRatio="none"
758 >
759 <polyline
760 points={profile.contributionData
761 .map(
762 (value, index) =>
763 `${
764 index *
765 (100 /
766 (profile.contributionData?.length ||
767 1))
768 },${30 - value * 30}`
769 )
770 .join(" ")}
771 fill="none"
772 stroke="currentColor"
773 strokeWidth="1.5"
774 strokeLinecap="round"
775 strokeLinejoin="round"
776 className={currentTheme.graphColor}
777 />
778 <path
779 d={`M0,30 ${profile.contributionData
780 .map(
781 (value, index) =>
782 `L${
783 index *
784 (100 /
785 (profile.contributionData?.length ||
786 1))
787 },${30 - value * 30}`
788 )
789 .join(" ")} L100,30 Z`}
790 fill="currentColor"
791 className={currentTheme.graphBgColor}
792 />
793 </svg>
794 </div>
795 </div>
796 )}
797 </TabsContent>
798 <TabsContent value="repositories" className="mt-3">
799 {profile.pinnedRepos && profile.pinnedRepos.length > 0 ? (
800 <div className={cn("space-y-3", currentTheme.textMuted)}>
801 {profile.pinnedRepos.slice(0, 2).map((repo) => (
802 <div key={repo.name}>
803 <Card
804 className={cn(
805 "transition-all duration-300 hover:shadow-sm hover:translate-y-[-2px]",
806 currentTheme.cardBg,
807 currentTheme.cardBorder
808 )}
809 >
810 <CardContent className="p-2">
811 <div
812 className={cn(
813 "flex items-start justify-between",
814 currentTheme.textMuted
815 )}
816 >
817 <div className="space-y-1">
818 <div className="flex items-center gap-1">
819 <BookOpen
820 className={cn(
821 "h-3.5 w-3.5 text-muted-foreground",
822 currentTheme.textMuted
823 )}
824 />
825 <Link
826 href={`https://github.com/${profile.login}/${repo.name}`}
827 target="_blank"
828 className={cn(
829 "text-sm font-medium hover:underline",
830 currentTheme.accentColor
831 )}
832 >
833 {repo.name}
834 </Link>
835 </div>
836 {repo.description && (
837 <p
838 className={cn(
839 "line-clamp-1 text-xs text-muted-foreground",
840 currentTheme.textMuted
841 )}
842 >
843 {repo.description}
844 </p>
845 )}
846 </div>
847 <Button
848 variant="ghost"
849 size="icon"
850 className="h-6 w-6"
851 asChild
852 >
853 <Link
854 href={`https://github.com/${profile.login}/${repo.name}`}
855 target="_blank"
856 >
857 <ExternalLink className="h-3 w-3" />
858 </Link>
859 </Button>
860 </div>
861 <div
862 className={cn(
863 "mt-2 flex items-center justify-between",
864 currentTheme.textMuted
865 )}
866 >
867 {repo.language && (
868 <div className="flex items-center gap-1.5">
869 <div
870 className="h-2 w-2 rounded-full"
871 style={{
872 backgroundColor: repo.languageColor,
873 }}
874 />
875 <span className="text-xs">
876 {repo.language}
877 </span>
878 </div>
879 )}
880 <div
881 className={cn(
882 "flex items-center gap-3 text-xs text-muted-foreground",
883 currentTheme.textMuted
884 )}
885 >
886 <div
887 className={cn(
888 "flex items-center gap-1",
889 currentTheme.textMuted
890 )}
891 >
892 <Star
893 className={cn(
894 "h-3 w-3",
895 currentTheme.textMuted
896 )}
897 />
898 <span
899 className={cn(
900 "text-xs",
901 currentTheme.textMuted
902 )}
903 >
904 {repo.stars}
905 </span>
906 </div>
907 <div
908 className={cn(
909 "flex items-center gap-1",
910 currentTheme.textMuted
911 )}
912 >
913 <GitFork
914 className={cn(
915 "h-3 w-3",
916 currentTheme.textMuted
917 )}
918 />
919 <span
920 className={cn(
921 "text-xs",
922 currentTheme.textMuted
923 )}
924 >
925 {repo.forks}
926 </span>
927 </div>
928 </div>
929 </div>
930 </CardContent>
931 </Card>
932 </div>
933 ))}
934 {profile.pinnedRepos.length > 2 && (
935 <div className="text-center">
936 <Button
937 variant="ghost"
938 size="sm"
939 className={cn(
940 "text-xs",
941 currentTheme.accentColor
942 )}
943 asChild
944 >
945 <Link
946 href={`https://github.com/${profile.login}?tab=repositories`}
947 target="_blank"
948 >
949 View all repositories
950 </Link>
951 </Button>
952 </div>
953 )}
954 </div>
955 ) : (
956 <div
957 className={cn(
958 "flex h-20 items-center justify-center text-sm text-muted-foreground",
959 currentTheme.textMuted
960 )}
961 >
962 No repositories available
963 </div>
964 )}
965 </TabsContent>
966 </Tabs>
967 </div>
968 </CardContent>
969 <CardFooter
970 className={cn(
971 "flex items-center justify-between border-t px-3 text-xs",
972 currentTheme.id === "github-light" && "border-gray-100",
973 currentTheme.id === "github-dark" && "border-gray-800",
974 currentTheme.id === "ocean" && "border-blue-100",
975 currentTheme.id === "forest" && "border-green-100",
976 currentTheme.id === "sunset" && "border-orange-100"
977 )}
978 >
979 <span
980 className={cn("text-muted-foreground", currentTheme.textMuted)}
981 >
982 {getYearsOnGitHub(profile.createdAt)} years on GitHub
983 </span>
984 <TooltipProvider>
985 <Tooltip>
986 <TooltipTrigger asChild>
987 <Button
988 variant="ghost"
989 size="icon"
990 className="h-6 w-6"
991 asChild
992 >
993 <Link
994 href={`https://github.com/${profile.login}`}
995 target="_blank"
996 >
997 <Github
998 className={cn("h-3.5 w-3.5", currentTheme.textMuted)}
999 />
1000 </Link>
1001 </Button>
1002 </TooltipTrigger>
1003 <TooltipContent>
1004 <p>View full profile</p>
1005 </TooltipContent>
1006 </Tooltip>
1007 </TooltipProvider>
1008 </CardFooter>
1009 </Card>
1010 </div>
1011 )}
1012 </>
1013 );
1014}
1015
4
Final Steps
Update Import Paths
Make sure to update the import paths in the component code to match your project structure. For example, change @/components/ui/button
to match your UI components location.
Props
Name | Type | Default | Description |
---|---|---|---|
username | string | undefined | GitHub username to fetch profile data for. Required when not using manualMode. |
githubToken | string | undefined | Optional GitHub API token for increased rate limits (unauthenticated: 60/hr, authenticated: 5,000/hr). Store securely using environment variables. |
manualMode | boolean | false | When true, uses provided profileData instead of fetching from GitHub API. Useful for avoiding rate limits or displaying custom profile data. |
profileData | ManualProfileData | undefined | Profile data object for manual mode. Required when manualMode is true. Includes fields for username, name, followers, etc. |
themeId | string | github-light | Visual theme for the card. Options: github-light, github-dark, ocean, forest, sunset, nuvyx. Some themes support automatic light/dark mode switching. |
Examples
JO
John Doe
johndoe
Frontend developer passionate about React and UI/UX design
San Francisco, CA
Joined Mar 2015
1,250followers
320following
45repos
Top Languages
JavaScript
45%TypeScript
35%CSS
20%Activity
JA
830followers
145following
32repos
Top Languages
TypeScript
50%Go
30%Python
20%Activity
DE
Developer Smith
devsmith
Full-stack developer specializing in React & Node.js
Seattle, WA
Joined Jan 2018
620followers
210following
29repos
Top Languages
JavaScript
40%TypeScript
35%Python
25%Activity
TE
1,850followers
105following
38repos
Top Languages
Java
45%Kotlin
30%Rust
25%Activity
CO
Code Artist
codeartist
UI/UX designer who codes with React & CSS wizardry
Portland, OR
Joined Mar 2019
740followers
280following
25repos
Top Languages
TypeScript
40%CSS
35%HTML
25%Activity
NI
Night Coder
nightcoder
DevOps engineer passionate about automation and cloud infrastructure
Amsterdam, Netherlands
Joined Dec 2017
920followers
125following
31repos
Top Languages
Python
45%Go
30%Shell
25%