.time ago

This commit is contained in:
afkarxyz
2026-04-02 18:34:48 +07:00
parent 7f12b76fd9
commit 0d42bc3877
+61 -10
View File
@@ -28,7 +28,7 @@ export function AboutPage() {
const [copiedUsdt, setCopiedUsdt] = useState(false); const [copiedUsdt, setCopiedUsdt] = useState(false);
useEffect(() => { useEffect(() => {
const fetchRepoStats = async () => { const fetchRepoStats = async () => {
const CACHE_KEY = "github_repo_stats_v3"; const CACHE_KEY = "github_repo_stats_v4";
const CACHE_DURATION = 1000 * 60 * 60; const CACHE_DURATION = 1000 * 60 * 60;
const cached = localStorage.getItem(CACHE_KEY); const cached = localStorage.getItem(CACHE_KEY);
if (cached) { if (cached) {
@@ -70,8 +70,10 @@ export function AboutPage() {
let totalDownloads = 0; let totalDownloads = 0;
let latestDownloads = 0; let latestDownloads = 0;
let latestVersion = ""; let latestVersion = "";
let latestReleaseAt = "";
if (releases.length > 0) { if (releases.length > 0) {
latestVersion = releases[0].tag_name || ""; latestVersion = releases[0].tag_name || "";
latestReleaseAt = releases[0].published_at || releases[0].created_at || "";
latestDownloads = latestDownloads =
releases[0].assets?.reduce((sum: number, asset: any) => sum + (asset.download_count || 0), 0) || 0; releases[0].assets?.reduce((sum: number, asset: any) => sum + (asset.download_count || 0), 0) || 0;
totalDownloads = releases.reduce((sum: number, release: any) => { totalDownloads = releases.reduce((sum: number, release: any) => {
@@ -91,6 +93,7 @@ export function AboutPage() {
totalDownloads, totalDownloads,
latestDownloads, latestDownloads,
latestVersion, latestVersion,
latestReleaseAt,
languages: topLangs, languages: topLangs,
}; };
} }
@@ -128,6 +131,39 @@ export function AboutPage() {
const diffYears = Math.floor(diffMonths / 12); const diffYears = Math.floor(diffMonths / 12);
return `${diffYears}y`; return `${diffYears}y`;
}; };
const formatReleaseTimeAgo = (dateString: string): string => {
if (!dateString) {
return "";
}
const now = Date.now();
const releasedAt = new Date(dateString).getTime();
if (Number.isNaN(releasedAt)) {
return "";
}
const diffMs = Math.max(0, now - releasedAt);
const totalMinutes = Math.floor(diffMs / (1000 * 60));
const totalHours = Math.floor(totalMinutes / 60);
const totalDays = Math.floor(totalHours / 24);
const totalMonths = Math.floor(totalDays / 30);
const totalYears = Math.floor(totalMonths / 12);
if (totalYears > 0) {
const remainingMonths = totalMonths % 12;
return remainingMonths > 0 ? `${totalYears}y ${remainingMonths}m ago` : `${totalYears}y ago`;
}
if (totalMonths > 0) {
const remainingDays = totalDays % 30;
return remainingDays > 0 ? `${totalMonths}m ${remainingDays}d ago` : `${totalMonths}m ago`;
}
if (totalDays > 0) {
const remainingHours = totalHours % 24;
return remainingHours > 0 ? `${totalDays}d ${remainingHours}h ago` : `${totalDays}d ago`;
}
if (totalHours > 0) {
const remainingMinutes = totalMinutes % 60;
return `${totalHours}h ${remainingMinutes}m ago`;
}
return `${totalMinutes}m ago`;
};
const formatNumber = (num: number): string => { const formatNumber = (num: number): string => {
if (num >= 1000) { if (num >= 1000) {
return num.toLocaleString(); return num.toLocaleString();
@@ -165,9 +201,14 @@ export function AboutPage() {
<CardHeader> <CardHeader>
<div className="flex justify-between items-start mb-2"> <div className="flex justify-between items-start mb-2">
<img src={SpotiFLACNextIcon} className="h-6 w-6 shrink-0" alt="SpotiFLAC Next"/> <img src={SpotiFLACNextIcon} className="h-6 w-6 shrink-0" alt="SpotiFLAC Next"/>
{repoStats["SpotiFLAC-Next"]?.latestVersion && (<span className="text-[10px] bg-primary text-primary-foreground px-1.5 py-0.5 rounded-sm font-mono font-semibold max-w-[80px] truncate"> <div className="flex items-center gap-2">
{repoStats["SpotiFLAC-Next"].latestVersion} {repoStats["SpotiFLAC-Next"]?.latestReleaseAt && (<span className="text-[10px] text-muted-foreground whitespace-nowrap">
</span>)} {formatReleaseTimeAgo(repoStats["SpotiFLAC-Next"].latestReleaseAt)}
</span>)}
{repoStats["SpotiFLAC-Next"]?.latestVersion && (<span className="text-[10px] bg-primary text-primary-foreground px-1.5 py-0.5 rounded-sm font-mono font-semibold max-w-[80px] truncate">
{repoStats["SpotiFLAC-Next"].latestVersion}
</span>)}
</div>
</div> </div>
<CardTitle className="leading-tight"> <CardTitle className="leading-tight">
SpotiFLAC Next SpotiFLAC Next
@@ -214,9 +255,14 @@ export function AboutPage() {
<CardHeader> <CardHeader>
<div className="flex justify-between items-start mb-2"> <div className="flex justify-between items-start mb-2">
<img src={SpotiDownloaderIcon} className="h-6 w-6 shrink-0" alt="SpotiDownloader"/> <img src={SpotiDownloaderIcon} className="h-6 w-6 shrink-0" alt="SpotiDownloader"/>
{repoStats["SpotiDownloader"]?.latestVersion && (<span className="text-[10px] bg-primary text-primary-foreground px-1.5 py-0.5 rounded-sm font-mono font-semibold max-w-[80px] truncate"> <div className="flex items-center gap-2">
{repoStats["SpotiDownloader"].latestVersion} {repoStats["SpotiDownloader"]?.latestReleaseAt && (<span className="text-[10px] text-muted-foreground whitespace-nowrap">
</span>)} {formatReleaseTimeAgo(repoStats["SpotiDownloader"].latestReleaseAt)}
</span>)}
{repoStats["SpotiDownloader"]?.latestVersion && (<span className="text-[10px] bg-primary text-primary-foreground px-1.5 py-0.5 rounded-sm font-mono font-semibold max-w-[80px] truncate">
{repoStats["SpotiDownloader"].latestVersion}
</span>)}
</div>
</div> </div>
<CardTitle className="leading-tight"> <CardTitle className="leading-tight">
SpotiDownloader SpotiDownloader
@@ -264,9 +310,14 @@ export function AboutPage() {
<CardHeader> <CardHeader>
<div className="flex justify-between items-start mb-2"> <div className="flex justify-between items-start mb-2">
<img src={XBatchDLIcon} className="h-6 w-6 shrink-0" alt="Twitter/X Media Batch Downloader"/> <img src={XBatchDLIcon} className="h-6 w-6 shrink-0" alt="Twitter/X Media Batch Downloader"/>
{repoStats["Twitter-X-Media-Batch-Downloader"]?.latestVersion && (<span className="text-[10px] bg-primary text-primary-foreground px-1.5 py-0.5 rounded-sm font-mono font-semibold max-w-[80px] truncate"> <div className="flex items-center gap-2">
{repoStats["Twitter-X-Media-Batch-Downloader"].latestVersion} {repoStats["Twitter-X-Media-Batch-Downloader"]?.latestReleaseAt && (<span className="text-[10px] text-muted-foreground whitespace-nowrap">
</span>)} {formatReleaseTimeAgo(repoStats["Twitter-X-Media-Batch-Downloader"].latestReleaseAt)}
</span>)}
{repoStats["Twitter-X-Media-Batch-Downloader"]?.latestVersion && (<span className="text-[10px] bg-primary text-primary-foreground px-1.5 py-0.5 rounded-sm font-mono font-semibold max-w-[80px] truncate">
{repoStats["Twitter-X-Media-Batch-Downloader"].latestVersion}
</span>)}
</div>
</div> </div>
<CardTitle className="leading-tight"> <CardTitle className="leading-tight">
Twitter/X Media Batch Downloader Twitter/X Media Batch Downloader