Skip to content

Commit 5a3638c

Browse files
add base for star history
1 parent 6fad883 commit 5a3638c

File tree

6 files changed

+186
-40
lines changed

6 files changed

+186
-40
lines changed

app/components/Header.tsx

Lines changed: 45 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { NavLink } from "@remix-run/react"
1+
import { NavLink } from "@remix-run/react";
22
import {
33
BlocksIcon,
44
BracesIcon,
@@ -11,59 +11,68 @@ import {
1111
SmilePlusIcon,
1212
TagIcon,
1313
XIcon,
14-
} from "lucide-react"
15-
import { type HTMLAttributes, useEffect, useState } from "react"
16-
import { ClientOnly } from "remix-utils/client-only"
17-
import useSWR from "swr"
18-
import { GITHUB_URL, SWR_CONFIG } from "~/utils/constants"
19-
import { cx } from "~/utils/cva"
20-
import { fetcher } from "~/utils/fetchers"
21-
import { getRepoOwnerAndName } from "~/utils/github"
22-
import { Badge } from "./Badge"
23-
import { Breadcrumbs } from "./Breadcrumbs"
24-
import { Button } from "./Button"
14+
} from "lucide-react";
15+
import { type HTMLAttributes, useEffect, useState } from "react";
16+
import { ClientOnly } from "remix-utils/client-only";
17+
import useSWR from "swr";
18+
import { GITHUB_URL, SWR_CONFIG } from "~/utils/constants";
19+
import { cx } from "~/utils/cva";
20+
import { fetcher } from "~/utils/fetchers";
21+
import { getRepoOwnerAndName } from "~/utils/github";
22+
import { Badge } from "./Badge";
23+
import { Breadcrumbs } from "./Breadcrumbs";
24+
import { Button } from "./Button";
2525
import {
2626
DropdownMenu,
2727
DropdownMenuContent,
2828
DropdownMenuItem,
2929
DropdownMenuTrigger,
30-
} from "./DropdownMenu"
31-
import { NavigationLink, navigationLinkVariants } from "./NavigationLink"
32-
import { Ping } from "./Ping"
33-
import { Series } from "./Series"
34-
import { ThemeSwitcher } from "./ThemeSwitcher"
30+
} from "./DropdownMenu";
31+
import { NavigationLink, navigationLinkVariants } from "./NavigationLink";
32+
import { Ping } from "./Ping";
33+
import { Series } from "./Series";
34+
import { ThemeSwitcher } from "./ThemeSwitcher";
3535

36-
export const Header = ({ className, ...props }: HTMLAttributes<HTMLElement>) => {
37-
const [isNavOpen, setNavOpen] = useState(false)
38-
const repo = getRepoOwnerAndName(GITHUB_URL)
39-
const formatter = new Intl.NumberFormat("en-US", { notation: "compact" })
36+
export const Header = ({
37+
className,
38+
...props
39+
}: HTMLAttributes<HTMLElement>) => {
40+
const [isNavOpen, setNavOpen] = useState(false);
41+
const repo = getRepoOwnerAndName(GITHUB_URL);
42+
const formatter = new Intl.NumberFormat("en-US", { notation: "compact" });
4043

4144
// Close the mobile navigation when the user presses the "Escape" key
4245
useEffect(() => {
4346
const onKeyDown = (e: KeyboardEvent) => {
44-
if (e.key === "Escape") setNavOpen(false)
45-
}
47+
if (e.key === "Escape") setNavOpen(false);
48+
};
4649

47-
document.addEventListener("keydown", onKeyDown)
48-
return () => document.removeEventListener("keydown", onKeyDown)
49-
}, [])
50+
document.addEventListener("keydown", onKeyDown);
51+
return () => document.removeEventListener("keydown", onKeyDown);
52+
}, []);
5053

5154
const { data, error, isLoading } = useSWR<number>(
5255
{ url: "/api/fetch-repository-stars", ...repo },
5356
fetcher,
54-
SWR_CONFIG,
55-
)
57+
SWR_CONFIG
58+
);
5659

