Skip to content

Commit 9e31caf

Browse files
committed
ranking tiers explanation
1 parent 5fe8c13 commit 9e31caf

File tree

19 files changed

+485
-96
lines changed

19 files changed

+485
-96
lines changed

app/app.consts.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
// Must be 7 categories
2-
export const TIER_NAMES = ['Beginner', 'Adept', 'Advanced', 'Expert', 'Master', 'Elite', 'Legend'];
2+
export const TIER_NAMES = ['Beginner', 'Adept', 'Advanced', 'Expert', 'Master', 'Elite', 'Legend'] as const;
3+
// duplicated from api
4+
export const TIER_FRACTIONS = [0.5, 0.25, 0.13, 0.06, 0.03, 0.02, 0.01] as const;
5+
export const TOP_LEVEL_FRACTIONS = [0.5, 0.3, 0.15, 0.04, 0.01] as const;
36

47
const MIN_VALUE = 5;
58
const MIN_PROFILES_THRESHOLD = 100;
@@ -10,7 +13,7 @@ export const RANK_DESCRIPTIONS = {
1013
s: {
1114
title: 'Stars rank',
1215
descriptionList: `Rank is based on the total number of stars across repositories owned by a user.`,
13-
descriptionProfile: `Counts stars on repositories owned by the profile. The ranking includes only profiles that have at least one repository with ${MIN_VALUE} or more stars.`,
16+
descriptionProfile: `Counts stars on repositories owned by the profile. The ranking includes only profiles that have at least one repository with ${MIN_VALUE}+ stars.`,
1417
entityName: 'star',
1518
notRankedMessage: `A profile must own at least one repository with ${MIN_VALUE}+ stars to be ranked.`,
1619
},
@@ -19,12 +22,12 @@ export const RANK_DESCRIPTIONS = {
1922
descriptionList: 'Ranks count stars from repos where a developer has merged PRs — excluding their own.',
2023
descriptionProfile: `Counts stars on repos owned by others with merged PRs from this profile. Listed only if it has contributed to at least one repo with ${MIN_VALUE}+ stars.`,
2124
entityName: 'star',
22-
notRankedMessage: `To be ranked, you need a merged PR in a repository with ${MIN_VALUE}+ stars.`,
25+
notRankedMessage: `Profiles need a merged PR in a repo with ${MIN_VALUE}+ stars to be ranked.`,
2326
},
2427
f: {
2528
title: 'Followers rank',
2629
descriptionList: 'Rank is based on the number of followers the user has on GitHub.',
27-
descriptionProfile: `Counts users who follow this profile. The ranking includes only profiles that have at least ${MIN_VALUE} followers.`,
30+
descriptionProfile: `Counts users who follow this profile. The ranking includes only profiles that have ${MIN_VALUE}+ followers.`,
2831
entityName: 'follower',
2932
notRankedMessage: `A profile needs at least ${MIN_VALUE} followers to be ranked.`,
3033
},

app/profile/[login]/components/profile-chart-card.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import { FC, PropsWithChildren } from 'react';
1+
import { FC, PropsWithChildren, ReactNode } from 'react';
2+
3+
import { AdaptiveTooltip } from '@/components/adaptive-tooltip/adaptive-tooltip';
24

35
import { ProfileCard, ProfileCardContent } from './profile-card';
46

57
type ProfileChartDataSlotProps = {
68
title: string;
79
value: string | number;
8-
onClick?: () => void;
10+
tooltip: ReactNode;
911
};
1012

1113
export const ProfileChartCard: FC<PropsWithChildren> = ({ children }) => (
@@ -20,14 +22,16 @@ export const ProfileChartSlot: FC<PropsWithChildren> = ({ children }) => {
2022
return children;
2123
};
2224

23-
export const ProfileChartDataSlot: FC<ProfileChartDataSlotProps> = ({ title, value, onClick }) => {
25+
export const ProfileChartDataSlot: FC<ProfileChartDataSlotProps> = ({ title, value, tooltip }) => {
2426
return (
2527
<div className="flex flex-col justify-center items-center flex-grow m-4">
2628
<div>
2729
<div className="text-lg">{title}</div>
28-
<div className="text-2xl font-semibold" onClick={onClick}>
29-
{value}
30-
</div>
30+
<AdaptiveTooltip
31+
trigger={<div className="text-2xl font-semibold underline decoration-dotted underline-offset-4">{value}</div>}
32+
>
33+
{tooltip}
34+
</AdaptiveTooltip>
3135
</div>
3236
</div>
3337
);

app/profile/[login]/components/profile-charts.tsx

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,17 @@ import { PersonaType } from '@/types/persona.types';
88

99
import { ProfileCardsGrid } from './profile-card';
1010
import { ProfileChartCard, ProfileChartDataSlot, ProfileChartSlot } from './profile-chart-card';
11-
import { BestTierResult, ProfileTierType } from '../utils/calculate-tiers/calculate-tiers.types';
11+
import { BestTierResult } from '../utils/calculate-tiers/calculate-tiers.types';
1212

1313
type ProfileChartsProps = {
1414
rankChartTitle: string;
15-
tiers?: Tier[] | null;
16-
sTier: ProfileTierType;
17-
cTier: ProfileTierType;
18-
fTier: ProfileTierType;
15+
sTier?: Tier;
16+
cTier?: Tier;
17+
fTier?: Tier;
1918
bestTier?: BestTierResult | null;
2019
};
2120

22-
export const ProfileCharts: FC<ProfileChartsProps> = ({ rankChartTitle, tiers, sTier, cTier, fTier, bestTier }) => {
21+
export const ProfileCharts: FC<ProfileChartsProps> = ({ rankChartTitle, sTier, cTier, fTier, bestTier }) => {
2322
if (!bestTier?.data?.tier) {
2423
return null;
2524
}
@@ -28,18 +27,43 @@ export const ProfileCharts: FC<ProfileChartsProps> = ({ rankChartTitle, tiers, s
2827
<ProfileCardsGrid>
2928
<ProfileChartCard>
3029
<ProfileChartSlot>
31-
<RankChart progress={bestTier?.data} tiers={tiers} />
30+
<RankChart progress={bestTier?.data} />
3231
</ProfileChartSlot>
3332
<ProfileChartDataSlot
33+
tooltip={
34+
<div className="max-w-72">
35+
This is your <b>highest rank</b> among Stars, Contributor, and Followers rankings. Click on a rank in the{' '}
36+
<i>Ranks Breakdown</i> section to see detailed explanations of each ranking.
37+
</div>
38+
}
3439
title={rankChartTitle}
3540
value={`${TIER_NAMES[bestTier?.data?.tier - 1]} ${bestTier?.data?.level}`}
3641
/>
3742
</ProfileChartCard>
3843
<ProfileChartCard>
3944
<ProfileChartSlot>
40-
<PersonaChart sTier={sTier.data} cTier={cTier.data} fTier={fTier.data} />
45+
<PersonaChart sTier={sTier} cTier={cTier} fTier={fTier} />
4146
</ProfileChartSlot>
42-
<ProfileChartDataSlot title="Persona" value={`${bestTier?.source.map((t) => PersonaType[t]).join(', ')}`} />
47+
<ProfileChartDataSlot
48+
tooltip={
49+
<div className="max-w-72">
50+
The Persona reflects where the profile ranks best:
51+
<ul className="list-disc pl-4">
52+
<li>
53+
<b>Creator:</b> Top rank comes from Stars
54+
</li>
55+
<li>
56+
<b>Contributor:</b> Top rank is in Contributor
57+
</li>
58+
<li>
59+
<b>Influencer:</b> Top rank is in Followers
60+
</li>
61+
</ul>
62+
</div>
63+
}
64+
title="Persona"
65+
value={`${bestTier?.source.map((t) => PersonaType[t]).join(', ')}`}
66+
/>
4367
</ProfileChartCard>
4468
</ProfileCardsGrid>
4569
);
Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,15 @@
1-
'use client';
21
import { Info } from 'lucide-react';
32

4-
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
3+
import { AdaptiveTooltip } from '@/components/adaptive-tooltip/adaptive-tooltip';
54

65
export const RankBreakdownTooltip = () => {
76
return (
8-
<TooltipProvider>
9-
<Tooltip>
10-
<TooltipTrigger asChild>
11-
<Info size={20} />
12-
</TooltipTrigger>
13-
<TooltipContent>
14-
<p className="max-w-96">
15-
We make the most of the GitHub API. On average the top 20% of GitHub profiles are updated every 2 weeks,
16-
while all other profiles are refreshed every 3–5 weeks. You can also trigger a manual refresh at any time
17-
(sign-in required so we can use your token). Ranks are recalculated daily based on the data in the database.
18-
</p>
19-
</TooltipContent>
20-
</Tooltip>
21-
</TooltipProvider>
7+
<AdaptiveTooltip trigger={<Info size={20} />}>
8+
<p className="max-w-96">
9+
We make the most of the GitHub API. On average the top 20% of GitHub profiles are updated every 2 weeks, while
10+
all other profiles are refreshed every 3–5 weeks. You can also trigger a manual refresh at any time (sign-in
11+
required so we can use your token). Ranks are recalculated daily based on the data in the database.
12+
</p>
13+
</AdaptiveTooltip>
2214
);
2315
};

app/profile/[login]/components/tier-value.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import { FC } from 'react';
22

3-
import { TIER_NAMES } from '@/app/app.consts';
3+
import { RANK_DESCRIPTIONS, TIER_NAMES } from '@/app/app.consts';
4+
import { AdaptiveModal } from '@/components/adaptive-modal/adaptive-modal';
45
import { TagProvisional } from '@/components/tag-provisional/tag-provisional';
6+
import { TiersExplanation } from '@/components/tiers-explanation/tiers-explanation';
57
import { cn } from '@/lib/utils';
8+
import { Tier } from '@/types/generated/graphql';
9+
import { UserRankProps } from '@/types/ranking.types';
610

711
import { hasTierData } from '../utils/calculate-tiers/calculate-tiers';
812
import { ProfileTierType } from '../utils/calculate-tiers/calculate-tiers.types';
@@ -11,6 +15,9 @@ type TierValueProps = {
1115
tierData?: ProfileTierType;
1216
isProvisional?: boolean;
1317
className?: string;
18+
tiers?: Tier[];
19+
rankedCount?: number;
20+
rankType: UserRankProps;
1421
};
1522

1623
const getTierName = (tierData?: ProfileTierType): string => {
@@ -29,10 +36,26 @@ const getTierName = (tierData?: ProfileTierType): string => {
2936
return `${TIER_NAMES[tierData.data?.tier - 1]} ${tierData.data?.level}`;
3037
};
3138

32-
export const TierValue: FC<TierValueProps> = ({ tierData, className }) => {
39+
export const TierValue: FC<TierValueProps> = ({ tierData, tiers, rankedCount, rankType, className }) => {
40+
const getTierNameComponent = (className?: string) => <span className={className}>{getTierName(tierData)}</span>;
41+
const { title } = RANK_DESCRIPTIONS[rankType];
42+
3343
return (
3444
<div className={cn('text-2xl font-semibold flex gap-2 items-center', className)}>
35-
<span>{getTierName(tierData)}</span>
45+
{!tiers?.length && getTierNameComponent()}
46+
{!!tiers?.length && (
47+
<AdaptiveModal
48+
trigger={getTierNameComponent('underline decoration-dotted underline-offset-4 cursor-pointer')}
49+
title={`${title}ing cut-offs`}
50+
description={`Below is the detailed breakdown of tiers and levels for the ${title}ing. It shows the rank range and the minimum score needed to earn each tier and level, based on today’s ${(
51+
rankedCount || 0
52+
).toLocaleString(
53+
'en-US',
54+
)} tracked profiles. The minimum score needed to reach each tier or level depends on the specific ranking and is updated weekly on Mondays.`}
55+
>
56+
<TiersExplanation tiers={tiers} rankedCount={rankedCount} rankType={rankType} tierData={tierData?.data} />
57+
</AdaptiveModal>
58+
)}
3659
{tierData?.isProvisional && <TagProvisional />}
3760
</div>
3861
);

app/profile/[login]/country/page.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,9 @@ export default async function ProfileRanks({ params }: { params: Promise<{ login
5151
<ProfileRankingSwitcher login={login} ranking="country" />
5252
<ProfileCharts
5353
rankChartTitle={`Rank in ${user.country}`}
54-
tiers={rankTiers?.sTiers || rankTiers?.cTiers || rankTiers?.fTiers}
55-
sTier={sTier}
56-
cTier={cTier}
57-
fTier={fTier}
54+
sTier={sTier.data}
55+
cTier={cTier.data}
56+
fTier={fTier.data}
5857
bestTier={bestTier}
5958
/>
6059

@@ -64,6 +63,7 @@ export default async function ProfileRanks({ params }: { params: Promise<{ login
6463
</h2>
6564
<ProfileCardsGrid>
6665
<RankCard
66+
tiers={rankTiers?.sTiers}
6767
tierData={sTier}
6868
rankType="s"
6969
rank={s}
@@ -74,6 +74,7 @@ export default async function ProfileRanks({ params }: { params: Promise<{ login
7474
/>
7575

7676
<RankCard
77+
tiers={rankTiers?.cTiers}
7778
tierData={cTier}
7879
rankType="c"
7980
rank={c}
@@ -83,6 +84,7 @@ export default async function ProfileRanks({ params }: { params: Promise<{ login
8384
login={login}
8485
/>
8586
<RankCard
87+
tiers={rankTiers?.fTiers}
8688
tierData={fTier}
8789
rankType="f"
8890
rank={f}

app/profile/[login]/page.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ import { NotFound } from './not-found';
1616
import { calculateTiers } from './utils/calculate-tiers/calculate-tiers';
1717
import { RankCard } from '../../../components/rank-card/rank-card';
1818

19-
// DRAWERS FOR TIER DETAILS
2019
// PROVISIONAL TAGS
20+
// contributions padding and border
2121

22+
// badges - tiers
23+
// messenger bots - tiers
2224
export default async function ProfileRanks({ params }: { params: Promise<{ login: string }> }) {
2325
const { login } = await params;
2426
cacheLife('hours');
@@ -46,10 +48,9 @@ export default async function ProfileRanks({ params }: { params: Promise<{ login
4648
<ProfileRankingSwitcher login={login} ranking="global" />
4749
<ProfileCharts
4850
rankChartTitle="Global Rank"
49-
tiers={rankTiers?.sTiers || rankTiers?.cTiers || rankTiers?.fTiers}
50-
sTier={sTier}
51-
cTier={cTier}
52-
fTier={fTier}
51+
sTier={sTier.data}
52+
cTier={cTier.data}
53+
fTier={fTier.data}
5354
bestTier={bestTier}
5455
/>
5556

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
'use client';
2+
3+
import { FC, ReactNode, useState } from 'react';
4+
import { useMediaQuery } from 'usehooks-ts';
5+
6+
import { Button } from '@/components/ui/button';
7+
import {
8+
Dialog,
9+
DialogContent,
10+
DialogDescription,
11+
DialogHeader,
12+
DialogTitle,
13+
DialogTrigger,
14+
} from '@/components/ui/dialog';
15+
import {
16+
Drawer,
17+
DrawerClose,
18+
DrawerContent,
19+
DrawerFooter,
20+
DrawerHeader,
21+
DrawerTitle,
22+
DrawerTrigger,
23+
} from '@/components/ui/drawer';
24+
25+
type AdaptiveModalProps = {
26+
trigger: ReactNode;
27+
children: ReactNode;
28+
title?: string;
29+
description?: string;
30+
};
31+
32+
export const AdaptiveModal: FC<AdaptiveModalProps> = ({ trigger, children, title, description }) => {
33+
const [open, setOpen] = useState(false);
34+
const isDesktop = useMediaQuery('(min-width: 768px)');
35+
36+
if (isDesktop) {
37+
return (
38+
<Dialog open={open} onOpenChange={setOpen}>
39+
<DialogTrigger asChild>{trigger}</DialogTrigger>
40+
<DialogContent className="max-h-3/4 overflow-y-auto">
41+
<DialogHeader>
42+
<DialogTitle>{title}</DialogTitle>
43+
<DialogDescription>{description}</DialogDescription>
44+
</DialogHeader>
45+
{children}
46+
</DialogContent>
47+
</Dialog>
48+
);
49+
}
50+
51+
return (
52+
<Drawer open={open} onOpenChange={setOpen}>
53+
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
54+
<DrawerContent>
55+
<DrawerHeader>
56+
<DrawerTitle>{title}</DrawerTitle>
57+
</DrawerHeader>
58+
<div className="overflow-y-auto px-4 pb-4 flex flex-col gap-4">
59+
<div className="text-sm text-muted-foreground">{description}</div>
60+
{children}
61+
</div>
62+
<DrawerFooter>
63+
<DrawerClose asChild>
64+
<Button variant="outline">Close</Button>
65+
</DrawerClose>
66+
</DrawerFooter>
67+
</DrawerContent>
68+
</Drawer>
69+
);
70+
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use client';
2+
3+
import { ReactNode } from 'react';
4+
import { useMediaQuery } from 'usehooks-ts';
5+
6+
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
7+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
8+
9+
export const AdaptiveTooltip = ({ trigger, children }: { children: ReactNode; trigger: ReactNode }) => {
10+
const isDesktop = useMediaQuery('(min-width: 768px)');
11+
12+
if (isDesktop) {
13+
return (
14+
<TooltipProvider>
15+
<Tooltip>
16+
<TooltipTrigger asChild>{trigger}</TooltipTrigger>
17+
<TooltipContent className="text-sm">{children}</TooltipContent>
18+
</Tooltip>
19+
</TooltipProvider>
20+
);
21+
}
22+
23+
return (
24+
<Popover>
25+
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
26+
<PopoverContent className="text-sm w-auto">{children}</PopoverContent>
27+
</Popover>
28+
);
29+
};

0 commit comments

Comments
 (0)