Introducing Nuvyx UI v1.0.0

GitHub Repo Card

Beautiful GitHub repository cards with customizable themes, activity graphs, and real-time data fetching.

shadcn's avatar

shadcn-ui

Public

Beautifully designed components built with Radix UI and Tailwind CSS.
ActivityUpdated 24 days ago
42,000
3,600
1,600
310
TypeScript

ui

components

+2 more

Installation Guide

1

Install Dependencies

Utility Functions

npm install clsx tailwind-merge
2

Setup Configuration

Create file: /lib/utils.ts

Create a utils.ts file with the cn utility function

/lib/utils.ts
1import { 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 Repo Card.tsx
1"use client";
2import { useState, useEffect, useCallback, useMemo } from "react";
3import Link from "next/link";
4import { BookOpen, Code, Eye, Github, History, Star, GitFork, AlertCircle, Check } from "lucide-react";
5import { cn } from "@/lib/utils";
6import Image from "next/image";
7
8const LANGUAGE_COLORS = {
9  JavaScript: "#f1e05a",
10  TypeScript: "#3178c6",
11  Python: "#3572A5",
12  Java: "#b07219",
13  Go: "#00ADD8",
14  Rust: "#dea584",
15  C: "#555555",
16  "C++": "#f34b7d",
17  "C#": "#178600",
18  PHP: "#4F5D95",
19  Ruby: "#701516",
20  Swift: "#F05138",
21  Kotlin: "#A97BFF",
22  Dart: "#00B4AB",
23  HTML: "#e34c26",
24  CSS: "#563d7c",
25  Shell: "#89e051",
26};
27
28export type ThemeOption = {
29  id: string;
30  name: string;
31  description: string;
32  cardBg: string;
33  cardBorder: string;
34  cardHoverShadow: string;
35  accentColor: string;
36  accentColorLight: string;
37  graphColor: string;
38  graphBgColor: string;
39  badgeBg: string;
40  badgeText: string;
41  textMuted: string;
42  textNormal: string;
43};
44
45export const themes: ThemeOption[] = [
46  {
47    id: "modern-dark",
48    name: "Modern Dark",
49    description: "Sleek dark theme with blue accents",
50    cardBg: "bg-gradient-to-br from-slate-900 to-slate-800",
51    cardBorder: "border border-slate-700/50",
52    cardHoverShadow: "hover:shadow-lg hover:shadow-blue-500/10 transition-all duration-300",
53    accentColor: "text-blue-400",
54    accentColorLight: "text-blue-400/10",
55    graphColor: "text-blue-400",
56    graphBgColor: "text-blue-400/10",
57    badgeBg: "bg-slate-800/80 backdrop-blur-sm",
58    badgeText: "text-blue-300",
59    textMuted: "text-slate-400",
60    textNormal: "text-slate-200",
61  },
62  {
63    id: "modern-light",
64    name: "Modern Light",
65    description: "Clean light theme with subtle shadows",
66    cardBg: "bg-gradient-to-br from-white to-slate-50",
67    cardBorder: "border border-slate-200",
68    cardHoverShadow: "hover:shadow-lg hover:shadow-slate-200/50 transition-all duration-300",
69    accentColor: "text-indigo-600",
70    accentColorLight: "text-indigo-600/10",
71    graphColor: "text-indigo-600",
72    graphBgColor: "text-indigo-600/10",
73    badgeBg: "bg-indigo-50",
74    badgeText: "text-indigo-700",
75    textMuted: "text-slate-600",
76    textNormal: "text-slate-900",
77  },
78  {
79    id: "retro",
80    name: "Neo Brutalist",
81    description: "Bold contrasting theme with box shadows",
82    cardBg: "bg-amber-50",
83    cardBorder: "border-2 border-black",
84    cardHoverShadow: "hover:shadow-[6px_6px_0px_0px_rgba(0,0,0,1)] transition-all duration-300",
85    accentColor: "text-rose-600",
86    accentColorLight: "text-rose-600/10",
87    graphColor: "text-rose-600",
88    graphBgColor: "text-rose-600/10",
89    badgeBg: "bg-white border border-black",
90    badgeText: "text-black font-bold",
91    textMuted: "text-slate-700",
92    textNormal: "text-black",
93  },
94  {
95    id: "midnight",
96    name: "Midnight",
97    description: "Deep dark theme with vibrant purples",
98    cardBg: "bg-gradient-to-br from-slate-950 to-slate-900",
99    cardBorder: "border border-purple-900/30",
100    cardHoverShadow: "hover:shadow-lg hover:shadow-purple-500/20 transition-all duration-300",
101    accentColor: "text-purple-400",
102    accentColorLight: "text-purple-400/10",
103    graphColor: "text-purple-400",
104    graphBgColor: "text-purple-400/10",
105    badgeBg: "bg-purple-950/80 backdrop-blur-sm",
106    badgeText: "text-purple-300",
107    textMuted: "text-slate-400",
108    textNormal: "text-slate-200",
109  }
110];
111
112export type RepoData = {
113  name: string;
114  fullName: string;
115  description?: string;
116  owner: {
117    login: string;
118    avatarUrl: string;
119  };
120  stars: number;
121  forks: number;
122  watchers: number;
123  issues: number;
124  language?: string;
125  languageColor?: string;
126  updatedAt: string;
127  topics: string[];
128  activityData?: number[];
129  isPrivate: boolean;
130};
131const CACHE_TTL = 15 * 60 * 1000;
132
133interface GitHubRepoCardProps {
134  repoOwner?: string;
135  repoName?: string;
136  githubToken?: string;
137  manualMode?: boolean;
138  repoData?: RepoData;
139  themeId?: string;
140}
141
142const getLanguageColor = (language: string) => {
143  return LANGUAGE_COLORS[language as keyof typeof LANGUAGE_COLORS] || "#858585";
144};
145
146const formatRelativeTime = (dateString: string) => {
147  const date = new Date(dateString);
148  const now = new Date();
149  const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
150  const intervals = {
151    year: 31536000,
152    month: 2592000,
153    day: 86400,
154    hour: 3600,
155    minute: 60,
156    second: 1
157  };
158  for (const [unit, seconds] of Object.entries(intervals)) {
159    const count = Math.floor(diffInSeconds / seconds);
160    if (count >= 1) {
161      return `${count} ${unit}${count !== 1 ? 's' : ''} ago`;
162    }
163  }
164  return 'just now';
165};
166
167const getCacheKey = (repoOwner: string, repoName: string) => {
168  return `github_repo_${repoOwner}_${repoName}`;
169};
170
171export function GitHubRepoCard({
172  repoOwner,
173  repoName,
174  githubToken,
175  manualMode = false,
176  repoData,
177  themeId = "modern-light",
178}: GitHubRepoCardProps) {
179  const [copied, setCopied] = useState(false);
180  const [loading, setLoading] = useState(!manualMode);
181  const [error, setError] = useState<string | null>(null);
182  const [repo, setRepo] = useState<RepoData | null>(manualMode ? repoData || null : null);
183  const [rateLimit, setRateLimit] = useState<{ remaining: number; limit: number } | null>(null);
184
185  const currentTheme = useMemo(() =>
186    themes.find((theme) => theme.id === themeId) || themes[0],
187    [themeId]);
188
189  const repoUrl = useMemo(() => {
190    if (!repo) return '';
191    return `https://github.com/${repo.fullName}`;
192  }, [repo]);
193
194  const cloneCommand = useMemo(() => {
195    if (!repo) return '';
196    return `git clone https://github.com/${repo.fullName}.git`;
197  }, [repo]);
198
199  const getCachedData = useCallback(() => {
200    if (!repoOwner || !repoName || typeof window === 'undefined') return null;
201
202    try {
203      const cacheKey = getCacheKey(repoOwner, repoName);
204      const cachedData = localStorage.getItem(cacheKey);
205
206      if (cachedData) {
207        const { data, timestamp } = JSON.parse(cachedData);
208
209        if (Date.now() - timestamp < CACHE_TTL) {
210          return data;
211        } else {
212          localStorage.removeItem(cacheKey);
213        }
214      }
215    } catch (err) {
216      console.error('Error reading from cache:', err);
217    }
218
219    return null;
220  }, [repoOwner, repoName]);
221
222  const setCachedData = useCallback((data: RepoData) => {
223    if (!repoOwner || !repoName || typeof window === 'undefined') return;
224
225    try {
226      const cacheKey = getCacheKey(repoOwner, repoName);
227      const cacheData = JSON.stringify({
228        data,
229        timestamp: Date.now(),
230      });
231
232      localStorage.setItem(cacheKey, cacheData);
233    } catch (err) {
234      console.error('Error writing to cache:', err);
235    }
236  }, [repoOwner, repoName]);
237
238  const fetchRepoData = useCallback(async () => {
239    if (!repoOwner || !repoName) return;
240
241    const cachedData = getCachedData();
242    if (cachedData) {
243      setRepo(cachedData);
244      setLoading(false);
245      return;
246    }
247    setLoading(true);
248    setError(null);
249    try {
250      const headers: HeadersInit = {};
251      if (githubToken) {
252        headers.Authorization = `token ${githubToken}`;
253      }
254      const repoResponse = await fetch(
255        `https://api.github.com/repos/${repoOwner}/${repoName}`,
256        { headers }
257      );
258      const rateLimitRemaining = repoResponse.headers.get("x-ratelimit-remaining");
259      const rateLimitLimit = repoResponse.headers.get("x-ratelimit-limit");
260
261      if (rateLimitRemaining && rateLimitLimit) {
262        setRateLimit({
263          remaining: parseInt(rateLimitRemaining, 10),
264          limit: parseInt(rateLimitLimit, 10),
265        });
266      }
267      if (!repoResponse.ok) {
268        if (repoResponse.status === 403 && rateLimitRemaining === "0") {
269          throw new Error("GitHub API rate limit exceeded. Please provide a GitHub token.");
270        } else {
271          throw new Error(`Failed to fetch repository data: ${repoResponse.status}`);
272        }
273      }
274      const repoData = await repoResponse.json();
275      const commitsResponse = await fetch(
276        `https://api.github.com/repos/${repoOwner}/${repoName}/stats/commit_activity`,
277        { headers }
278      );
279      let activityData: number[] = [];
280      if (commitsResponse.ok) {
281        const commitsData = await commitsResponse.json();
282        const recentCommits = commitsData.slice(-12).map((week: { total: number }) => week.total);
283        const maxCommit = Math.max(...recentCommits, 1);
284        activityData = recentCommits.map((count: number) => count / maxCommit);
285      }
286      const transformedRepo: RepoData = {
287        name: repoData.name,
288        fullName: repoData.full_name,
289        description: repoData.description || "",
290        owner: {
291          login: repoData.owner.login,
292          avatarUrl: repoData.owner.avatar_url,
293        },
294        stars: repoData.stargazers_count,
295        forks: repoData.forks_count,
296        watchers: repoData.watchers_count,
297        issues: repoData.open_issues_count,
298        language: repoData.language,
299        languageColor: repoData.language ? getLanguageColor(repoData.language) : undefined,
300        updatedAt: repoData.updated_at,
301        topics: repoData.topics || [],
302        activityData,
303        isPrivate: repoData.private,
304      };
305
306      setRepo(transformedRepo);
307      setCachedData(transformedRepo);
308      setLoading(false);
309    } catch (err) {
310      setError(err instanceof Error ? err.message : "An unknown error occurred");
311      setLoading(false);
312    }
313  }, [repoOwner, repoName, githubToken, getCachedData, setCachedData]);
314
315  useEffect(() => {
316    if (manualMode && repoData) {
317      setRepo(repoData);
318      setLoading(false);
319      return;
320    }
321
322    if (!manualMode && repoOwner && repoName) {
323      fetchRepoData();
324    }
325  }, [manualMode, repoData, repoOwner, repoName, fetchRepoData]);
326
327  const copyToClipboard = () => {
328    if (repo) {
329      navigator.clipboard
330        .writeText(cloneCommand)
331        .then(() => {
332          setCopied(true);
333          setTimeout(() => setCopied(false), 2000);
334        })
335        .catch(console.error);
336    }
337  };
338
339  const renderLoadingState = () => (
340    <div
341      className={cn(
342        "w-full max-w-full overflow-hidden transition-all duration-300 p-4 sm:p-6 rounded-md",
343        currentTheme.cardBg,
344        currentTheme.cardBorder,
345        currentTheme.cardHoverShadow
346      )}
347      aria-busy="true"
348      aria-live="polite"
349    >
350      <div className="pb-2">
351        <div className="mb-2">
352          <div className="flex items-center gap-2">
353            <div className="h-4 w-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
354            <div className="flex items-center">
355              <div className="h-4 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
356              <div className="mx-1 h-4 w-2 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
357              <div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
358            </div>
359            <div className="ml-auto h-4 w-28 bg-gray-200 dark:bg-gray-700 rounded animate-pulse hidden sm:block" />
360          </div>
361        </div>
362
363        <div className="flex items-center gap-2 mb-3">
364          <div className="h-6 w-6 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse" />
365          <div className="h-5 w-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
366          <div className="ml-auto h-6 w-16 bg-gray-200 dark:bg-gray-700 rounded-lg animate-pulse" />
367        </div>
368
369        <div className="space-y-2 mb-4">
370          <div className="h-4 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
371          <div className="h-4 w-3/4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
372        </div>
373      </div>
374      <div className="pb-2">
375        <div className="mb-4 sm:mb-6 rounded-md border border-gray-200 dark:border-gray-700 shadow-sm">
376          <div className="mb-1 sm:mb-2 flex items-center justify-between p-1.5 sm:p-2">
377            <div className="h-4 w-16 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
378            <div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
379          </div>
380          <div className="h-[40px] sm:h-[60px] w-full rounded-b-lg bg-gray-100 dark:bg-gray-800 p-1 sm:p-2 animate-pulse">
381            <div className="h-full w-full bg-gray-200 dark:bg-gray-700 rounded opacity-30 animate-pulse" />
382          </div>
383        </div>
384        <div className="grid grid-cols-2 xs:grid-cols-4 gap-1 sm:gap-2">
385          {[0, 1, 2, 3].map((index) => (
386            <div key={index} className="flex items-center gap-1">
387              <div className="h-4 w-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
388              <div className="h-4 w-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
389            </div>
390          ))}
391        </div>
392        <div className="mt-3 sm:mt-4">
393          <div className="mb-1.5 sm:mb-2 flex items-center gap-2">
394            <div className="flex items-center gap-1.5">
395              <div className="h-3 w-3 rounded-full bg-gray-200 dark:bg-gray-700 animate-pulse" />
396              <div className="h-4 w-16 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
397            </div>
398          </div>
399
400          <div className="flex flex-col xs:flex-row items-start xs:items-center justify-between gap-2">
401            <div className="flex flex-wrap gap-1.5">
402              {[0, 1, 2].map((index) => (
403                <div
404                  key={index}
405                  className="h-6 w-16 bg-gray-200 dark:bg-gray-700 rounded-md animate-pulse"
406                />
407              ))}
408            </div>
409            <div className="w-full xs:w-auto flex justify-end">
410              <div className="h-6 w-16 bg-gray-200 dark:bg-gray-700 rounded-md animate-pulse" />
411            </div>
412          </div>
413        </div>
414      </div>
415    </div>
416  );
417  const renderErrorState = () => (
418    <div
419      className={cn(
420        "w-full max-w-full overflow-hidden transition-all duration-300 p-4 sm:p-6 rounded-md",
421        currentTheme.cardBg,
422        currentTheme.cardBorder,
423        currentTheme.cardHoverShadow
424      )}
425      aria-live="assertive"
426    >
427      <div className="flex flex-col items-center justify-center py-6 sm:py-8">
428        <div className={cn(
429          "flex items-center justify-center w-16 h-16 mb-4 rounded-full",
430          currentTheme.badgeBg
431        )}>
432          <AlertCircle className={cn("h-8 w-8", currentTheme.accentColor)} aria-hidden="true" />
433        </div>
434
435        <h3 className={cn("text-lg font-semibold mb-2 text-center", currentTheme.textNormal)}>
436          Repository Not Found
437        </h3>
438
439        <p className={cn("text-sm text-center max-w-xs mb-4", currentTheme.textMuted)}>
440          {error?.includes("404")
441            ? "This repository doesn't exist or you might not have access to it."
442            : error?.includes("rate limit")
443              ? "GitHub API rate limit exceeded. Please try again later or use a GitHub token."
444              : error}
445        </p>
446
447        <div className="flex flex-wrap gap-3 justify-center">
448          {error?.includes("rate limit") && (
449            <button
450              onClick={() => {
451                if (typeof window !== 'undefined') {
452                  try {
453                    const cacheKey = getCacheKey(repoOwner || '', repoName || '');
454                    localStorage.removeItem(cacheKey);
455                  } catch (e) {
456                    console.error('Error clearing cache:', e);
457                  }
458                }
459                fetchRepoData();
460              }}
461              className={cn(
462                "flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-md",
463                currentTheme.badgeBg,
464                currentTheme.badgeText,
465                "hover:opacity-90 transition-all duration-200"
466              )}
467            >
468              <svg
469                className="h-4 w-4"
470                fill="none"
471                viewBox="0 0 24 24"
472                stroke="currentColor"
473              >
474                <path
475                  strokeLinecap="round"
476                  strokeLinejoin="round"
477                  strokeWidth={2}
478                  d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
479                />
480              </svg>
481              Try Again
482            </button>
483          )}
484
485          <Link
486            href={`https://github.com/${repoOwner || ''}/${repoName || ''}`}
487            target="_blank"
488            rel="noopener noreferrer"
489            className={cn(
490              "flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-md",
491              currentTheme.badgeBg,
492              currentTheme.badgeText,
493              "hover:opacity-90 transition-all duration-200"
494            )}
495          >
496            <Github className="h-4 w-4" aria-hidden="true" />
497            Visit GitHub
498          </Link>
499        </div>
500      </div>
501    </div>
502  );
503
504  if (loading) return renderLoadingState();
505  if (error) return renderErrorState();
506  if (!repo) return null;
507
508  return (
509    <div
510      className={cn(
511        "w-full max-w-full overflow-hidden transition-all duration-300 p-4 sm:p-6 rounded-md",
512        currentTheme.cardBg,
513        currentTheme.cardBorder,
514        currentTheme.cardHoverShadow
515      )}
516      role="article"
517      aria-label={`GitHub repository: ${repo?.name}`}
518    >
519      <div className="pb-2">
520        <div>
521          <div className="mb-2">
522            <div className="flex flex-wrap items-center gap-1 sm:gap-2">
523              <Github className={cn("h-3 w-3 sm:h-4 sm:w-4", currentTheme.accentColor)} aria-hidden="true" />
524              <div className="flex items-center text-xs sm:text-sm">
525                <Link
526                  href={`https://github.com/${repo.owner.login}`}
527                  className={cn("hover:underline font-medium", currentTheme.textMuted)}
528                  aria-label={`View ${repo.owner.login}'s GitHub profile`}
529                >
530                  {repo.owner.login}
531                </Link>
532                <span className={cn("mx-1", currentTheme.textMuted)} aria-hidden="true">/</span>
533                <Link
534                  href={repoUrl}
535                  className={cn("font-medium hover:underline", currentTheme.accentColor)}
536                  aria-label={`View ${repo.name} repository on GitHub`}
537                >
538                  {repo.name}
539                </Link>
540              </div>
541              {rateLimit && (
542                <div className={cn("ml-auto text-xs font-medium hidden sm:flex items-center", currentTheme.textMuted)} aria-label={`GitHub API rate limit: ${rateLimit.remaining} of ${rateLimit.limit} requests remaining`}>
543                  <span>{rateLimit.remaining}/{rateLimit.limit}</span>
544                  <span className="ml-1 text-xs">API requests</span>
545                </div>
546              )}
547            </div>
548          </div>
549        </div>
550        <div className="flex items-center gap-2">
551          <div className="relative inline-flex h-5 w-5 sm:h-6 sm:w-6 items-center justify-center overflow-hidden rounded-full">
552            {repo.owner.avatarUrl ? (
553              <Image
554                src={repo.owner.avatarUrl}
555                alt={`${repo.owner.login}'s avatar`}
556                className="h-full w-full object-cover"
557                width={24}
558                height={24}
559                loading="lazy"
560              />
561            ) : (
562              <div className="flex h-full w-full items-center justify-center bg-gray-200 text-xs font-medium">
563                {repo.owner.login.substring(0, 2).toUpperCase()}
564              </div>
565            )}
566          </div>
567          <h1 className={cn("text-sm sm:text-lg font-semibold truncate max-w-[150px] sm:max-w-full", currentTheme.textNormal)}>
568            {repo.name}
569          </h1>
570          <h1
571            className={cn(
572              "ml-auto text-[10px] sm:text-xs font-medium px-1.5 sm:px-2 py-0.5 sm:py-1 rounded-lg border",
573              currentTheme.badgeBg,
574              currentTheme.badgeText
575            )}
576          >
577            {repo.isPrivate ? "Private" : "Public"}
578          </h1>
579        </div>
580        <div className={cn(
581          "line-clamp-2 text-xs sm:text-sm font-normal leading-snug tracking-wide my-2 sm:my-3",
582          currentTheme.textMuted
583        )}>
584          {repo.description || "No description provided"}
585        </div>
586      </div>
587      <div className="pb-2">
588        <div className="mb-4 sm:mb-6 rounded-md border border-border shadow-sm" aria-label="Repository activity graph">
589          <div className="mb-1 sm:mb-2 flex items-center justify-between text-xs p-1.5 sm:p-2">
590            <span className={cn("flex items-center gap-0.5 sm:gap-1 font-semibold text-[10px] sm:text-xs", currentTheme.textMuted)}>
591              <History className="h-3 w-3 sm:h-4 sm:w-4" aria-hidden="true" />
592              Activity
593            </span>
594            <span className={cn("text-[10px] sm:text-xs font-medium", currentTheme.textMuted)}>
595              Updated {formatRelativeTime(repo.updatedAt)}
596            </span>
597          </div>
598          <div className="h-[40px] sm:h-[60px] w-full overflow-hidden rounded-b-lg bg-muted/50 dark:bg-gray-800/50 p-1 sm:p-2" role="img" aria-label="Repository commit activity visualization">
599            {repo.activityData && repo.activityData.length > 0 ? (
600              <svg
601                className="h-full w-full"
602                viewBox="0 0 100 20"
603                preserveAspectRatio="none"
604                aria-hidden="true"
605              >
606                <line x1="0" y1="5" x2="100" y2="5" stroke="currentColor" strokeWidth="0.2" strokeDasharray="1" className="text-muted/30" />
607                <line x1="0" y1="10" x2="100" y2="10" stroke="currentColor" strokeWidth="0.2" strokeDasharray="1" className="text-muted/30" />
608                <line x1="0" y1="15" x2="100" y2="15" stroke="currentColor" strokeWidth="0.2" strokeDasharray="1" className="text-muted/30" />
609                {repo.activityData.map((value, index) => (
610                  <circle
611                    key={index}
612                    cx={`${index * (100 / (repo.activityData?.length || 1))}`}
613                    cy={`${20 - value * 20}`}
614                    r="0.8"
615                    className={currentTheme.graphColor}
616                  />
617                ))}
618                <polyline
619                  points={repo.activityData
620                    .map((value, index) =>
621                      `${index * (100 / (repo.activityData?.length || 1))},${20 - value * 20}`
622                    )
623                    .join(" ")}
624                  fill="none"
625                  stroke="currentColor"
626                  strokeWidth="1.5"
627                  strokeLinecap="round"
628                  strokeLinejoin="round"
629                  className={currentTheme.graphColor}
630                />
631                <path
632                  d={`M0,20 ${repo.activityData
633                    .map((value, index) =>
634                      `L${index * (100 / (repo.activityData?.length || 1))},${20 - value * 20}`
635                    )
636                    .join(" ")} L100,20 Z`}
637                  fill="currentColor"
638                  className={currentTheme.graphBgColor}
639                />
640              </svg>
641            ) : (
642              <div className={cn("flex h-full items-center justify-center rounded-md bg-background/50 text-xs font-medium", currentTheme.textMuted)} aria-label="No activity data available">
643                No activity data available
644              </div>
645            )}
646          </div>
647        </div>
648        <div className="grid grid-cols-2 xs:grid-cols-4 gap-1 sm:gap-2 text-xs sm:text-sm" role="group" aria-label="Repository statistics">
649          <div>
650            <div className={cn("flex items-center gap-0.5 sm:gap-1 font-normal", currentTheme.textMuted)} aria-label={`${repo.stars.toLocaleString()} stars`}>
651              <Star className="h-3 w-3 sm:h-4 sm:w-4" aria-hidden="true" />
652              <span>{repo.stars.toLocaleString()}</span>
653            </div>
654          </div>
655          <div>
656            <div className={cn("flex items-center gap-0.5 sm:gap-1 font-normal", currentTheme.textMuted)} aria-label={`${repo.forks.toLocaleString()} forks`}>
657              <GitFork className="h-3 w-3 sm:h-4 sm:w-4" aria-hidden="true" />
658              <span>{repo.forks.toLocaleString()}</span>
659            </div>
660          </div>
661          <div>
662            <div className={cn("flex items-center gap-0.5 sm:gap-1 font-normal", currentTheme.textMuted)} aria-label={`${repo.watchers.toLocaleString()} watchers`}>
663              <Eye className="h-3 w-3 sm:h-4 sm:w-4" aria-hidden="true" />
664              <span>{repo.watchers.toLocaleString()}</span>
665            </div>
666          </div>
667          <div>
668            <div className={cn("flex items-center gap-0.5 sm:gap-1 font-normal", currentTheme.textMuted)} aria-label={`${repo.issues.toLocaleString()} issues`}>
669              <BookOpen className="h-3 w-3 sm:h-4 sm:w-4" aria-hidden="true" />
670              <span>{repo.issues.toLocaleString()}</span>
671            </div>
672          </div>
673        </div>
674        <div className="mt-3 sm:mt-4">
675          {repo.language && (
676            <div className="mb-1.5 sm:mb-2 flex items-center gap-2" aria-label={`Primary language: ${repo.language}`}>
677              <div className="flex items-center gap-1 sm:gap-1.5">
678                <div
679                  className="h-2 w-2 sm:h-3 sm:w-3 rounded-full"
680                  style={{ backgroundColor: repo.languageColor }}
681                  aria-hidden="true"
682                />
683                <span className={cn("text-xs sm:text-sm font-medium", currentTheme.textNormal)}>
684                  {repo.language}
685                </span>
686              </div>
687            </div>
688          )}
689          <div className="flex flex-col xs:flex-row items-start xs:items-center justify-between gap-2">
690            <div className="w-full xs:w-auto">
691              {repo.topics && repo.topics.length > 0 && (
692                <div className="flex flex-wrap gap-1 sm:gap-1.5" role="group" aria-label="Repository topics">
693                  {repo.topics.slice(0, 2).map((topic) => (
694                    <h1
695                      key={topic}
696                      className={cn(
697                        "text-[10px] sm:text-xs font-medium px-1.5 sm:px-2 py-0.5 sm:py-1 rounded-md border leading-snug tracking-wide",
698                        currentTheme.badgeBg,
699                        currentTheme.badgeText
700                      )}
701                    >
702                      {topic}
703                    </h1>
704                  ))}
705                  {repo.topics.length > 2 && (
706                    <h1
707                      className={cn("text-[10px] sm:text-xs font-medium px-1.5 sm:px-2 py-0.5 sm:py-1 rounded-md border leading-snug tracking-wide", currentTheme.badgeBg, currentTheme.badgeText)}
708                    >
709                      +{repo.topics.length - 2} more
710                    </h1>
711                  )}
712                </div>
713              )}
714            </div>
715            <div className="w-full xs:w-auto flex justify-end">
716              <button
717                className={`flex items-center gap-1 border border-${currentTheme.accentColor.split(' ')[0]} text-[15px] sm:text-xs font-medium px-1.5 sm:px-2 py-0.5 sm:py-1 rounded-md transition-colors duration-200 ${currentTheme.accentColor} hover:bg-slate-100/20 dark:hover:bg-slate-800/20 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 ${currentTheme.textNormal}`}
718                onClick={copyToClipboard}
719                type="button"
720                aria-label={copied ? "Clone command copied to clipboard" : "Copy clone command to clipboard"}
721              >
722                {copied ? (
723                  <>
724                    <Check className="h-3 w-3 sm:h-3.5 sm:w-3.5" aria-hidden="true" />
725                    <span>Copied</span>
726                  </>
727                ) : (
728                  <>
729                    <Code className="h-3 w-3 sm:h-3.5 sm:w-3.5" aria-hidden="true" />
730                    <span>Clone</span>
731                  </>
732                )}
733              </button>
734            </div>
735          </div>
736        </div>
737      </div>
738    </div>
739  );
740}
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.

Examples

microsoft's avatar

vscode

Public

Visual Studio Code is a code editor redefined and optimized for building and debugging modern web and cloud applications.
ActivityUpdated 26 days ago
145,000
25,600
3,100
7,800
TypeScript

editor

ide

+2 more

tailwindlabs's avatar

tailwindcss

Public

A utility-first CSS framework for rapid UI development.
ActivityUpdated 1 month ago
68,000
3,500
1,800
95
JavaScript

css

framework

+2 more

Props

NameTypeDefaultDescription
repoOwnerstringundefinedGitHub username or organization name that owns the repository. Required when not using manualMode.
repoNamestringundefinedName of the GitHub repository. Required when not using manualMode.
githubTokenstringundefinedOptional GitHub API token for increased rate limits (unauthenticated: 60/hr, authenticated: 5,000/hr). Store securely using environment variables.
manualModebooleanfalseWhen true, uses provided repoData instead of fetching from GitHub API. Useful for avoiding rate limits or displaying custom repository data.
repoDataManualRepoDataundefinedRepository data object for manual mode. Required when manualMode is true. Includes fields for repository name, stars, forks, language, etc.
themeIdstringmodern-lightVisual theme for the card. Options: modern-dark, modern-light, retro, midnight. Some themes support automatic light/dark mode switching.