5760
return (
5861
<div
5962
className={cx(
6063
"sticky top-0 z-10 flex flex-wrap items-center py-3 -my-3 gap-3 backdrop-blur-sm bg-background/95 md:gap-4",
61-
className,
64+
className
6265
)}
6366
{...props}
6467
>
65-
<button type="button" onClick={() => setNavOpen(!isNavOpen)} className="lg:hidden">
66-
<MenuIcon className={cx("size-6 stroke-[1.5]", isNavOpen && "hidden")} />
68+
<button
69+
type="button"
70+
onClick={() => setNavOpen(!isNavOpen)}
71+
className="lg:hidden"
72+
>
73+
<MenuIcon
74+
className={cx("size-6 stroke-[1.5]", isNavOpen && "hidden")}
75+
/>
6776
<XIcon className={cx("size-6 stroke-[1.5]", !isNavOpen && "hidden")} />
6877
<span className="sr-only">Toggle navigation</span>
6978
</button>
@@ -72,7 +81,9 @@ export const Header = ({ className, ...props }: HTMLAttributes<HTMLElement>) =>
7281

7382
<nav className="contents max-lg:hidden">
7483
<DropdownMenu>
75-
<DropdownMenuTrigger className={cx(navigationLinkVariants({ className: "gap-1" }))}>
84+
<DropdownMenuTrigger
85+
className={cx(navigationLinkVariants({ className: "gap-1" }))}
86+
>
7687
Browse <ChevronDownIcon />
7788
</DropdownMenuTrigger>
7889

@@ -160,5 +171,5 @@ export const Header = ({ className, ...props }: HTMLAttributes<HTMLElement>) =>
160171
</nav>
161172
)}
162173
</div>
163-
)
164-
}
174+
);
175+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { LoaderFunctionArgs } from "@remix-run/node"
2+
import { z } from "zod"
3+
import { getAllGithubStars } from "~/utils/github"
4+
5+
export const loader = async ({ request }: LoaderFunctionArgs) => {
6+
const url = new URL(request.url)
7+
const searchParams = Object.fromEntries(url.searchParams)
8+
9+
const schema = z.object({
10+
owner: z.string(),
11+
name: z.string(),
12+
})
13+
14+
const { owner, name } = schema.parse(searchParams)
15+
16+
const starsMonth = await getAllGithubStars(owner, name)
17+
18+
for (const stars of starsMonth) {
19+
console.log(stars.stars)
20+
console.log({
21+
month: stars.date.month,
22+
year: stars.date.year,
23+
day: stars.date.day,
24+
})
25+
}
26+
27+
return null
28+
}

app/utils/github.ts

Lines changed: 100 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import axios from "axios"
2+
import { add, differenceInDays, format, isAfter, parseISO } from "date-fns"
13
import { DAY_IN_MS } from "./constants"
24

