Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 1 addition & 11 deletions components/sections/CommunitySupportedNumbers.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { trackEvent } from "lib/posthog";
import { DISCORD_URL } from "../../lib/constants";
import Link from "components/elements/Link";
import { formatNumberToK } from "../../lib/helpers";

const CommunitySupportedNumbers = ({
stargazersCount,
Expand Down Expand Up @@ -77,15 +78,4 @@ const CommunitySupportedNumbers = ({
);
};

function formatNumberToK(number: number) {
if (number >= 1000) {
const suffixes = ["", "k", "M", "G", "T", "P"];
const magnitude = Math.floor(Math.log10(number) / 3);
const scaledNumber = number / Math.pow(1000, magnitude);
const formattedNumber = scaledNumber.toFixed(1);
return `${formattedNumber}${suffixes[magnitude]}`;
}
return number.toString();
}

export default CommunitySupportedNumbers;
41 changes: 40 additions & 1 deletion components/sections/Navigation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@

import clsx from "clsx";
import CustomButton from "../../elements/CustomButton";
import { Hamburger, Logo } from "components/svgs";
import { GithubLogo, Hamburger, Logo } from "components/svgs";
// import { Github, Discord } from "components/svgs";
import { trackEvent } from "lib/posthog";
import dynamic from "next/dynamic";
import Link from "components/elements/Link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { DiscordLogo } from "components/svgs/DiscordLogo";
import { useStargazersCount } from "hooks/useStargazersCount";
import { formatNumberToK } from "lib/helpers";

const ThemeSwitcher = dynamic(() => import("./ThemeSwitcher"), { ssr: false });

Expand Down Expand Up @@ -48,6 +52,7 @@ const LinkItem = ({
const Navigation = () => {
const [open, setOpen] = useState(false);
const router = useRouter();
const { count: stargazersCount } = useStargazersCount(); // now available here

useEffect(() => {
function updateMenu() {
Expand Down Expand Up @@ -135,6 +140,40 @@ const Navigation = () => {
))}
</div>
<div className="mt-10 xl:ml-auto xl:mt-0 xl:flex xl:items-center xl:gap-5">
<div className="flex items-center gap-4 pl-px xl:ml-4">
{[
{
icon: <GithubLogo className="w-6 h-6" />,
href: "https://github.com/shuttle-hq/shuttle",
text: `${stargazersCount !== null ? formatNumberToK(stargazersCount) : ""}`,
title: `${stargazersCount ?? "N/A"} GitHub stars`,
},
{
icon: <DiscordLogo className="w-6 h-6" />,
href: "https://discord.com/invite/shuttle",
text: "",
title: "Join us on Discord",
},
].map(({ icon, href, text, title }, index) => (
<a
key={href}
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={href.includes("github") ? "GitHub" : "Discord"}
title={title}
onClick={() =>
trackEvent(
`homepage_mainnav_${href.includes("github") ? "github" : "discord"}`,
)
}
className={`flex gap-1 transition-colors text-[#D8D8D8] hover:text-black dark:text-white/60 dark:hover:text-white bg-[#1D1D1D] flex items-center justify-center rounded-full h-[48px] ${text ? " px-3" : "w-[48px]"}`}
>
{icon}
{text && <span className="text-xs">{text}</span>}
</a>
))}
</div>
<div
className="mt-10 flex flex-wrap items-center gap-5 xl:mt-0"
onClick={() => {
Expand Down
18 changes: 18 additions & 0 deletions components/svgs/DiscordLogo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as React from "react";

export const DiscordLogo = ({
className,
...props
}: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16"
className={className}
{...props}
>
<path d="M13.545 2.907a13.2 13.2 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.2 12.2 0 0 0-3.658 0 8 8 0 0 0-.412-.833.05.05 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.04.04 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032q.003.022.021.037a13.3 13.3 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019q.463-.63.818-1.329a.05.05 0 0 0-.01-.059l-.018-.011a9 9 0 0 1-1.248-.595.05.05 0 0 1-.02-.066l.015-.019q.127-.095.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.05.05 0 0 1 .053.007q.121.1.248.195a.05.05 0 0 1-.004.085 8 8 0 0 1-1.249.594.05.05 0 0 0-.03.03.05.05 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.2 13.2 0 0 0 4.001-2.02.05.05 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.03.03 0 0 0-.02-.019m-8.198 7.307c-.789 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612" />
</svg>
);
20 changes: 20 additions & 0 deletions hooks/useStargazersCount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import useSWR from "swr";

const fetcher = (url: string) => fetch(url).then((r) => r.json());

export function useStargazersCount(initial?: number) {
const { data, error, isLoading } = useSWR<{ count: number | null }>(
"/api/github/stars",
fetcher,
{
fallbackData: initial != null ? { count: initial } : undefined,
revalidateOnFocus: false,
},
);

return {
count: data?.count ?? null,
isLoading,
error,
};
}
19 changes: 19 additions & 0 deletions lib/github.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export async function fetchStargazersCount(): Promise<number> {
const headers: Record<string, string> = {
Accept: "application/vnd.github+json",
};

if (process.env.GITHUB_TOKEN) {
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
}

const res = await fetch("https://api.github.com/repos/shuttle-hq/shuttle", {
headers,
// Avoid Next.js fetch caching surprises when called server-side
next: { revalidate: 3600 },
});

if (!res.ok) throw new Error(`GitHub API error: ${res.status}`);
const json = await res.json();
return json.stargazers_count ?? 0;
}
11 changes: 11 additions & 0 deletions lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,14 @@ export function generateReadingTime(text: string): string {
}

export const mergeClasses = (...inputs: ClassValue[]) => twMerge(clsx(inputs));

export const formatNumberToK = (number: number) => {
if (number >= 1000) {
const suffixes = ["", "K", "M", "G", "T", "P"];
const magnitude = Math.floor(Math.log10(number) / 3);
const scaledNumber = number / Math.pow(1000, magnitude);
const formattedNumber = scaledNumber.toFixed(1);
return `${formattedNumber}${suffixes[magnitude]}`;
}
return number.toString();
};
23 changes: 23 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"remark-gfm": "3.0.1",
"remark-gfm-4": "npm:[email protected]",
"sharp": "^0.34.4",
"swr": "^2.3.6",
"tailwind-merge": "^3.0.2",
"typescript": "^5.5.4"
},
Expand Down
21 changes: 21 additions & 0 deletions pages/api/github/stars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { fetchStargazersCount } from "lib/github";

type StarsResponse = { count: number | null };

export default async function handler(
_req: NextApiRequest,
res: NextApiResponse<StarsResponse>,
) {
try {
const count = await fetchStargazersCount();
// Cache on the CDN for 1h, serve stale while revalidating for a day
res.setHeader(
"Cache-Control",
"s-maxage=3600, stale-while-revalidate=86400",
);
res.status(200).json({ count });
} catch {
res.status(200).json({ count: null });
}
}