Skip to content

Commit

Permalink
feat: connect repositories page to API data and pagination (#405)
Browse files Browse the repository at this point in the history
Closes #320, #384
  • Loading branch information
brandonroberts committed Sep 22, 2022
1 parent 4e394bd commit 634de8e
Show file tree
Hide file tree
Showing 20 changed files with 248 additions and 145 deletions.
2 changes: 1 addition & 1 deletion components/atoms/Pill/pill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const Pill: React.FC<PillProps> = ({ className, text, color = "slate", size = "b
return (
<div
className={`
${color === "green" ? "bg-light-grass-4 " : color === "yellow" ? "bg-light-amber-4 " : color === "red" ? "bg-light-red-4 " : "bg-light-slate-4 "}
${color === "green" ? "bg-light-grass-4 " : color === "yellow" ? "bg-amber-100 " : color === "red" ? "bg-light-red-4 " : "bg-light-slate-4 "}
${size === "small" ? "py-1 px-1.5 gap-1 " : "py-1.5 px-2 gap-1 "}
inline-flex rounded-full`}>
{icon}
Expand Down
17 changes: 9 additions & 8 deletions components/molecules/Pagination/pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,27 @@ const Pagination = ({
page,
divisor = true,
goToPage = false,
pageSize = 5,
pageSize = 10,
hasPreviousPage = false,
hasNextPage = true
hasNextPage = true,
onPageChange
}: PaginationProps): JSX.Element => {
// This logics are meant for testing purpose
const [selected, setSelected] = useState<number>(1);
const [selected, setSelected] = useState<number>(page);
const handleSelected = (pageNumber: number) => {
setSelected(pageNumber);
onPageChange(pageNumber);
};
const handleNext = () => {
setSelected((prev) => prev + 1);
onPageChange(page + 1);
};
const handlePrev = () => {
setSelected((prev) => prev - 1);
onPageChange(page - 1);
};

return (
<div className=" w-max flex gap-x-4 items-center ">
<div className="flex items-center gap-x-4">
<button className="text-light-slate-9 disabled:text-light-slate-7" disabled={!hasPreviousPage ? true : false} onClick={() => handleNext()}>
<button className="text-light-slate-9 disabled:text-light-slate-7" disabled={!hasPreviousPage ? true : false} onClick={() => handlePrev()}>
<RiArrowLeftSLine onClick={() => handlePrev()} className="text-lg " />
</button>
{pages.map((page, index) => {
Expand Down Expand Up @@ -72,7 +73,7 @@ const Pagination = ({
>
Total {totalPage > 999 ? humanizeNumber(totalPage, null) : totalPage} pages
</div>
{goToPage && <PaginationGotoPage currentPage={page} name={""} />}
{goToPage && <PaginationGotoPage page={page} setPage={handleSelected} name={""} />}
</div>
);
};
Expand Down
18 changes: 12 additions & 6 deletions components/molecules/PaginationGotoPage/pagination-goto-page.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import React, { ChangeEvent, useState } from "react";
import React, { useEffect, useState } from "react";

interface PaginationGotoPageProps {
currentPage: number;
page: number;
name: string;
setPage: Function;
}

const PaginationGotoPage = ({ currentPage, name }: PaginationGotoPageProps): JSX.Element => {
const [pageNumber, setPageNumber] = useState<number | string>(currentPage);
const PaginationGotoPage = ({ page, name, setPage }: PaginationGotoPageProps): JSX.Element => {
const [pageNumber, setPageNumber] = useState<number | string>(page);
const handleGotoPage = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if(pageNumber === currentPage) return;

// logic to switch page goes here
if(pageNumber === page) return;

setPage(pageNumber);
};

useEffect(() => {
setPageNumber(page);
}, [page]);

return (
<form onSubmit={(e) => handleGotoPage(e)} className="flex font-medium gap-x-3">
<input
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ArrowDownIcon, ArrowUpIcon } from "@primer/octicons-react";
import React from "react";
import PullRequestOverviewChart from "../../atoms/PullRequestOverviewChart/pull-request-overview-chart";

Expand All @@ -20,14 +21,14 @@ const PullRequestOverview: React.FC<PullRequestOverviewProps> = ({ className, op

{/* Total Number of Pull Requests */}
<div className="font-medium text-base text-light-slate-11 tracking-tight">
{totalPullRequests} {`PR${totalPullRequests > 1 ? "s" : ""}`}
{totalPullRequests} {`PR${totalPullRequests === 1 ? "" : "s"}`}
</div>

{/* Churn Number compared with previous date (default: last 30 days vs. previous 30 days range) */}
<div className={`
${churnDirection === "up" ? "text-light-grass-10" : "text-light-red-10"}
font-medium text-base tracking-tight`}>
{churnDirection === "up" ? "+" : "-"}{churn}%
{churnDirection === "up" ? <ArrowUpIcon size={14} /> : <ArrowDownIcon size={14} />}{churn || 0}%
</div>
</div>

Expand Down
218 changes: 111 additions & 107 deletions components/molecules/RepoRow/repo-row.tsx
Original file line number Diff line number Diff line change
@@ -1,153 +1,157 @@
import { ArrowTrendingDownIcon, ArrowTrendingUpIcon, MinusSmallIcon } from "@heroicons/react/24/solid";
import StackedAvatar from "components/molecules/StackedAvatar/stacked-avatar";
import { RepositoriesRows } from "components/organisms/RepositoriesTable/repositories-table";
import React from "react";

import {classNames} from "components/organisms/RepositoriesTable/repositories-table";
import TableRepositoryName from "../TableRepositoryName/table-repository-name";
import Pill from "components/atoms/Pill/pill";
import PullRequestOverview from "../PullRequestOverview/pull-request-overview";
import Avatar from "components/atoms/Avatar/avatar";

import Sparkline from "components/atoms/Sparkline/sparkline";
import { truncateString } from "lib/utils/truncate-string";

import { useContributionsList } from "lib/hooks/useContributionsList";
import { useRepositoryCommits } from "lib/hooks/useRepositoryCommits";
import differenceInDays from "date-fns/differenceInDays";

interface RepoRowProps {
repo: RepositoriesRows;
}

const RepoRow = ({repo}:RepoRowProps): JSX.Element =>{
const {name, handle, owner_avatar: ownerAvatar, activity = "high", openPrsCount, closedPrsCount, draftPrsCount,mergedPrsCount, spamPrsCount,churn,churnTotalCount, churnDirection} = repo;
interface CommitGraphData {
x: number,
y: number;
}

const contributors = [
{
avatarURL: "",
initials: "ES",
alt: "E"
},
{
avatarURL: "",
initials: "ES",
alt: "E"
},
{
avatarURL: "",
initials: "ES",
alt: "E"
const getActivity = (total?: number, loading?: boolean) => {
if (total === undefined || loading) {
return "-";
}

if (total > 80) {
return <Pill icon={<ArrowTrendingUpIcon color="green" className="h-4 w-4" />} text="High" color="green" />;
}

if (total >= 20 && total <= 80) {
return <Pill icon={<MinusSmallIcon color="black" className="h-4 w-4" />} text="Medium" color="yellow" />;
}

return <Pill icon={<ArrowTrendingDownIcon color="red" className="h-4 w-4" />} text="Low" color="red" />;
};

const getCommitsLast30Days = (commits: DbRepoCommit[]): CommitGraphData[] => {
const commitDays = commits.reduce((days: { [name: string]: number }, curr: DbRepoCommit) => {
const day = differenceInDays(new Date(), new Date(Number(curr.commit_time)));

if (days[day]) {
days[day]++;
} else {
days[day] = 1;
}
];

return days;
}, {});

const days: any[] = [];
for(let d=30;d>=0;d--) {
days.push({ x: d, y: commitDays[d] || 0 });
}

return days;
};

const getTotalPrs = (openPrsCount?: number, mergedPrsCount?: number, closedPrsCount?: number, draftPrsCount?: number): number => {
const open = openPrsCount || 0;
const merged = mergedPrsCount || 0;
const closed = closedPrsCount || 0;
const drafts = draftPrsCount || 0;

const total = open + closed + merged - drafts;

if (total <= 0) {
return 0;
}

return total;
};

const getPrsMerged = (total: number, merged: number): number => {
if (total <= 0) {
return 0;
}

const result = Math.floor(merged/total * 100);

return result;
};

const getPrsSpam = (total: number, spam: number): number => {
if (total <= 0) {
return 0;
}

const result = Math.floor(spam/total * 100);

return result;
};

const RepoRow = ({repo}:RepoRowProps): JSX.Element => {
const { name, owner: handle, owner_avatar: ownerAvatar, openPrsCount, closedPrsCount, draftPrsCount, mergedPrsCount, spamPrsCount, churn, churnTotalCount, churnDirection, prVelocityCount } = repo;
const { data: contributorData, meta: contributorMeta } = useContributionsList(repo.id, "", "updated_at");
const { data: commitsData, meta: commitMeta, isLoading: commitLoading } = useRepositoryCommits(repo.id);
const totalPrs = getTotalPrs(openPrsCount, mergedPrsCount, closedPrsCount, draftPrsCount);
const prsMergedPercentage = getPrsMerged(totalPrs, mergedPrsCount || 0);
const spamPrsPercentage = getPrsSpam(totalPrs, spamPrsCount || 0);

const days = getCommitsLast30Days(commitsData);
const last30days = [
{
"id": "japan",
"id": `last30-${repo.id}`,
"color": "hsl(63, 70%, 50%)",
"data": [
{
"x": "plane",
"y": 287
},
{
"x": "helicopter",
"y": 183
},
{
"x": "boat",
"y": 112
},
{
"x": "train",
"y": 78
},
{
"x": "subway",
"y": 47
},
{
"x": "bus",
"y": 218
},
{
"x": "car",
"y": 106
},
{
"x": "moto",
"y": 190
},
{
"x": "bicycle",
"y": 88
},
{
"x": "horse",
"y": 8
},
{
"x": "skateboard",
"y": 248
},
{
"x": "others",
"y": 76
},
{
"x": "adwawd",
"y": 76
},
{
"x": "awdawdd",
"y": 38
},
{
"x": "awd",
"y": 42
},
{
"x": "adwadadw",
"y": 26
},
{
"x": "dadawda",
"y": 76
}
]
data: days
}
];
return ( <div className={`${classNames.row}`}>

return (<div className={`${classNames.row}`}>

{/* Column: Repository Name */}
<div className={classNames.cols.repository}>
<TableRepositoryName avatarURL={ownerAvatar} name={name} handle={handle}></TableRepositoryName>

</div>

{/* Column: Activity */}
<div className={classNames.cols.activity}>
{ activity &&
<Pill text={activity} />
}

{ getActivity(commitMeta.itemCount, commitLoading) }
</div>

{/* Column: PR Overview */}
<div className={classNames.cols.prOverview}>
<PullRequestOverview open={openPrsCount} merged={mergedPrsCount} closed={closedPrsCount} draft={draftPrsCount} churn={churnTotalCount} churnDirection={`${churnDirection}`}></PullRequestOverview>

</div>

{/* Column: PR Velocity */}
<div className={`${classNames.cols.prVelocity}`}>
<div>3 PRs</div>
<Pill text="10%" size="small" color="green" />
<div>{ prVelocityCount ?? 0 } PR{ prVelocityCount === 1 ? "" : "s" }</div>
<Pill text={`${prsMergedPercentage}%`} size="small" color="green" />
</div>

{/* Column: SPAM */}
<div className={`${classNames.cols.prVelocity}`}>
<div>{spamPrsCount + " PRs"}</div>
<Pill text={`${churn}`} size="small" color="green" />
<div className={`${classNames.cols.spam}`}>
{
spamPrsCount && spamPrsCount > 0 ?
<>
<div>{spamPrsCount || 0} PR{ spamPrsCount === 1 ? "" : "s" }</div>
<Pill text={`${spamPrsPercentage || 0}%`} size="small" color={spamPrsPercentage > 10 ? "red" : "yellow"} />
</>
:
"-"
}
</div>

{/* Column: Contributors */}
<div className={`flex ${classNames.cols.contributors}`}>
{ contributorMeta.itemCount! > 0 ? <StackedAvatar contributors={contributorData}/> : "-" }

{contributors?.map(({ avatarURL, initials, alt}) =>
<Avatar key={`${initials}-${alt}`} avatarURL={avatarURL} initials={initials} size={32} hasBorder isCircle />
)}
{ contributorMeta.itemCount! >= 5 ? <div>&nbsp;{`+${contributorMeta.itemCount - 5}`}</div>: "" }
</div>

{/* Column: Last 30 Days */}
Expand Down
20 changes: 20 additions & 0 deletions components/molecules/StackedAvatar/stacked-avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getAvatarLink } from "lib/utils/github";
import Avatar from "../../atoms/Avatar/avatar";

export declare interface StackedAvatarProps {
contributors: DbContribution[];
}
const StackedAvatar = ({ contributors }: StackedAvatarProps) => (
<div className="-space-x-3 flex">
{contributors && contributors.slice(0, 5).map(({ host_login: hostLogin, name }, index) => (
<div
key={`contributor-avatar-${hostLogin}`}
className={`w-8 h-8 overflow-hidden rounded-full border-2 border-solid border-white z-${50-(index+1)*10}`}
>
<Avatar key={`${hostLogin}`} avatarURL={getAvatarLink(hostLogin)} alt={name} size={32} hasBorder isCircle />
</div>
))}
</div>
);

export default StackedAvatar;
Loading

0 comments on commit 634de8e

Please sign in to comment.