Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release v1.1.0-beta.3 -> main #24

Merged
merged 8 commits into from
Jan 5, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: spice up contributors pr chart styles (#20)
* use v2 pr/search

* fix build

* wip data

* data is fetched and structured

* remove async functions

* add nivo/line and prep data

* o.filter is not a function

* dirty fix for rendering the chart

* fix type error

* adds meta data

* npm run format

* swap Datum

* npm run format

* chore: add required packages

* chore: disable comma dangle rule

* chore: update chart styles and data

* feat: fix all type errors and refactor implementation

* chore: show stats data

* chore: more updates to charts data

* chore: update formating and color of chart lines

* misc: minor refactor

* fix: build now goes through

* Update components/CIResponsiveLine.tsx

* chore: fetch data from server and remove loading state

* style: update styles for mobile

* style: reduce chart margin right

* fix: chart lines inconsistenccy

* fix: time formating error

---------

Co-authored-by: Brian 'bdougie' Douglas <[email protected]>
Co-authored-by: Brian Douglas <[email protected]>
3 people authored Jan 5, 2024
commit bdf937fbe3f5d900576f4401f1667253b3832a7a
21 changes: 17 additions & 4 deletions components/CIResponsiveLine.tsx
Original file line number Diff line number Diff line change
@@ -7,12 +7,13 @@ const CIResponsiveLine = ({ data }: Datum) => {
<div style={{ height: 400, width: "100%" }}>
<ResponsiveLine
data={data}
margin={{ top: 50, right: 150, bottom: 50, left: 60 }}
margin={{ top: 50, right: 50, bottom: 50, left: 60 }}
yScale={{
type: "linear",
stacked: true,
}}
motionConfig="stiff"
curve="catmullRom"
curve="basis"
enableSlices="x"
axisTop={null}
isInteractive={true}
@@ -28,8 +29,21 @@ const CIResponsiveLine = ({ data }: Datum) => {
}}
axisBottom={{
tickSize: 0,
tickValues: 7,
tickPadding: 5,
format: (value) => {
return format(parse(value, "MM/dd/yyyy", new Date()), "MMM d");
const date = parse(value, "MM/dd/yyyy", new Date());
return format(date, "MMM d");
},
}}
theme={{
axis: {},
grid: {
line: {
strokeDasharray: "4 4",
strokeWidth: 1,
strokeOpacity: 0.7,
},
},
}}
pointSize={0}
@@ -38,7 +52,6 @@ const CIResponsiveLine = ({ data }: Datum) => {
enableGridY={false}
useMesh={true}
enableArea={false}
enableCrosshair={true}
enablePointLabel={false}
colors={(d) => d.color}
/>
19 changes: 16 additions & 3 deletions hooks/useContributorData.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import useSWR from "swr";
import useSWR, { Fetcher } from "swr";
import { useCallback } from "react";
import apiFetcher from "./useSWR";
interface PaginatedDataResponse {
readonly data: DBContributorsPR[];
readonly meta: Meta;
}

type query = {
repo: string;
limit?: number;
startDate?: number;
status?: "closed" | "open";
initialData?: PaginatedDataResponse;
};

// We're not currently using this, we're just using useSWR directly inside ChildWithSWR
// this needs useCallback wrap if we want to use it in the other component
const useContributorData = (repo: string, startDate?: number, status?: "closed" | "open") => {
const useContributorData = ({ repo, startDate, status, limit, initialData }: query) => {
const query = new URLSearchParams();

if (startDate) {
@@ -24,7 +33,11 @@ const useContributorData = (repo: string, startDate?: number, status?: "closed"

const endpointString = `${baseEndpoint}?${query.toString()}`;

const { data, error, mutate } = useSWR<PaginatedDataResponse, Error>(repo ? endpointString : null);
const { data, error, mutate } = useSWR<PaginatedDataResponse, Error>(
repo ? endpointString : null,
apiFetcher as Fetcher<PaginatedDataResponse, Error>,
{ fallbackData: initialData }
);

return {
data: data?.data ?? [],
14 changes: 8 additions & 6 deletions lib/prCounts.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { format, parse } from "date-fns";

const count = (prData: DBContributorsPR[]): { mergedCount: number; closedCount: number; totalCount: number } => {
const mergedCount = prData.filter((item) => item.pr_is_merged).length; // Merged PRs
const closedCount = prData.filter((item) => item.pr_state === "closed").length; // Closed PRs
@@ -13,8 +15,8 @@ const count = (prData: DBContributorsPR[]): { mergedCount: number; closedCount:

const prPerDay = (open: DBContributorsPR[], closed: DBContributorsPR[]) => {
const sortedMergedPRs = closed.sort((a, b) => {
const aDate = new Date(a.pr_created_at);
const bDate = new Date(b.pr_created_at);
const aDate = new Date(a.pr_closed_at);
const bDate = new Date(b.pr_closed_at);

return aDate.getTime() - bDate.getTime();
});
@@ -26,7 +28,7 @@ const prPerDay = (open: DBContributorsPR[], closed: DBContributorsPR[]) => {
return aDate.getTime() - bDate.getTime();
});
const mergedPRsPerDay = sortedMergedPRs.reduce<Record<string, number>>((acc, item) => {
const mergedDate = new Date(item.pr_merged_at).toLocaleDateString();
const mergedDate = format(new Date(item.pr_merged_at), "MM/dd/yyyy");

if (item.pr_is_merged) {
if (!acc[mergedDate]) {
@@ -39,7 +41,7 @@ const prPerDay = (open: DBContributorsPR[], closed: DBContributorsPR[]) => {
}, {});

const closedPRsPerDay = sortedMergedPRs.reduce<Record<string, number>>((acc, item) => {
const closedDate = new Date(item.pr_updated_at).toLocaleDateString();
const closedDate = format(new Date(item.pr_closed_at), "MM/dd/yyyy");

if (item.pr_is_merged === false) {
if (!acc[closedDate]) {
@@ -52,7 +54,7 @@ const prPerDay = (open: DBContributorsPR[], closed: DBContributorsPR[]) => {
}, {});

const openedPRsPerDay = sortedOpenedPRs.reduce<Record<string, number>>((acc, item) => {
const openedDate = new Date(item.pr_created_at).toLocaleDateString();
const openedDate = format(new Date(item.pr_created_at), "MM/dd/yyyy");

if (!acc[openedDate]) {
acc[openedDate] = 0;
@@ -75,7 +77,7 @@ const prPerDay = (open: DBContributorsPR[], closed: DBContributorsPR[]) => {
},
{
id: "Merged PRs",
color: "#3b38f1",
color: "#A78BFA",
data: mergedPrs,
},
{
1 change: 1 addition & 0 deletions next-types.d.ts
Original file line number Diff line number Diff line change
@@ -24,4 +24,5 @@ interface DBContributorsPR {
readonly pr_title: string;
readonly pr_updated_at: string;
readonly repo_name: string;
readonly pr_closed_at: string;
}
25 changes: 19 additions & 6 deletions npm-shrinkwrap.json

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

279 changes: 188 additions & 91 deletions pages/[owner]/[repo]/index.tsx
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ const CIResponsiveLine = dynamic(() => import("components/CIResponsiveLine"), {

import prPerDay from "lib/prCounts";
import { useContributorData } from "hooks/useContributorData";
import { GetServerSidePropsContext } from "next";

export interface PaginatedDataResponse {
readonly data: DBContributorsPR[];
@@ -22,123 +23,219 @@ interface StatsCardProps {
status: "open" | "closed" | "merged";
prevMonthCount?: number;
}
const ChildWithSWR = (props: { owner: string; repo: string }) => {
const { owner, repo } = props;

const { data: openedPrs } = useContributorData(`${owner}/${repo}`, 0, "open");
const { data: prevMonthOpenedPrs, meta: prevMonthOpenedPrsMeta } = useContributorData(`${owner}/${repo}`, 30, "open");

const { data: closedPrs } = useContributorData(`${owner}/${repo}`, 0, "closed");
const { data: prevMonthClosedPrs, meta: prevMonthClosedPrsMeta } = useContributorData(
`${owner}/${repo}`,
30,
"closed"
);

const { data: currentData, isError, isLoading, meta } = useContributorData(`${owner}/${repo}`);

// fetch previous months data seperately to compare

if (isLoading) {
return <>Loading...</>;
}

const chartData = prPerDay(openedPrs, closedPrs);

const StatsCard = ({ type, status, count, prevMonthCount }: StatsCardProps) => {
const IconMap = {
issue: {
open: <GoIssueOpened />,
closed: <GoIssueClosed />,
merged: <GoGitMerge />,
},
pr: {
open: <AiOutlinePullRequest />,
closed: <GoGitPullRequestClosed />,
merged: <GoGitMerge />,
},
};
const getPercentageChange = (prevCount: number, currentCount: number) => {
const percentageChange = Math.abs((currentCount - prevCount) / prevCount) * 100;
return percentageChange;
};

const StatsCard = ({ type, status, count, prevMonthCount }: StatsCardProps) => {
const IconMap = {
issue: {
open: <GoIssueOpened />,
closed: <GoIssueClosed />,
merged: <GoGitMerge />,
},
pr: {
open: <AiOutlinePullRequest />,
closed: <GoGitPullRequestClosed />,
merged: <GoGitMerge />,
},
};

return (
<div className="bg-white border shadow rounded-lg p-4 pb-6 flex flex-col gap-4 text-slate-800 min-w-[340px]">
<div
className={`border p-3 rounded-md w-max ${
status === "open" ? "text-green-500" : status === "closed" ? "text-red-500" : "text-violet-400"
}`}
>
{IconMap[type][status]}
</div>
<div className="capitalize text-gray-500 text-sm">
Pull Requests {status === "merged" || status === "closed" ? status : null}
</div>
<div className="flex items-center justify-between">
<span className="text-3xl font-semibold">{count}</span>
<div className="flex items-center gap-1">
{getPercentageChange(prevMonthCount!, count) >= 0 ? (
<FaArrowTrendUp className="text-green-700" />
) : (
<FaArrowTrendDown className="text-red-700" />
)}
<span className={`${getPercentageChange(prevMonthCount!, count) >= 0 ? "text-green-700" : "text-red-700"}`}>
{getPercentageChange(prevMonthCount!, count).toFixed(2)}%
</span>{" "}
vs last month
</div>
return (
<div className="bg-white border shadow rounded-lg p-4 pb-6 flex flex-col gap-4 text-slate-800 w-full md:w-[340px] ">
<div
className={`border p-3 rounded-md w-max ${
status === "open" ? "text-green-500" : status === "closed" ? "text-red-500" : "text-violet-400"
}`}
>
{IconMap[type][status]}
</div>
<div className="capitalize text-gray-500 text-sm">
Pull Requests {status === "merged" || status === "closed" ? status : null}
</div>
<div className="flex items-center justify-between">
<span className="text-3xl font-semibold">{count}</span>
<div className="flex items-center gap-1">
{getPercentageChange(prevMonthCount!, count) >= 0 ? (
<FaArrowTrendUp className="text-green-700" />
) : (
<FaArrowTrendDown className="text-red-700" />
)}
<span className={`${getPercentageChange(prevMonthCount!, count) >= 0 ? "text-green-700" : "text-red-700"}`}>
{getPercentageChange(prevMonthCount!, count).toFixed(2)}%
</span>{" "}
vs last month
</div>
</div>
);
</div>
);
};

const DataLabel = ({ label, type }: { label: string; type: "merged" | "closed" | "open" }) => {
const getColorByType = (type: string) => {
switch (type) {
case "merged":
return "bg-violet-400";
case "closed":
return "bg-red-500";
case "open":
return "bg-green-500";
}
};

return (
<div className="bg-white h-screen flex justify-center items-center w-full flex-col px-8 text-black">
<div className="flex gap-3 mr-auto flex-wrap">
<StatsCard
type="pr"
status="open"
count={meta.itemCount}
prevMonthCount={prevMonthOpenedPrs ? prevMonthOpenedPrsMeta.itemCount : undefined}
/>
<StatsCard
type="pr"
status="merged"
count={closedPrs.filter((pr) => pr.pr_is_merged === true).length}
prevMonthCount={prevMonthClosedPrs ? prevMonthClosedPrsMeta.itemCount : undefined}
/>
<StatsCard
type="pr"
status="closed"
count={closedPrs.filter((item) => item.pr_state === "close" && !item.pr_is_merged).length}
prevMonthCount={prevMonthClosedPrsMeta ? prevMonthClosedPrsMeta.itemCount : undefined}
/>
</div>

<CIResponsiveLine data={chartData} />

<div className="flex justify-center gap-4 flex-wrap"></div>
<div className="flex items-center gap-2">
<span className={`p-1 rounded-full ${getColorByType(type)}`}></span>
<p className="text-sm text-gray-600">{label}</p>
</div>
);
};

// Is this a React component? Maybe the file should be .tsx? +1
const OwnerRepo = () => {
const OwnerRepo = ({
currentOpenPrs,
prevOpenPrs,
currentClosedPrs,
prevClosedPrs,
}: {
currentOpenPrs: PaginatedDataResponse;
prevOpenPrs: PaginatedDataResponse;
currentClosedPrs: PaginatedDataResponse;
prevClosedPrs: PaginatedDataResponse;
}) => {
const router = useRouter();

// Keep track of our assumptions about data types coming from `router`
const owner = router.query["owner"] as string;
const repo = router.query["repo"] as string;

const { data: openedPrs, meta: openedPrsMeta } = useContributorData({
repo: `${owner}/${repo}`,
status: "open",
initialData: currentOpenPrs,
});
const { data: prevMonthOpenedPrs, meta: prevMonthOpenedPrsMeta } = useContributorData({
repo: `${owner}/${repo}`,
startDate: 30,
status: "open",
initialData: prevOpenPrs,
});

const { data: closedPrs } = useContributorData({
repo: `${owner}/${repo}`,
status: "closed",
initialData: currentClosedPrs,
});
const { data: prevMonthClosedPrs, meta: prevMonthClosedPrsMeta } = useContributorData({
repo: `${owner}/${repo}`,
startDate: 30,
status: "closed",
initialData: prevClosedPrs,
});

if (owner && repo) {
return <ChildWithSWR owner={owner} repo={repo} />;
const chartData = prPerDay(openedPrs, closedPrs);

return (
<div className="bg-white">
<div className="min-h-screen flex pt-20 w-full container flex-col text-black">
<div className="flex gap-3 mr-auto flex-wrap">
<StatsCard
type="pr"
status="open"
count={openedPrsMeta.itemCount}
prevMonthCount={prevMonthOpenedPrs ? prevMonthOpenedPrsMeta.itemCount : undefined}
/>
<StatsCard
type="pr"
status="merged"
count={closedPrs.filter((pr) => pr.pr_is_merged === true).length}
prevMonthCount={prevMonthClosedPrs ? prevMonthClosedPrsMeta.itemCount : undefined}
/>
<StatsCard
type="pr"
status="closed"
count={closedPrs.filter((item) => item.pr_state === "close" && !item.pr_is_merged).length}
prevMonthCount={prevMonthClosedPrsMeta ? prevMonthClosedPrsMeta.itemCount : undefined}
/>
</div>

<CIResponsiveLine data={chartData} />

<div className="flex gap-4 flex-wrap mt-3 pl-5">
<DataLabel label="Pull Requests" type="open" />
<DataLabel label="Pull Requests Merged" type="merged" />
<DataLabel label="Pull Requests Closed" type="closed" />
</div>
</div>
</div>
);
}

console.warn("No owner or repo:", owner, repo);

return "Loading...";
};

export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const { owner, repo } = ctx.params as { owner: string; repo: string };

if (!owner || !repo) {
return {
notFound: true,
};
}

const [currentOpenData, prevOpenData, currentClosedData, prevClosedData] = await Promise.all([
fetch(`${process.env.NEXT_PUBLIC_V2_API_URL}/prs/search?repo=${owner}/${repo}&limit=100&state=open`, {
headers: {
accept: "application/json",
"Content-Type": "application/json",
},
}),
fetch(`${process.env.NEXT_PUBLIC_V2_API_URL}/prs/search?repo=${owner}/${repo}&prev_days_start_date=30&state=open`, {
headers: {
accept: "application/json",
"Content-Type": "application/json",
},
}),
fetch(`${process.env.NEXT_PUBLIC_V2_API_URL}/prs/search?repo=${owner}/${repo}&limit=100&state=closed`, {
headers: {
accept: "application/json",
"Content-Type": "application/json",
},
}),
fetch(
`${process.env.NEXT_PUBLIC_V2_API_URL}/prs/search?repo=${owner}/${repo}&prev_days_start_date=30&state=closed`,
{
headers: {
accept: "application/json",
"Content-Type": "application/json",
},
}
),
]);

if (!currentOpenData.ok || !currentOpenData.ok) {
return {
notFound: true,
};
}

const [currentOpenDataJson, prevOpenDataJson, currentClosedDataJson, prevClosedDataJson] = await Promise.all([
currentOpenData.json(),
prevOpenData.json(),
currentClosedData.json(),
prevClosedData.json(),
]);

return {
props: {
currentOpenPrs: currentOpenDataJson as PaginatedDataResponse,
prevOpenPrs: prevOpenDataJson as PaginatedDataResponse,
currentClosedPrs: currentClosedDataJson as PaginatedDataResponse,
prevClosedPrs: prevClosedDataJson as PaginatedDataResponse,
},
};
}

export default OwnerRepo;
17 changes: 6 additions & 11 deletions pages/index.tsx
Original file line number Diff line number Diff line change
@@ -15,15 +15,6 @@ const Home: NextPage = () => {
event.preventDefault();
};

// const handleLuck = (React.MouseEvent) => {
// const randomRepo = Math.floor(Math.random() * 100);

// // fetch top 100 repos and pass int
// searchBoxRef.current && router.push(searchBoxRef.current.value);
// event.preventDefault();
// };


useKeys(["Enter"], handleSubmit, { target: searchBoxRef });

return (
@@ -52,8 +43,12 @@ const Home: NextPage = () => {
</div>

<div className="flex">
<button onClick={(e) => handleSubmit(e)} type="submit" className="mr-2 bui-btn sbui-btn-primary sbui-btn-container--shadow sbui-btn--tiny undefined !text-sm !font-semibold !tracking-tight !py-1 !px-3 !rounded-md !px- focus-visible:!border-orange-500 focus:outline-none focus-visible:ring focus-visible:!ring-orange-200 !bg-orange-500 !border-orange-500 hover:!bg-orange-600 sbui-btn--text-align-center">
<span >submit</span>
<button
onClick={(e) => handleSubmit(e)}
type="submit"
className="mr-2 bui-btn sbui-btn-primary sbui-btn-container--shadow sbui-btn--tiny undefined !text-sm !font-semibold !tracking-tight !py-1 !px-3 !rounded-md !px- focus-visible:!border-orange-500 focus:outline-none focus-visible:ring focus-visible:!ring-orange-200 !bg-orange-500 !border-orange-500 hover:!bg-orange-600 sbui-btn--text-align-center"
>
<span>submit</span>
</button>
{/* <button
onClick={() => console.log("onHandeLuck")}