Skip to content

Commit

Permalink
feat: add most used languages graph (#2158)
Browse files Browse the repository at this point in the history
  • Loading branch information
nickytonline authored Nov 17, 2023
2 parents 09ec0ac + f974d95 commit c79381b
Show file tree
Hide file tree
Showing 9 changed files with 259 additions and 53 deletions.
134 changes: 134 additions & 0 deletions components/Graphs/MostUsedLanguagesGraph/most-used-languages-graph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { BsFillCircleFill } from "react-icons/bs";
import Skeleton from "react-loading-skeleton";
import { useEffect, useRef, useState } from "react";
import Card from "components/atoms/Card/card";
import Text from "components/atoms/Typography/text";
import { ContributorType, ContributorTypeFilter } from "../shared/contributor-type-filter";

export interface MostUsedLanguagesGraphProps {
data: {
mainLanguage: string;
data: {
name: string;
value: number;
}[];
};
setContributorType: (type: ContributorType) => void;
contributorType: ContributorType;
isLoading?: boolean;
}

export const MostUsedLanguagesGraph = ({
data,
setContributorType,
contributorType,
isLoading = false,
}: MostUsedLanguagesGraphProps) => {
const colors = [
"hsl(53, 91%, 59%)",
"hsl(204, 100%, 40%)",
"hsl(14, 98%, 49%)",
"hsl(267, 36%, 37%)",
"hsl(17, 100%, 50%)",
];
const percentage = 10;
const { data: languages = [], mainLanguage } = data;
const lastItem = languages.length > 0 ? languages.length - 1 : 0;
const sortedLanguages = languages.sort((a, b) => b.value - a.value);
const languagesRef = useRef<HTMLUListElement>(null);
const [language, setLanguage] = useState<string | null>();

useEffect(() => {
if (language) {
const languageElement = languagesRef.current?.querySelector(`[data-language="${language}"]`);
if (languageElement) {
languageElement.classList.add("font-semibold");
}
}
}, [language]);

return (
<Card className="p-5">
<div className="flex flex-col gap-6">
<div>
<h2 className="pb-1 font-medium text-lg tracking-tight">Most used languages</h2>
<Text>
{mainLanguage} contributions have been growing on average {percentage}% MoM
</Text>
</div>
<div className="w-max">
<ContributorTypeFilter setContributorType={setContributorType} contributorType={contributorType} />
</div>

<div className="flex h-3 place-content-center">
{isLoading ? (
<div className="loading rounded-lg w-max" style={{ width: "100%" }}>
<span className="sr-only">loading most used languages graph</span>
</div>
) : (
<>
{sortedLanguages.length > 0 ? (
sortedLanguages.map((item, index) => {
return (
<button
aria-label={`${item.name} is ${item.value}% of the most used languages for contributors in your list`}
key={item.name}
data-language={item.name}
className={`${index === 0 ? "rounded-l-lg" : ""} ${
index === lastItem ? "rounded-r-lg" : ""
} transform hover:scale-110 transition-transform hover:z-10`}
style={{ backgroundColor: colors[index], width: `${item.value}%` }}
onMouseOver={(event) => {
const { language } = event.currentTarget.dataset;
setLanguage(language);
}}
onMouseOut={(event) => {
setLanguage(null);
}}
onFocus={(event) => {
const { language } = event.currentTarget.dataset;
setLanguage(language);
}}
onBlur={(event) => {
setLanguage(null);
}}
/>
);
})
) : (
<div className="rounded-lg bg-slate-100 w-full" />
)}
</>
)}
</div>

{isLoading ? (
<Skeleton height={24} count={5} className="mt-4 mb-4" />
) : (
<ul ref={languagesRef} className="grid grid-cols-1 content-center">
{sortedLanguages.length > 0 ? (
sortedLanguages.map((item, index) => (
<li
key={item.name}
className={`flex justify-between pt-4 pb-4 ${
index === lastItem ? "" : "border-b-1 border-slate-100"
} ${language === item.name ? "font-semibold" : ""}`}
>
<span
className={`flex gap-2 items-center ${language === item.name ? "text-black" : "text-slate-700"}`}
>
<BsFillCircleFill size={11} style={{ fill: colors[index] }} />
{item.name}
</span>
<span className={`${language === item.name ? "text-black" : "text-slate-600"}`}>{item.value}%</span>
</li>
))
) : (
<p className="text-center">There is no language data</p>
)}
</ul>
)}
</div>
</Card>
);
};
50 changes: 50 additions & 0 deletions components/Graphs/shared/contributor-type-filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Button from "components/atoms/Button/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "components/atoms/Dropdown/dropdown";
import PeopleIcon from "img/icons/people.svg";
import ChevronDownIcon from "img/chevron-down.svg";
import SVGIcon from "components/atoms/SVGIcon/svg-icon";
import Icon from "components/atoms/Icon/icon";

interface ContributorTypeFilterProps {
setContributorType: (type: ContributorType) => void;
contributorType: ContributorType;
}

export type ContributorType = "all" | "active" | "new" | "alumni";

const peopleFilters: Record<ContributorType, string> = {
all: "All Contributors",
active: "Active Contributors",
new: "New Contributors",
alumni: "Alumni Contributors",
};

export const ContributorTypeFilter = ({ setContributorType, contributorType }: ContributorTypeFilterProps) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="default" className="items-center gap-1">
<SVGIcon IconImage={`${PeopleIcon.src}#icon`} className="w-4 h-4" />
{peopleFilters[contributorType]}
<Icon IconImage={ChevronDownIcon} className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="flex flex-col gap-2">
{Object.entries(peopleFilters).map(([key, value]) => (
<DropdownMenuItem
key={key}
className="rounded-md !cursor-pointer"
onClick={() => setContributorType(key as keyof typeof peopleFilters)}
>
{value}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,10 @@ import { useGesture } from "@use-gesture/react";

import { ReactNode, useState } from "react";
import * as RawTooltip from "@radix-ui/react-tooltip";
import Button from "components/atoms/Button/button";
import Card from "components/atoms/Card/card";
import Icon from "components/atoms/Icon/icon";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "components/atoms/Dropdown/dropdown";
import PeopleIcon from "img/icons/people.svg";
import ChevronDownIcon from "img/chevron-down.svg";
import SVGIcon from "components/atoms/SVGIcon/svg-icon";

import Tooltip from "components/atoms/Tooltip/tooltip";
import { ContributorType, ContributorTypeFilter } from "components/Graphs/shared/contributor-type-filter";
import AvatarHoverCard from "components/atoms/Avatar/avatar-hover-card";

// omit total_contributions and login from ContributorStat
Expand All @@ -40,8 +31,6 @@ export interface ContributorStat {
total_contributions: number;
}

export type ContributorType = "all" | "active" | "new" | "alumni";

interface Props {
topContributor?: ContributorStat;
data: ContributorStat[];
Expand All @@ -51,13 +40,6 @@ interface Props {
totalContributions: number;
}

const peopleFilters: Record<ContributorType, string> = {
all: "All Contributors",
active: "Active Contributors",
new: "New Contributors",
alumni: "Alumni Contributors",
};

const LegendItem = ({ color, title }: { color?: string; title: string }) => {
return (
<div className="flex items-center gap-2">
Expand Down Expand Up @@ -136,26 +118,7 @@ export default function MostActiveContributorsCard({

{/* buttons */}
<div className="flex gap-1 mb-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="default" className="items-center gap-1">
<SVGIcon IconImage={`${PeopleIcon.src}#icon`} className="w-4 h-4" />
{peopleFilters[contributorType]}
<Icon IconImage={ChevronDownIcon} className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="flex flex-col gap-2">
{Object.entries(peopleFilters).map(([key, value]) => (
<DropdownMenuItem
key={key}
className="rounded-md !cursor-pointer"
onClick={() => setContributorType(key as keyof typeof peopleFilters)}
>
{value}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<ContributorTypeFilter contributorType={contributorType} setContributorType={setContributorType} />
</div>
{/* chart */}
<div className="relative grid place-content-stretch overflow-y-hidden mb-4">
Expand Down
2 changes: 1 addition & 1 deletion lib/hooks/api/useContributionsByEvolutionType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { useState } from "react";
import useSWR, { Fetcher } from "swr";

import publicApiFetcher from "lib/utils/public-api-fetcher";
import { ContributorType } from "components/molecules/MostActiveContributorsCard/most-active-contributors-card";
import { ContributionEvolutionByTypeDatum } from "components/molecules/ContributionsEvolutionByTypeCard/contributions-evolution-by-type-card";
import { ContributorType } from "components/Graphs/shared/contributor-type-filter";

/**
* Fetch most active contributors from a list.
Expand Down
6 changes: 2 additions & 4 deletions lib/hooks/api/useMostActiveContributors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ import useSWR, { Fetcher } from "swr";
import { useRouter } from "next/router";

import publicApiFetcher from "lib/utils/public-api-fetcher";
import {
ContributorStat,
ContributorType,
} from "components/molecules/MostActiveContributorsCard/most-active-contributors-card";
import { ContributorStat } from "components/molecules/MostActiveContributorsCard/most-active-contributors-card";
import { ContributorType } from "components/Graphs/shared/contributor-type-filter";

interface PaginatedResponse {
readonly data: ContributorStat[];
Expand Down
63 changes: 63 additions & 0 deletions stories/graphs/most-used-languages-graph.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { MostUsedLanguagesGraph } from "components/Graphs/MostUsedLanguagesGraph/most-used-languages-graph";
import type { Meta, StoryObj } from "@storybook/react";

type MetaData = Meta<typeof MostUsedLanguagesGraph>;

const meta: MetaData = {
title: "Components/Graphs/Most Used Languages Graph",
component: MostUsedLanguagesGraph,
};

export default meta;
type Story = StoryObj<typeof MostUsedLanguagesGraph>;

function generateData() {
return {
mainLanguage: "TypeScript",
data: [
{
name: "Python",
value: 10,
},
{
name: "TypeScript",
value: 25,
},
{
name: "JavaScript",
value: 20,
},
{
name: "C++",
value: 15,
},
{
name: "Zig",
value: 30,
},
],
};
}

export const Default: Story = {
args: {
data: generateData(),
contributorType: "all",
setContributorType(type) {
// eslint-disable-next-line no-console
console.log(type);
},
},
};

export const Loading: Story = {
args: {
isLoading: true,
data: generateData(),
contributorType: "all",
setContributorType(type) {
// eslint-disable-next-line no-console
console.log(type);
},
},
};
6 changes: 2 additions & 4 deletions stories/molecules/most-active-contributors-card.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Meta, StoryObj } from "@storybook/react";
import { Meta } from "@storybook/react";
import { useEffect, useState } from "react";
import Button from "components/atoms/Button/button";
import MostActiveContributorsCard, {
ContributorStat,
ContributorType,
} from "components/molecules/MostActiveContributorsCard/most-active-contributors-card";
import { ContributorType } from "components/Graphs/shared/contributor-type-filter";

const meta = {
title: "Design System/Molecules/Most Active Contributors Card",
Expand All @@ -22,8 +22,6 @@ const meta = {

export default meta;

type Story = StoryObj;

export const Default = () => {
const [data, setData] = useState<any>(generateData());

Expand Down
5 changes: 1 addition & 4 deletions styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,8 @@
src: url("../public/assets/fonts/Inter-VariableFont_slnt,wght.ttf");
}

* {
@apply font-normal;
}

body {
@apply font-normal;
background-color: #f8f9fa;
}

Expand Down
3 changes: 3 additions & 0 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,9 @@ module.exports = {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
},
borderWidth: {
1: "1px",
},
},
},
safelist: [
Expand Down

0 comments on commit c79381b

Please sign in to comment.