Skip to content

Commit

Permalink
Website: new page for GitHub insights (#926)
Browse files Browse the repository at this point in the history
  • Loading branch information
ssandino authored Dec 10, 2024
1 parent a27099e commit e3eb8fc
Show file tree
Hide file tree
Showing 20 changed files with 1,971 additions and 1 deletion.
27 changes: 27 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 41 additions & 0 deletions shared/locales/en/website-open-source.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"metadata": {
"title": "Open-Source Insights ",
"heading": "Contributors"
},
"contributors": {
"title": "Contributors"
},
"hero": {
"title-1": [
{
"text": "Tools for Change. "
},
{
"text": "Open to All.",
"color": "accent"
}
],
"subtitle": "Everything we build is open source—free to use, adapt, and improve. Join our volunteer community creating tools for equality and change."
},
"overview": {
"forks": {
"title": "Forks",
"time": "last month"
},
"commits": {
"title": "Commits",
"time": "last month"
},
"stars": {
"title": "GitHub Stars",
"time": "last month"
}
},
"issues": {
"title": "Open Issues",
"header": "Issue Title",
"link": "View on GitHub",
"filter": "All Issues"
}
}
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@heroicons/react": "^2.1.5",
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1",
Expand Down
38 changes: 38 additions & 0 deletions ui/src/components/avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as AvatarPrimitive from '@radix-ui/react-avatar';
import * as React from 'react';

import { cn } from '../lib/utils';

const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn('relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full', className)}
{...props}
/>
));
Avatar.displayName = AvatarPrimitive.Root.displayName;

const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image ref={ref} className={cn('aspect-square h-full w-full', className)} {...props} />
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;

const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn('bg-muted flex h-full w-full items-center justify-center rounded-full', className)}
{...props}
/>
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;

export { Avatar, AvatarFallback, AvatarImage };
1 change: 1 addition & 0 deletions ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import './globals.css';

export * from './components/accordion';
export * from './components/alert';
export * from './components/avatar';
export * from './components/badge';
export * from './components/button';
export * from './components/card';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'use client';

import { Avatar, AvatarFallback, AvatarImage, Button, Typography } from '@socialincome/ui';
import { useState } from 'react';

type ContributorProp = {
name: string;
commits: number;
avatarUrl: string;
};

function Contributor({ name, commits, avatarUrl }: ContributorProp) {
return (
<article className="flex min-w-60 flex-row items-center py-2">
<Avatar className="h-12 w-12 flex-shrink-0">
<AvatarImage src={avatarUrl} alt={`${name}'s avatar`} />
<AvatarFallback>{name.slice(0, 2).toUpperCase()}</AvatarFallback>
</Avatar>
<div className="ml-4 flex flex-col items-start">
<Typography as="p" size="xl">
{name}
</Typography>
<Typography as="span" size="md" className="text-card-foreground-muted mt-1">
{commits} {commits === 1 ? 'commit' : 'commits'}
</Typography>
</div>
</article>
);
}

