Skip to content

Commit 4a668fd

Browse files
committed
feat: add last updated time for github projects
1 parent 291d20b commit 4a668fd

File tree

2 files changed

+105
-35
lines changed

2 files changed

+105
-35
lines changed

src/components/ui/github-stats.tsx

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState, useEffect } from 'react';
2-
import { fetchGitHubRepoStats, extractGitHubInfo, formatNumber, type GitHubRepoStats } from '@/lib/utils';
2+
import { fetchGitHubRepoStats, extractGitHubInfo, formatNumber, formatRelativeTime, type GitHubRepoStats } from '@/lib/utils';
33

44
interface GitHubStatsProps {
55
url: string;
@@ -81,32 +81,59 @@ export function GitHubStats({ url, authorName, authorLink }: GitHubStatsProps) {
8181
</div>
8282
)}
8383

84-
{/* Stats (stars/forks) - only show if not fetch failed */}
84+
{/* Stats (stars/forks/last updated) - only show if not fetch failed */}
8585
{!fetchFailed && (
86-
<div className="flex items-center space-x-3 text-sm text-muted-foreground animate-fade-in" onClick={e => e.stopPropagation()}>
87-
{loading ? (
88-
<div className="flex items-center space-x-3">
89-
<div className="w-16 h-5 bg-muted animate-pulse rounded"></div>
90-
<div className="w-16 h-5 bg-muted animate-pulse rounded"></div>
91-
</div>
92-
) : (
93-
<>
94-
<div className="flex items-center space-x-1" title={`${stats?.stars} stars`}>
95-
<svg
96-
xmlns="http://www.w3.org/2000/svg"
97-
viewBox="0 0 24 24"
98-
fill="none"
99-
stroke="currentColor"
100-
strokeWidth="2"
101-
strokeLinecap="round"
102-
strokeLinejoin="round"
103-
className="h-4 w-4"
104-
>
105-
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
106-
</svg>
107-
<span>{formatNumber(stats?.stars || 0)}</span>
86+
<div className="flex flex-col items-end space-y-1">
87+
<div className="flex items-center space-x-3 text-sm text-muted-foreground animate-fade-in" onClick={e => e.stopPropagation()}>
88+
{loading ? (
89+
<div className="flex items-center space-x-3">
90+
<div className="w-16 h-5 bg-muted animate-pulse rounded"></div>
91+
<div className="w-16 h-5 bg-muted animate-pulse rounded"></div>
10892
</div>
109-
<div className="flex items-center space-x-1" title={`${stats?.forks} forks`}>
93+
) : (
94+
<>
95+
<div className="flex items-center space-x-1" title={`${stats?.stars} stars`}>
96+
<svg
97+
xmlns="http://www.w3.org/2000/svg"
98+
viewBox="0 0 24 24"
99+
fill="none"
100+
stroke="currentColor"
101+
strokeWidth="2"
102+
strokeLinecap="round"
103+
strokeLinejoin="round"
104+
className="h-4 w-4"
105+
>
106+
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
107+
</svg>
108+
<span>{formatNumber(stats?.stars || 0)}</span>
109+
</div>
110+
<div className="flex items-center space-x-1" title={`${stats?.forks} forks`}>
111+
<svg
112+
xmlns="http://www.w3.org/2000/svg"
113+
viewBox="0 0 24 24"
114+
fill="none"
115+
stroke="currentColor"
116+
strokeWidth="2"
117+
strokeLinecap="round"
118+
strokeLinejoin="round"
119+
className="h-4 w-4"
120+
>
121+
<path d="M9 21v-6"></path>
122+
<path d="M15 15v6"></path>
123+
<circle cx="9" cy="6" r="3"></circle>
124+
<circle cx="15" cy="18" r="3"></circle>
125+
<path d="M12 9A6 6 0 0 0 6 15h6a6 6 0 0 0 6-6"></path>
126+
</svg>
127+
<span>{formatNumber(stats?.forks || 0)}</span>
128+
</div>
129+
</>
130+
)}
131+
</div>
132+
133+
{/* Last updated time */}
134+
{!loading && stats?.lastUpdated && (
135+
<div className="text-xs text-muted-foreground animate-fade-in" title={`Last updated: ${new Date(stats.lastUpdated).toLocaleString()}`}>
136+
<div className="flex items-center space-x-1">
110137
<svg
111138
xmlns="http://www.w3.org/2000/svg"
112139
viewBox="0 0 24 24"
@@ -115,17 +142,14 @@ export function GitHubStats({ url, authorName, authorLink }: GitHubStatsProps) {
115142
strokeWidth="2"
116143
strokeLinecap="round"
117144
strokeLinejoin="round"
118-
className="h-4 w-4"
145+
className="h-3.5 w-3.5"
119146
>
120-
<path d="M9 21v-6"></path>
121-
<path d="M15 15v6"></path>
122-
<circle cx="9" cy="6" r="3"></circle>
123-
<circle cx="15" cy="18" r="3"></circle>
124-
<path d="M12 9A6 6 0 0 0 6 15h6a6 6 0 0 0 6-6"></path>
147+
<circle cx="12" cy="12" r="10"></circle>
148+
<polyline points="12 6 12 12 16 14"></polyline>
125149
</svg>
126-
<span>{formatNumber(stats?.forks || 0)}</span>
150+
<span>Updated {formatRelativeTime(stats.lastUpdated)}</span>
127151
</div>
128-
</>
152+
</div>
129153
)}
130154
</div>
131155
)}

src/lib/utils.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { twMerge, ClassNameValue } from "tailwind-merge"
44
export interface GitHubRepoStats {
55
stars: number;
66
forks: number;
7+
lastUpdated: string | null; // Add last updated time
78
loading?: boolean;
89
}
910

@@ -41,7 +42,7 @@ export async function fetchGitHubRepoStats(url: string): Promise<GitHubRepoStats
4142
}
4243

4344
// Mark as loading in cache
44-
githubStatsCache[url] = { stars: 0, forks: 0, loading: true };
45+
githubStatsCache[url] = { stars: 0, forks: 0, lastUpdated: null, loading: true };
4546

4647
try {
4748
const repoInfo = extractGitHubInfo(url);
@@ -66,7 +67,8 @@ export async function fetchGitHubRepoStats(url: string): Promise<GitHubRepoStats
6667

6768
const result = {
6869
stars: data.stargazers_count || 0,
69-
forks: data.forks_count || 0
70+
forks: data.forks_count || 0,
71+
lastUpdated: data.pushed_at || null
7072
};
7173

7274
// Cache the result
@@ -93,6 +95,50 @@ export function formatNumber(num: number): string {
9395
return num.toString();
9496
}
9597

98+
/**
99+
* Format a date string to a relative time format (e.g. "2 months ago")
100+
*/
101+
export function formatRelativeTime(dateString: string | null): string {
102+
if (!dateString) return 'Unknown';
103+
104+
const date = new Date(dateString);
105+
const now = new Date();
106+
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
107+
108+
// Less than a minute
109+
if (diffInSeconds < 60) {
110+
return 'just now';
111+
}
112+
113+
// Less than an hour
114+
if (diffInSeconds < 3600) {
115+
const minutes = Math.floor(diffInSeconds / 60);
116+
return `${minutes} ${minutes === 1 ? 'minute' : 'minutes'} ago`;
117+
}
118+
119+
// Less than a day
120+
if (diffInSeconds < 86400) {
121+
const hours = Math.floor(diffInSeconds / 3600);
122+
return `${hours} ${hours === 1 ? 'hour' : 'hours'} ago`;
123+
}
124+
125+
// Less than a month
126+
if (diffInSeconds < 2592000) {
127+
const days = Math.floor(diffInSeconds / 86400);
128+
return `${days} ${days === 1 ? 'day' : 'days'} ago`;
129+
}
130+
131+
// Less than a year
132+
if (diffInSeconds < 31536000) {
133+
const months = Math.floor(diffInSeconds / 2592000);
134+
return `${months} ${months === 1 ? 'month' : 'months'} ago`;
135+
}
136+
137+
// More than a year
138+
const years = Math.floor(diffInSeconds / 31536000);
139+
return `${years} ${years === 1 ? 'year' : 'years'} ago`;
140+
}
141+
96142
// Merges and formats the class names
97143
export function cn(...inputs: ClassNameValue[]) {
98144
return twMerge(inputs)

0 commit comments

Comments
 (0)