feat: update language leaderboard calculation

This commit is contained in:
Khairul Hidayat 2024-08-09 21:45:14 +07:00
parent a5cd6383f3
commit 0142224f60
19 changed files with 330 additions and 59 deletions

View File

@ -1,3 +1,11 @@
CREATE TABLE `repository_languages` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`repo_id` integer NOT NULL,
`name` text NOT NULL,
`percentage` real NOT NULL,
FOREIGN KEY (`repo_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `repositories` ( CREATE TABLE `repositories` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`user_id` integer NOT NULL, `user_id` integer NOT NULL,
@ -7,7 +15,6 @@ CREATE TABLE `repositories` (
`stars` integer NOT NULL, `stars` integer NOT NULL,
`forks` integer NOT NULL, `forks` integer NOT NULL,
`last_update` text NOT NULL, `last_update` text NOT NULL,
`languages` text,
`contributors` text, `contributors` text,
`is_pending` integer DEFAULT false NOT NULL, `is_pending` integer DEFAULT false NOT NULL,
`is_error` integer DEFAULT false NOT NULL, `is_error` integer DEFAULT false NOT NULL,
@ -34,6 +41,7 @@ CREATE TABLE `users` (
`updated_at` text NOT NULL `updated_at` text NOT NULL
); );
--> statement-breakpoint --> statement-breakpoint
CREATE INDEX `repository_languages_name_idx` ON `repository_languages` (`name`);--> statement-breakpoint
CREATE INDEX `repositories_name_idx` ON `repositories` (`name`);--> statement-breakpoint CREATE INDEX `repositories_name_idx` ON `repositories` (`name`);--> statement-breakpoint
CREATE INDEX `repositories_uri_idx` ON `repositories` (`uri`);--> statement-breakpoint CREATE INDEX `repositories_uri_idx` ON `repositories` (`uri`);--> statement-breakpoint
CREATE INDEX `repositories_language_idx` ON `repositories` (`language`);--> statement-breakpoint CREATE INDEX `repositories_language_idx` ON `repositories` (`language`);--> statement-breakpoint

View File

@ -1,9 +1,68 @@
{ {
"version": "6", "version": "6",
"dialect": "sqlite", "dialect": "sqlite",
"id": "13d859a6-4c32-46a6-90e9-19740b2bb13a", "id": "a268ec23-239a-4d86-8830-2f3c1c2110df",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"tables": { "tables": {
"repository_languages": {
"name": "repository_languages",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"repo_id": {
"name": "repo_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"percentage": {
"name": "percentage",
"type": "real",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"repository_languages_name_idx": {
"name": "repository_languages_name_idx",
"columns": [
"name"
],
"isUnique": false
}
},
"foreignKeys": {
"repository_languages_repo_id_repositories_id_fk": {
"name": "repository_languages_repo_id_repositories_id_fk",
"tableFrom": "repository_languages",
"tableTo": "repositories",
"columnsFrom": [
"repo_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"repositories": { "repositories": {
"name": "repositories", "name": "repositories",
"columns": { "columns": {
@ -63,13 +122,6 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"languages": {
"name": "languages",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"contributors": { "contributors": {
"name": "contributors", "name": "contributors",
"type": "text", "type": "text",

View File

@ -5,8 +5,8 @@
{ {
"idx": 0, "idx": 0,
"version": "6", "version": "6",
"when": 1723210265266, "when": 1723211354217,
"tag": "0000_young_emma_frost", "tag": "0000_tough_jubilee",
"breakpoints": true "breakpoints": true
} }
] ]

34
server/import-user.ts Normal file
View File

@ -0,0 +1,34 @@
import db from "./db";
import queue from "./lib/queue";
import { users } from "./models";
const main = async () => {
const username = process.argv[2];
if (!username) {
throw new Error("Missing username");
}
const [user] = await db
.insert(users)
.values({ username, name: username })
.onConflictDoUpdate({
target: users.username,
set: { username },
})
.returning();
await queue.add(
"fetchUserProfile",
{ userId: user.id },
{ jobId: `fetchUserProfile:${user.id}` }
);
await queue.add(
"fetchUserRepos",
{ userId: user.id },
{ jobId: `fetchUserRepos:${user.id}` }
);
process.exit();
};
main();

View File

@ -37,15 +37,15 @@ export const calculateUserPoints = async (data: CalculateUserPointsType) => {
: 0; : 0;
// User repositories // User repositories
const repos = await db const repos = await db.query.repositories.findMany({
.select() where: eq(repositories.userId, user.id),
.from(repositories) with: { languages: true },
.where(eq(repositories.userId, user.id)); });
points += repos.length * weights.repositories; points += repos.length * weights.repositories;
// Languages known // Languages known
const languages = new Set( const languages = new Set(
repos.flatMap((i) => i.languages?.map((j) => j.lang)) repos.flatMap((i) => i.languages?.map((j) => j.name))
); );
points += languages.size * weights.languagesKnown; points += languages.size * weights.languagesKnown;

View File

@ -2,7 +2,8 @@ import db from "@server/db";
import github from "@server/lib/github"; import github from "@server/lib/github";
import queue from "@server/lib/queue"; import queue from "@server/lib/queue";
import { repositories } from "@server/models"; import { repositories } from "@server/models";
import { eq } from "drizzle-orm"; import { repoLanguages } from "@server/models/repo-languages";
import { and, eq, notInArray } from "drizzle-orm";
export type FetchRepoDataJobType = { export type FetchRepoDataJobType = {
id: number; id: number;
@ -10,17 +11,55 @@ export type FetchRepoDataJobType = {
}; };
export const fetchRepoData = async (data: FetchRepoDataJobType) => { export const fetchRepoData = async (data: FetchRepoDataJobType) => {
const details = await github.getRepoDetails(data.uri); const [repository] = await db
.select()
.from(repositories)
.where(eq(repositories.id, data.id));
const [result] = await db if (!repository) {
.update(repositories)
.set({ languages: details.languages })
.where(eq(repositories.id, data.id))
.returning();
if (!result) {
throw new Error("Repository not found!"); throw new Error("Repository not found!");
} }
await queue.add("calculateUserPoints", { userId: result.userId }); const details = await github.getRepoDetails(data.uri);
const { languages } = details;
await db.transaction(async (tx) => {
// Remove languages that don't exist anymore
const purgeLangFilter = and(
eq(repoLanguages.repoId, repository.id),
notInArray(
repoLanguages.name,
languages.map((i) => i.lang)
)
);
await tx.delete(repoLanguages).where(purgeLangFilter);
// Add or update languages
for (const lang of languages) {
const [existing] = await tx
.select({ id: repoLanguages.id })
.from(repoLanguages)
.where(
and(
eq(repoLanguages.repoId, repository.id),
eq(repoLanguages.name, lang.lang)
)
);
if (existing) {
await tx
.update(repoLanguages)
.set({ percentage: lang.amount })
.where(eq(repoLanguages.id, existing.id));
} else {
await tx.insert(repoLanguages).values({
repoId: repository.id,
name: lang.lang,
percentage: lang.amount,
});
}
}
});
await queue.add("calculateUserPoints", { userId: repository.userId });
}; };

View File

@ -19,6 +19,7 @@ export const fetchUserProfile = async (data: FetchUserProfileType) => {
.update(users) .update(users)
.set({ .set({
name: details.name, name: details.name,
avatar: details.avatar,
followers: details.followers, followers: details.followers,
following: details.following, following: details.following,
location: details.location, location: details.location,

View File

@ -8,6 +8,7 @@ const GITHUB_API_URL = "https://api.github.com";
const selectors = { const selectors = {
user: { user: {
name: "h1.vcard-names > span.vcard-fullname", name: "h1.vcard-names > span.vcard-fullname",
avatar: ".js-profile-editable-replace img.avatar-user",
location: "li[itemprop='homeLocation'] span", location: "li[itemprop='homeLocation'] span",
followers: ".js-profile-editable-area a[href$='?tab=followers'] > span", followers: ".js-profile-editable-area a[href$='?tab=followers'] > span",
following: ".js-profile-editable-area a[href$='?tab=following'] > span", following: ".js-profile-editable-area a[href$='?tab=following'] > span",
@ -30,6 +31,8 @@ const github = {
const $ = cheerio.load(response); const $ = cheerio.load(response);
const name = $(selectors.user.name).text().trim(); const name = $(selectors.user.name).text().trim();
const avatar = $(selectors.user.avatar).attr("src");
console.log({ avatar });
const location = $(selectors.user.location).text().trim(); const location = $(selectors.user.location).text().trim();
const followers = intval($(selectors.user.followers).text().trim()); const followers = intval($(selectors.user.followers).text().trim());
const following = intval($(selectors.user.following).text().trim()); const following = intval($(selectors.user.following).text().trim());
@ -43,6 +46,7 @@ const github = {
return { return {
name: name || username, name: name || username,
avatar,
username, username,
location, location,
followers, followers,

View File

@ -12,6 +12,9 @@ export const intval = (value: any) => {
export const getLanguageLogo = (language: string) => { export const getLanguageLogo = (language: string) => {
const alias: Record<string, string> = { const alias: Record<string, string> = {
gdscript: "godot", gdscript: "godot",
gap: "godot",
html: "html5",
css: "css3",
}; };
let lang = language.toLowerCase().replace(/[^a-zA-Z]/g, ""); let lang = language.toLowerCase().replace(/[^a-zA-Z]/g, "");

View File

@ -1,2 +1,3 @@
export { users } from "./users"; export { users } from "./users";
export { repositories } from "./repositories"; export { repositories, repositoriesRelations } from "./repositories";
export { repoLanguages, repoLanguagesRelations } from "./repo-languages";

View File

@ -0,0 +1,35 @@
import { InferInsertModel, InferSelectModel, relations } from "drizzle-orm";
import {
text,
sqliteTable,
integer,
index,
real,
} from "drizzle-orm/sqlite-core";
import { repositories } from "./repositories";
export const repoLanguages = sqliteTable(
"repository_languages",
{
id: integer("id").primaryKey({ autoIncrement: true }),
repoId: integer("repo_id")
.notNull()
.references(() => repositories.id),
name: text("name").notNull(),
percentage: real("percentage").notNull(),
},
(t) => ({
nameIdx: index("repository_languages_name_idx").on(t.name),
})
);
export const repoLanguagesRelations = relations(repoLanguages, ({ one }) => ({
repository: one(repositories, {
fields: [repoLanguages.repoId],
references: [repositories.id],
}),
}));
export type RepositoryLanguage = InferSelectModel<typeof repoLanguages>;
export type CreateRepositoryLanguage = InferInsertModel<typeof repoLanguages>;

View File

@ -1,7 +1,13 @@
import { Contributor, Language } from "@server/lib/github"; import { Contributor } from "@server/lib/github";
import { InferInsertModel, InferSelectModel, sql } from "drizzle-orm"; import {
InferInsertModel,
InferSelectModel,
relations,
sql,
} from "drizzle-orm";
import { text, sqliteTable, integer, index } from "drizzle-orm/sqlite-core"; import { text, sqliteTable, integer, index } from "drizzle-orm/sqlite-core";
import { users } from "./users"; import { users } from "./users";
import { repoLanguages } from "./repo-languages";
export const repositories = sqliteTable( export const repositories = sqliteTable(
"repositories", "repositories",
@ -17,7 +23,6 @@ export const repositories = sqliteTable(
stars: integer("stars").notNull(), stars: integer("stars").notNull(),
forks: integer("forks").notNull(), forks: integer("forks").notNull(),
lastUpdate: text("last_update").notNull(), lastUpdate: text("last_update").notNull(),
languages: text("languages", { mode: "json" }).$type<Language[]>(),
contributors: text("contributors", { mode: "json" }).$type<Contributor[]>(), contributors: text("contributors", { mode: "json" }).$type<Contributor[]>(),
isPending: integer("is_pending", { mode: "boolean" }) isPending: integer("is_pending", { mode: "boolean" })
@ -39,5 +44,9 @@ export const repositories = sqliteTable(
}) })
); );
export const repositoriesRelations = relations(repositories, ({ many }) => ({
languages: many(repoLanguages),
}));
export type Repository = InferSelectModel<typeof repositories>; export type Repository = InferSelectModel<typeof repositories>;
export type CreateRepository = InferInsertModel<typeof repositories>; export type CreateRepository = InferInsertModel<typeof repositories>;

View File

@ -22,7 +22,7 @@ const onJobRetriesExhausted = async (job: Job) => {
const worker = new Worker(BULLMQ_JOB_NAME, handler, { const worker = new Worker(BULLMQ_JOB_NAME, handler, {
connection: BULLMQ_CONNECTION, connection: BULLMQ_CONNECTION,
concurrency: Number(import.meta.env.QUEUE_CONCURRENCY) || 1, concurrency: Number(import.meta.env.QUEUE_CONCURRENCY) || 10,
removeOnComplete: { count: 0 }, removeOnComplete: { count: 0 },
removeOnFail: { count: 0 }, removeOnFail: { count: 0 },
}); });

View File

@ -1,11 +1,11 @@
import db from "@server/db"; import db from "@server/db";
import { getLanguageLogo } from "@server/lib/utils"; import { getLanguageLogo } from "@server/lib/utils";
import { repositories, users } from "@server/models"; import { repoLanguages, repositories, users } from "@server/models";
import { count, desc, eq, getTableColumns, inArray, sql } from "drizzle-orm"; import { count, desc, eq, getTableColumns, inArray, sql } from "drizzle-orm";
import { HTTPException } from "hono/http-exception"; import { HTTPException } from "hono/http-exception";
export type GetLeaderboardRes = { export type GetLeaderboardRes = {
columns: { title: string; selector: string }[]; columns: { title: string; selector: string; hideOnSmall?: boolean }[];
rows: { rows: {
id?: string | null; id?: string | null;
rank: number; rank: number;
@ -48,12 +48,18 @@ export class LeaderboardRepository {
async getTopLanguages() { async getTopLanguages() {
const data = await db const data = await db
.select({ .select({
language: repositories.language, language: repoLanguages.name,
count: count(), count: count(),
total: sql<number>`sum(${repoLanguages.percentage})`.as("total"),
avg: sql<number>`avg(${repoLanguages.percentage})`.as("avg"),
coverage: sql<number>`count(*) * avg(${repoLanguages.percentage})`.as(
"coverage"
),
}) })
.from(repositories) .from(repositories)
.groupBy(repositories.language) .innerJoin(repoLanguages, eq(repoLanguages.repoId, repositories.id))
.orderBy(desc(count())); .groupBy(repoLanguages.name)
.orderBy(desc(sql`count(*) * avg(${repoLanguages.percentage})`));
const columns = [ const columns = [
{ {
@ -62,8 +68,21 @@ export class LeaderboardRepository {
}, },
{ {
title: "Total Repo", title: "Total Repo",
selector: "repo",
},
{
title: "Rerata",
selector: "avg",
hideOnSmall: true,
},
{
title: "Cakupan",
selector: "sub", selector: "sub",
}, },
// {
// title: "Total Persentase",
// selector: "total",
// },
]; ];
const rows = data const rows = data
@ -71,8 +90,11 @@ export class LeaderboardRepository {
.map((data, idx) => ({ .map((data, idx) => ({
rank: idx + 1, rank: idx + 1,
name: data.language, name: data.language,
sub: `${data.count} repo`, sub: data.coverage.toFixed(0),
image: getLanguageLogo(data.language), image: getLanguageLogo(data.language),
repo: `${data.count} repo`,
total: data.total,
avg: data.avg.toFixed(1),
})); }));
return { columns, rows } satisfies GetLeaderboardRes; return { columns, rows } satisfies GetLeaderboardRes;
@ -91,7 +113,8 @@ export class LeaderboardRepository {
}) })
.from(repositories) .from(repositories)
.innerJoin(users, eq(users.id, repositories.userId)) .innerJoin(users, eq(users.id, repositories.userId))
.where(inArray(sql`lower(${repositories.language})`, languages)) .innerJoin(repoLanguages, eq(repoLanguages.repoId, repositories.id))
.where(inArray(sql`lower(${repoLanguages.name})`, languages))
.groupBy(users.id) .groupBy(users.id)
.orderBy(desc(count())); .orderBy(desc(count()));
@ -141,18 +164,18 @@ export class LeaderboardRepository {
throw new HTTPException(404, { message: "User not found!" }); throw new HTTPException(404, { message: "User not found!" });
} }
const repos = await db const repos = await db.query.repositories.findMany({
.select() where: eq(repositories.userId, user.id),
.from(repositories) orderBy: [desc(repositories.stars), desc(repositories.forks)],
.where(eq(repositories.userId, user.id)) with: { languages: true },
.orderBy(desc(repositories.stars), desc(repositories.forks)); });
const languageMap: Record<string, number> = {}; const languageMap: Record<string, number> = {};
repos repos
.flatMap((i) => i.languages || []) .flatMap((i) => i.languages || [])
.forEach((i) => { .forEach((i) => {
if (!languageMap[i.lang]) languageMap[i.lang] = 0; if (!languageMap[i.name]) languageMap[i.name] = 0;
languageMap[i.lang] += i.amount; languageMap[i.name] += i.percentage;
}); });
const totalLangWeight = Object.values(languageMap).reduce( const totalLangWeight = Object.values(languageMap).reduce(

View File

@ -0,0 +1,19 @@
import React, { useState } from "react";
type Props = React.ComponentPropsWithoutRef<"img"> & {
fallback?: string;
};
const Image = ({ src, fallback, ...props }: Props) => {
const [isError, setError] = useState(false);
return (
<img
{...props}
src={isError && (fallback || !src) ? fallback : src}
onError={() => setError(true)}
/>
);
};
export default Image;

View File

@ -49,7 +49,8 @@ const RankListItem = ({
"flex-1 truncate", "flex-1 truncate",
idx === 0 && data.image idx === 0 && data.image
? "flex flex-row items-center gap-x-2" ? "flex flex-row items-center gap-x-2"
: "" : "",
col.hideOnSmall && "hidden md:flex"
)} )}
> >
{idx === 0 && data.image ? ( {idx === 0 && data.image ? (
@ -78,7 +79,10 @@ export const RankListHeader = ({ columns }: RankListHeaderProps) => {
<div className="flex flex-row items-center gap-x-2 w-full text-sm bg-base-300 text-base-content/80 p-4 py-2 mt-2 sticky z-[2] top-0 rounded-lg"> <div className="flex flex-row items-center gap-x-2 w-full text-sm bg-base-300 text-base-content/80 p-4 py-2 mt-2 sticky z-[2] top-0 rounded-lg">
<div className="w-10" /> <div className="w-10" />
{columns.map((col, idx) => ( {columns.map((col, idx) => (
<p key={idx} className="flex-1"> <p
key={idx}
className={cn("flex-1", col.hideOnSmall && "hidden md:flex")}
>
{col.title} {col.title}
</p> </p>
))} ))}

View File

@ -22,7 +22,7 @@ const UserRank = () => {
if (!data) { if (!data) {
return ( return (
<div className="bg-base-100 mx-4 rounded-lg px-6 py-4 sticky bottom-4 z-[2]"> <div className="bg-base-100 mx-4 mt-4 rounded-lg px-6 py-4 sticky bottom-4 z-[2]">
<p>Pengen nama kamu masuk list ini juga?</p> <p>Pengen nama kamu masuk list ini juga?</p>
<p className="inline">Hayuk</p> <p className="inline">Hayuk</p>
<Button size="sm" color="primary" className="mx-2" onClick={onLogin}> <Button size="sm" color="primary" className="mx-2" onClick={onLogin}>

View File

@ -3,7 +3,7 @@ import BottomSheet, {
BottomSheetTitle, BottomSheetTitle,
} from "@client/components/ui/bottom-sheet"; } from "@client/components/ui/bottom-sheet";
import { memo, useEffect, useMemo } from "react"; import { memo, useEffect, useMemo } from "react";
import { Avatar, Badge, Card, Progress } from "react-daisyui"; import { Avatar, Badge, Card, Dropdown, Progress } from "react-daisyui";
import { useGetUserLeaderboard } from "../hooks"; import { useGetUserLeaderboard } from "../hooks";
import { dummyAvatar } from "@client/lib/utils"; import { dummyAvatar } from "@client/lib/utils";
import { FiGitMerge, FiStar, FiType, FiUsers } from "react-icons/fi"; import { FiGitMerge, FiStar, FiType, FiUsers } from "react-icons/fi";
@ -11,6 +11,7 @@ import { FaCode, FaRegStar, FaTrophy } from "react-icons/fa";
import { LuFolderGit } from "react-icons/lu"; import { LuFolderGit } from "react-icons/lu";
import { IoMdGitBranch, IoMdGitCommit } from "react-icons/io"; import { IoMdGitBranch, IoMdGitCommit } from "react-icons/io";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { pointWeights } from "../data";
const ViewSheet = () => { const ViewSheet = () => {
const { type, id } = useParams(); const { type, id } = useParams();
@ -25,8 +26,6 @@ const ViewSheet = () => {
return data?.repositories.filter((i) => i.isPending).length || 0; return data?.repositories.filter((i) => i.isPending).length || 0;
}, [data]); }, [data]);
console.log({ pendingRepos, totalRepo });
useEffect(() => { useEffect(() => {
if (!pendingRepos) { if (!pendingRepos) {
return; return;
@ -115,13 +114,40 @@ const ViewSheet = () => {
</p> </p>
</div> </div>
<div className="bg-neutral text-neutral-content px-6 py-3 w-full md:w-auto rounded-lg"> <Dropdown className="dropdown-end w-full md:w-auto">
<div className="flex flex-row items-center justify-center font-mono gap-2 text-4xl md:text-3xl text-primary"> <Dropdown.Toggle button={false}>
<FaTrophy size={24} /> <button className="bg-neutral hover:bg-neutral/80 active:opacity-50 text-neutral-content px-6 py-3 w-full rounded-lg">
<p>{data?.user.rank}</p> <div className="flex flex-row items-center justify-center font-mono gap-2 text-4xl md:text-3xl text-primary">
</div> <FaTrophy size={24} />
<p className="text-xs">{data?.user.points + " pts"}</p> <p>{data?.user.rank}</p>
</div> </div>
<p className="text-xs">{data?.user.points + " pts"}</p>
</button>
</Dropdown.Toggle>
<Dropdown.Menu className="card card-compact w-64 p-2 z-10 shadow-lg bg-base-300 text-base-content my-2 text-left">
<Card.Body>
<Card.Title tag="h3" className="text-base">
Perhitungan Point
</Card.Title>
<pre className="font-mono whitespace-break-spaces">
{JSON.stringify(pointWeights, null, 2)}
</pre>
<p>
Cek lebih lengkap{" "}
<a
href="https://github.com/khairul169/github-leaderboard/blob/main/server/jobs/calculate-user-points.ts"
target="_blank"
className="link"
>
disini
</a>
.
</p>
</Card.Body>
</Dropdown.Menu>
</Dropdown>
</div> </div>
{data && pendingRepos > 0 && ( {data && pendingRepos > 0 && (
@ -202,7 +228,7 @@ const ViewSheet = () => {
<div className="flex-1 truncate"> <div className="flex-1 truncate">
<p className="mt-1 truncate">{item.name}</p> <p className="mt-1 truncate">{item.name}</p>
<p className="text-xs truncate mt-0.5 text-base-content/80"> <p className="text-xs truncate mt-0.5 text-base-content/80">
{item.languages?.map((i) => i.lang).join(", ") || {item.languages?.map((i) => i.name).join(", ") ||
item.language} item.language}
</p> </p>

View File

@ -28,3 +28,16 @@ export type LeaderboardType = {
value: LeaderboardTypes; value: LeaderboardTypes;
query?: any; query?: any;
}; };
export const pointWeights = {
followers: 20,
following: 10,
achievements: 100,
repositories: 1,
contributorsAmount: 25,
stars: 10,
forks: 10,
languagesKnown: 50,
commits: 1,
lineOfCodes: 0.01,
};