export function OpenSourceContributorsClient({
contributors,
heading,
totalContributors,
}: {
contributors: Array<{ name: string; commits: number; avatarUrl: string; id: number }>;
heading: string;
totalContributors: number;
}) {
const [showAllContributors, setShowAllContributors] = useState(false);

const displayedContributors = showAllContributors ? contributors : contributors.slice(0, 16);

return (
<section className="flex flex-col justify-self-start">
<section>
<Typography as="h2" size="3xl" lineHeight="snug" className="mb-10">
{`${totalContributors} ${heading}`}
</Typography>
</section>

<section className="flex flex-wrap gap-4">
{displayedContributors.map((contributor) => (
<Contributor
key={contributor.id}
name={contributor.name}
commits={contributor.commits}
avatarUrl={contributor.avatarUrl}
/>
))}
</section>

{!showAllContributors && totalContributors > 16 && (
<div className="mt-12 flex justify-center">
<Button
variant="link"
onClick={() => setShowAllContributors(true)}
className="text-card-foreground mr-20 text-xl"
>
{`Show all ${totalContributors} contributors`}
</Button>
</div>
)}
</section>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use client';

import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@socialincome/ui';
import { useState } from 'react';

interface FilterFormProps {
labels: string[];
handleLabel: (label: string) => void;
filterText: string;
}

export function FilterForm({ labels, handleLabel, filterText }: FilterFormProps) {
const [selectedLabel, setSelectedLabel] = useState('');

const handleChange = (value: string) => {
if (value === filterText) {
setSelectedLabel('');
handleLabel('');
} else {
setSelectedLabel(value);
handleLabel(value);
}
};

return (
<section className="mb-8 max-w-44">
<Select value={selectedLabel} onValueChange={handleChange}>
<SelectTrigger aria-label="Filter issues by label">
<SelectValue placeholder={filterText} />
</SelectTrigger>
<SelectContent>
<SelectItem value={filterText}>{filterText}</SelectItem>
{labels.map((label) => (
<SelectItem key={label} value={label}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</section>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
const owner = 'socialincome-san';
const repo = 'public';

interface GitHubCommit {
author: {
id: number;
login: string;
avatar_url: string;
} | null;
commit: {
author: {
date: string;
};
};
}

export async function getCommits() {
// Calculate the date 30 days ago from today
const endDate = new Date().toISOString();
const startDate = new Date();
startDate.setDate(startDate.getDate() - 30);
const startDateISO = startDate.toISOString();

// Fetch recent commits from the last 30 days
const commitUrl = `https://api.github.com/repos/${owner}/${repo}/commits?since=${startDateISO}&until=${endDate}`;
const headers: Record<string, string> = {
Accept: 'application/vnd.github+json',
};
// Conditionally add the Authorization header if GITHUB_PAT is available
if (process.env.GITHUB_PAT) {
headers['Authorization'] = `Bearer ${process.env.GITHUB_PAT}`;
}
const res = await fetch(commitUrl, {
headers,
});

if (!res.ok) {
const errorDetails = await res.text();
const status = res.status;
if (status === 403) {
throw new Error(
'GitHub API rate limit exceeded. Please try again later or increase rate limit by authenticating.',
);
} else if (status === 404) {
throw new Error(`GitHub repository ${owner}/${repo} not found.`);
} else {
throw new Error(`Failed to fetch recent commits from GitHub: ${status} - ${errorDetails}`);
}
}

const recentCommits: GitHubCommit[] = await res.json();

// Fetch total commit count
const totalCommitsUrl = `https://api.github.com/repos/${owner}/${repo}/commits?per_page=1`;
const totalCommitsRes = await fetch(totalCommitsUrl, {
headers,
});

if (!totalCommitsRes.ok) {
const errorDetails = await totalCommitsRes.text();
const status = totalCommitsRes.status;
if (status === 403) {
throw new Error(`GitHub API rate limit exceeded: ${status} - ${errorDetails}.`);
} else if (status === 404) {
throw new Error(`GitHub repository ${owner}/${repo} not found while fetching total commits.`);
} else {
throw new Error(`Failed to fetch total commits from GitHub: ${status} - ${errorDetails}`);
}
}

// Extract the last page number from the Link header to get the total commit count
const linkHeader = totalCommitsRes.headers.get('link');
// Default to 1 in case no Link header is provided
let totalCommits = 1;

if (linkHeader) {
const match = linkHeader.match(/&page=(\d+)>; rel="last"/);
if (match) {
totalCommits = parseInt(match[1], 10);
} else {
console.warn('Link header exists but could not parse total commits page count.');
}
} else {
console.warn('No Link header found; assuming a single commit.');
}

return {
totalCommits,
newCommits: recentCommits.length,
};
}
Loading

0 comments on commit e3eb8fc

Please sign in to comment.