Skip to content

Commit d71c03f

Browse files
theflucsfuoridallareteBalastrong
authored
refactor: stats page (#107)
* created new type * created new components * created new custom hook * style: minor fixes * created new and refactor utils functions * refactor pages and deleted unnecessary code * refactor ReposFilters component * style fix: revert to previous style * removed unused props * convert interfaces into ts types * converted enum into ts type * fix imports * fix imports * fix warnings * build: resolve paths for vitest --------- Co-authored-by: sa.cux <[email protected]> Co-authored-by: Leonardo Montini <[email protected]>
1 parent c06d357 commit d71c03f

20 files changed

+425
-358
lines changed

src/components/CardSkeleton.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from "react";
22
import Skeleton from "react-loading-skeleton";
33
import "react-loading-skeleton/dist/skeleton.css";
44

5-
const CardSkeleton = () => {
5+
export const CardSkeleton = () => {
66
return (
77
<div className="card-body h-full">
88
<h2 className="card-title flex justify-between">
@@ -25,5 +25,3 @@ const CardSkeleton = () => {
2525
</div>
2626
);
2727
};
28-
29-
export default CardSkeleton;

src/components/ExportDropdown.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { FC } from "react";
2+
import { exportAsImage } from "@/utils";
3+
4+
export const ExportDropdown: FC = () => {
5+
return (
6+
<div className="dropdown">
7+
<button
8+
tabIndex={0}
9+
className="block w-fit btn btn-primary m-1 p-2 rounded"
10+
>
11+
Export as image
12+
</button>
13+
<ul
14+
tabIndex={0}
15+
className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"
16+
>
17+
<li>
18+
<button
19+
className="btn-ghost"
20+
onClick={() => exportAsImage(".grid", "download", "stats")}
21+
>
22+
Download as PNG
23+
</button>
24+
</li>
25+
<li>
26+
<button
27+
className="btn-ghost"
28+
onClick={() => exportAsImage(".grid", "clipboard")}
29+
>
30+
Copy to Clipboard
31+
</button>
32+
</li>
33+
</ul>
34+
</div>
35+
);
36+
};
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { FC } from "react";
2+
import { useMemo } from "react";
3+
import { RepositoryContributionsCard } from "./RepositoryContributionsCard";
4+
import { ExportDropdown } from "./ExportDropdown";
5+
import {
6+
PullRequestContributionsByRepository,
7+
RepositoryRenderFormat,
8+
} from "@/types/github";
9+
import { exportAsJSON, exportAsText, generateText } from "@/utils";
10+
11+
type NoContributionsProps = {
12+
message: string;
13+
};
14+
15+
const NoContributions: FC<NoContributionsProps> = ({ message }) => (
16+
<div className="flex flex-col items-center justify-center">
17+
<p className="text-4xl p-2">📃</p>
18+
<p className="text-xl">{message}</p>
19+
</div>
20+
);
21+
22+
type FormatStatsRenderProps = {
23+
repositories: PullRequestContributionsByRepository[];
24+
format: RepositoryRenderFormat;
25+
};
26+
27+
export const FormatStatsRender: FC<FormatStatsRenderProps> = ({
28+
repositories,
29+
format,
30+
}) => {
31+
const renderContent = useMemo(() => {
32+
if (repositories?.length === 0) {
33+
return <NoContributions message="No Contributions" />;
34+
}
35+
36+
switch (format) {
37+
case "cards":
38+
return (
39+
<>
40+
<ExportDropdown />
41+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 pb-4">
42+
{repositories?.map(({ repository, contributions }, i) => (
43+
<RepositoryContributionsCard
44+
key={i + repository.name}
45+
repository={repository}
46+
contributions={contributions}
47+
/>
48+
))}
49+
</div>
50+
</>
51+
);
52+
case "json":
53+
return (
54+
<div>
55+
<button
56+
className="btn btn-primary p-2 m-1 rounded"
57+
onClick={() => exportAsJSON(repositories)}
58+
>
59+
Export as JSON
60+
</button>
61+
<div className="p-2 m-1 text-xs overflow-x-auto sm:text-sm md:text-base lg:text-lg">
62+
<pre>{JSON.stringify(repositories, null, 2)}</pre>
63+
</div>
64+
</div>
65+
);
66+
67+
case "text":
68+
return (
69+
<div>
70+
<button
71+
className="btn btn-primary p-2 m-1 rounded"
72+
onClick={() => exportAsText(generateText(repositories))}
73+
>
74+
Export as Text
75+
</button>
76+
<div className="p-2 m-1 text-xs overflow-x-auto sm:text-sm md:text-base lg:text-lg">
77+
<pre>{generateText(repositories)}</pre>
78+
</div>
79+
</div>
80+
);
81+
default:
82+
return <NoContributions message="Format is not matching any!" />;
83+
}
84+
}, [format, repositories]);
85+
86+
return renderContent;
87+
};

src/components/Header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const Header = () => {
1414
<div className="navbar-start">
1515
<div className="dropdown">
1616
<label
17-
for="menu"
17+
htmlFor="menu"
1818
tabIndex={0}
1919
className="btn btn-ghost lg:hidden"
2020
>

src/components/ReposFilters.tsx

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { FC } from "react";
2+
import { RepositoryRenderFormat } from "@/types/github";
3+
4+
type ReposFiltersProps = {
5+
searchQuery: string;
6+
setSearchQuery: React.Dispatch<React.SetStateAction<string>>;
7+
baseYear: number;
8+
year: number;
9+
setYear: React.Dispatch<React.SetStateAction<number>>;
10+
format: RepositoryRenderFormat;
11+
setFormat: React.Dispatch<React.SetStateAction<RepositoryRenderFormat>>;
12+
hideOwnRepo: boolean;
13+
setHideOwnRepo: React.Dispatch<React.SetStateAction<boolean>>;
14+
};
15+
16+
export const ReposFilters: FC<ReposFiltersProps> = ({
17+
searchQuery,
18+
setSearchQuery,
19+
baseYear,
20+
year,
21+
setYear,
22+
format,
23+
setFormat,
24+
hideOwnRepo,
25+
setHideOwnRepo,
26+
}) => {
27+
const YEARS_RANGE = 4;
28+
const FORMAT_OPTIONS = ["cards", "text", "json"] as const;
29+
30+
const handleYearChange = (selectedYear: number) => {
31+
setYear(selectedYear);
32+
};
33+
34+
const handleFormatChange = (selectedFormat: RepositoryRenderFormat) => {
35+
setFormat(selectedFormat);
36+
};
37+
38+
const handleHideOwnRepoChange = () => {
39+
setHideOwnRepo((prevHideOwnRepo) => !prevHideOwnRepo);
40+
};
41+
42+
return (
43+
<div className="flex justify-between sm:gap-0 sm:flex-row flex-col gap-3">
44+
<div className="sm:text-left text-center">
45+
<div>Select Year</div>
46+
<div className="join">
47+
{Array.from({ length: YEARS_RANGE }).map((_, i) => {
48+
const radioYear = baseYear - YEARS_RANGE + i + 1;
49+
return (
50+
<input
51+
key={i}
52+
className="join-item btn"
53+
type="radio"
54+
name="year"
55+
aria-label={radioYear.toString()}
56+
onChange={() => handleYearChange(radioYear)}
57+
checked={year === radioYear}
58+
/>
59+
);
60+
})}
61+
</div>
62+
<div className="my-5 flex flex-col sm:items-start items-center">
63+
<label className="mb-2">Search</label>
64+
<input
65+
type="text"
66+
placeholder="Type here"
67+
className="input input-bordered w-full max-w-md"
68+
value={searchQuery}
69+
onChange={(e) => setSearchQuery(e.target.value)}
70+
/>
71+
</div>
72+
73+
<div className="flex sm:items-start items-center justify-center md:justify-start">
74+
<input
75+
type="checkbox"
76+
name="hide-own-repo"
77+
checked={hideOwnRepo}
78+
onChange={handleHideOwnRepoChange}
79+
className="checkbox checkbox-sm checkbox-primary"
80+
/>
81+
<label className="ml-2">Hide own repositories</label>
82+
</div>
83+
</div>
84+
85+
<div className="sm:text-right text-center">
86+
<div>Select Format</div>
87+
<div className="join">
88+
{FORMAT_OPTIONS.map((formatOption: RepositoryRenderFormat) => (
89+
<input
90+
key={formatOption}
91+
className="join-item btn"
92+
type="radio"
93+
name="format"
94+
aria-label={formatOption}
95+
onChange={() => handleFormatChange(formatOption)}
96+
checked={format === formatOption}
97+
/>
98+
))}
99+
</div>
100+
</div>
101+
</div>
102+
);
103+
};

src/components/ThemeSelector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export function ThemeSelector() {
3131
<>
3232
<div className="dropdown dropdown-bottom dropdown-end">
3333
<label
34-
for="themeToggle"
34+
htmlFor="themeToggle"
3535
tabIndex={0}
3636
className="btn btn-circle btn-ghost m-1"
3737
data-testid="themeSelectorButton"

src/components/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
export * from "./ReposFilters";
2+
export * from "./ExportDropdown";
3+
export * from "./FormatStatsRender";
14
export * from "./RepositoryContributionsCard";
5+
export * from "./CardSkeleton";
26
export * from "./Header";
37
export * from "./RootLayout";

src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from "./useGitHubPullRequests";
22
export * from "./useGitHubQuery";
3+
export * from "./useFilteredRepositories";
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { useMemo } from "react";
2+
import { useSession } from "next-auth/react";
3+
import { PullRequestContributionsByRepository } from "@/types/github";
4+
5+
export const useFilteredRepositories = (
6+
repositories: PullRequestContributionsByRepository[],
7+
searchQuery: string,
8+
hideOwnRepo: boolean
9+
) => {
10+
const { data: session } = useSession();
11+
12+
const filteredRepositories = useMemo(() => {
13+
const filterOutOwnRepos = (
14+
repos: PullRequestContributionsByRepository[]
15+
) => {
16+
return repos?.filter(
17+
(repoData) => repoData.repository.owner.login !== session?.user.login
18+
);
19+
};
20+
21+
const filterReposBySearchQuery = (
22+
repos: PullRequestContributionsByRepository[]
23+
) => {
24+
const query = searchQuery.toLowerCase();
25+
return repos?.filter((repoData) =>
26+
repoData.repository.name.toLowerCase().includes(query)
27+
);
28+
};
29+
30+
const filterRepos = (repos: PullRequestContributionsByRepository[]) => {
31+
let filteredRepos = repos;
32+
if (!searchQuery) {
33+
filteredRepos = hideOwnRepo ? filterOutOwnRepos(repos) : repos;
34+
} else {
35+
const filteredReposBySearchQuery = filterReposBySearchQuery(repos);
36+
filteredRepos = hideOwnRepo
37+
? filterOutOwnRepos(filteredReposBySearchQuery)
38+
: filteredReposBySearchQuery;
39+
}
40+
return filteredRepos;
41+
};
42+
43+
return filterRepos(repositories);
44+
}, [repositories, searchQuery, hideOwnRepo, session]);
45+
46+
return filteredRepositories;
47+
};

src/pages/profile/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import {
66
import { useGitHubQuery } from "@/hooks";
77
import Image from "next/image";
88
import Link from "next/link";
9-
import { exportAsImage } from "@/utils";
109
import GitHubCalendar from "react-github-calendar";
1110
import { Tooltip as ReactTooltip } from "react-tooltip";
11+
import { exportAsImage } from "@/utils";
1212

1313
interface Activity {
1414
date: string;

0 commit comments

Comments
 (0)