35
export type RepositoryQueryResult = {
@@ -104,6 +106,31 @@ export const repositoryStarsQuery = `query RepositoryQuery($owner: String!, $nam
104106
}
105107
}`
106108

109+
export const getRepoStargazers = async (owner: string, name: string, page?: number) => {
110+
let url = `https://api.github.com/repos/${owner}/${name}/stargazers?per_page=${30}`
111+
112+
if (page !== undefined) {
113+
url = `${url}&page=${page}`
114+
}
115+
return axios.get(url, {
116+
headers: {
117+
Accept: "application/vnd.github.v3.star+json",
118+
Authorization: `token ${process.env.GITHUB_TOKEN}`,
119+
},
120+
})
121+
}
122+
123+
export const getRepoStargazersCount = async (owner: string, name: string) => {
124+
const { data } = await axios.get(`https://api.github.com/repos/${owner}/${name}`, {
125+
headers: {
126+
Accept: "application/vnd.github.v3.star+json",
127+
Authorization: `token ${process.env.GITHUB_TOKEN}`,
128+
},
129+
})
130+
131+
return data.stargazers_count
132+
}
133+
107134
/**
108135
* Extracts the repository owner and name from a GitHub URL.
109136
*
@@ -134,12 +161,12 @@ type GetToolScoreProps = {
134161
/**
135162
* Calculates a score for a tool based on its GitHub statistics and an optional bump.
136163
*
137-
* @param props.stars - The number of stars the tool has on GitHub.
138-
* @param props.forks - The number of forks the tool has on GitHub.
139-
* @param props.contributors - The number of contributors to the tool's repository.
140-
* @param props.watchers - The number of watchers the tool has on GitHub.
141-
* @param props.lastCommitDate - The date of the last commit to the tool's repository.
142-
* @param props.bump - An optional bump to the final score.
164+
* @param stars - The number of stars the tool has on GitHub.
165+
* @param forks - The number of forks the tool has on GitHub.
166+
* @param contributors - The number of contributors to the tool's repository.
167+
* @param watchers - The number of watchers the tool has on GitHub.
168+
* @param lastCommitDate - The date of the last commit to the tool's repository.
169+
* @param bump - An optional bump to the final score.
143170
* @returns The calculated score for the tool.
144171
*/
145172
export const calculateHealthScore = ({
@@ -164,3 +191,70 @@ export const calculateHealthScore = ({
164191
starsScore + forksScore + contributorsScore + watchersScore - lastCommitPenalty + (bump || 0),
165192
)
166193
}
194+
195+
export const getAllGithubStars = async (owner: string, name: string, amount = 20) => {
196+
// Get the total amount of stars from GitHub
197+
const totalStars = await getRepoStargazersCount(owner, name)
198+
199+
// get total pages
200+
const totalPages = Math.ceil(totalStars / 100)
201+
202+
// How many pages to skip? We don't want to spam requests
203+
const pageSkips = totalPages < amount ? amount : Math.ceil(totalPages / amount)
204+
205+
// Send all the requests at the same time
206+
const starsDates = (
207+
await Promise.all(
208+
[...new Array(amount)].map(async (_, index) => {
209+
const page = index * pageSkips || 1
210+
return getRepoStargazers(owner, name, page).then(res => res.data)
211+
}),
212+
)
213+
)
214+
.flatMap(p => p)
215+
.reduce((acc: any, stars: any) => {
216+
const yearMonth = stars.starred_at.split("T")[0]
217+
acc[yearMonth] = (acc[yearMonth] || 0) + 1
218+
return acc
219+
}, {})
220+
221+
console.log(starsDates)
222+
223+
// how many stars did we find from a total of `requestAmount` requests?
224+
const foundStars = Object.keys(starsDates).reduce((all, current) => all + starsDates[current], 0)
225+
226+
// Find the earliest date
227+
const lowestMonthYear = Object.keys(starsDates).reduce((lowest, current) => {
228+
const currentDate = parseISO(current.split("T")[0])
229+
if (isAfter(currentDate, lowest)) {
230+
return currentDate
231+
}
232+
return lowest
233+
}, parseISO(new Date().toISOString()))
234+
235+
// Count dates until today
236+
const splitDate = differenceInDays(new Date(), lowestMonthYear) + 1
237+
238+
// Create an array with the amount of stars we didn't find
239+
const array = [...new Array(totalStars - foundStars)]
240+
241+
// Set the amount of value to add proportionally for each day
242+
const splitStars: any[][] = []
243+
for (let i = splitDate; i > 0; i--) {
244+
splitStars.push(array.splice(0, Math.ceil(array.length / i)))
245+
}
246+
247+
// Calculate the amount of stars for each day
248+
return [...new Array(splitDate)].map((_, index, arr) => {
249+
const yearMonthDay = format(add(lowestMonthYear, { days: index }), "yyyy-MM-dd")
250+
const value = starsDates[yearMonthDay] || 0
251+
return {
252+
stars: value + splitStars[index].length,
253+
date: {
254+
month: +format(add(lowestMonthYear, { days: index }), "M"),
255+
year: +format(add(lowestMonthYear, { days: index }), "yyyy"),
256+
day: +format(add(lowestMonthYear, { days: index }), "d"),
257+
},
258+
}
259+
})
260+
}

bun.lockb

2.41 KB
Binary file not shown.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@
3838
"@uidotdev/usehooks": "^2.4.1",
3939
"@vercel/speed-insights": "^1.0.10",
4040
"algoliasearch": "^4.23.2",
41+
"axios": "^1.7.2",
4142
"cva": "beta",
4243
"date-fns": "^3.6.0",
44+
"dayjs": "^1.11.11",
4345
"got": "^14.2.1",
4446
"inngest": "^3.16.1",
4547
"instantsearch.js": "^4.66.1",

prisma/schema.prisma

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,14 @@ model TopicToTool {
122122
123123
@@id([toolId, topicSlug])
124124
}
125+
126+
model RepositoryStars {
127+
id String @id @default(uuid())
128+
month Int
129+
year Int
130+
day Int
131+
name String
132+
stars Int
133+
134+
@@unique([name, day, month, year])
135+
}

0 commit comments

Comments
 (0)