Skip to content

Commit e78d2cd

Browse files
committed
2.0.0 – mutual followers feature and URL persistence
1 parent de93112 commit e78d2cd

21 files changed

Lines changed: 104 additions & 16 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "githubster",
3-
"version": "1.8.0",
3+
"version": "2.0.0",
44
"description": "Track your GitHub followers, unfollowers, and following relationships",
55
"license": "MIT",
66
"scripts": {

src/app/page.tsx

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useState, useEffect } from "react";
3+
import { useState, useEffect, useCallback } from "react";
44
import { SearchForm } from "@/components/search-form";
55
import { Tabs } from "@/components/tabs";
66
import { UserGrid } from "@/components/user-grid";
@@ -11,7 +11,7 @@ import { RateLimit } from "@/components/rate-limit";
1111
import { useLocale } from "@/lib/locale-context";
1212
import { getFollowData, type FollowData, type RateLimitInfo } from "@/lib/github";
1313

14-
type TabId = "unfollowers" | "notFollowingBack" | "following" | "followers";
14+
type TabId = "unfollowers" | "notFollowingBack" | "mutuals" | "following" | "followers";
1515

1616
export default function Home() {
1717
const { t } = useLocale();
@@ -24,20 +24,17 @@ export default function Home() {
2424
const [rateLimit, setRateLimit] = useState<RateLimitInfo | null>(null);
2525
const [lastSearch, setLastSearch] = useState<{ user: string; token?: string } | null>(null);
2626

27-
useEffect(() => {
28-
if (!showPrivacy) return;
29-
const handleKey = (e: KeyboardEvent) => {
30-
if (e.key === "Escape") setShowPrivacy(false);
31-
};
32-
document.addEventListener("keydown", handleKey);
33-
return () => document.removeEventListener("keydown", handleKey);
34-
}, [showPrivacy]);
35-
36-
async function handleSearch(user: string, token?: string) {
27+
const handleSearch = useCallback(async (user: string, token?: string) => {
3728
setIsLoading(true);
3829
setError(null);
3930
setData(null);
4031
setLastSearch({ user, token });
32+
33+
// Update URL without reload
34+
const url = new URL(window.location.href);
35+
url.searchParams.set("user", user);
36+
window.history.pushState({}, "", url.toString());
37+
4138
try {
4239
const result = await getFollowData(user, token);
4340
setData(result);
@@ -50,7 +47,25 @@ export default function Home() {
5047
} finally {
5148
setIsLoading(false);
5249
}
53-
}
50+
}, []);
51+
52+
// Read username from URL on mount
53+
useEffect(() => {
54+
const params = new URLSearchParams(window.location.search);
55+
const userFromUrl = params.get("user");
56+
if (userFromUrl) {
57+
handleSearch(userFromUrl);
58+
}
59+
}, [handleSearch]);
60+
61+
useEffect(() => {
62+
if (!showPrivacy) return;
63+
const handleKey = (e: KeyboardEvent) => {
64+
if (e.key === "Escape") setShowPrivacy(false);
65+
};
66+
document.addEventListener("keydown", handleKey);
67+
return () => document.removeEventListener("keydown", handleKey);
68+
}, [showPrivacy]);
5469

5570
function handleRetry() {
5671
if (lastSearch) {
@@ -85,6 +100,19 @@ export default function Home() {
85100
</svg>
86101
),
87102
},
103+
{
104+
id: "mutuals",
105+
label: t.tabs.mutuals,
106+
count: data.mutuals.length,
107+
icon: (
108+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
109+
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
110+
<circle cx="8.5" cy="7" r="4" />
111+
<path d="M17 14h6" />
112+
<path d="M20 11v6" />
113+
</svg>
114+
),
115+
},
88116
{
89117
id: "following",
90118
label: t.tabs.following,
@@ -118,6 +146,7 @@ export default function Home() {
118146
const emptyMessages: Record<TabId, string> = {
119147
unfollowers: t.empty.unfollowers,
120148
notFollowingBack: t.empty.notFollowingBack,
149+
mutuals: t.empty.mutuals,
121150
following: t.empty.following,
122151
followers: t.empty.followers,
123152
};
@@ -234,6 +263,7 @@ export default function Home() {
234263
following={data.following.length}
235264
unfollowers={data.unfollowers.length}
236265
notFollowingBack={data.notFollowingBack.length}
266+
mutuals={data.mutuals.length}
237267
/>
238268

239269
{rateLimit && <RateLimit rateLimit={rateLimit} />}

src/components/stats-bar.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,28 @@ interface StatsBarProps {
77
following: number;
88
unfollowers: number;
99
notFollowingBack: number;
10+
mutuals: number;
1011
}
1112

1213
export function StatsBar({
1314
followers,
1415
following,
1516
unfollowers,
1617
notFollowingBack,
18+
mutuals,
1719
}: StatsBarProps) {
1820
const { t } = useLocale();
1921

2022
const stats = [
2123
{ label: t.stats.followers, value: followers, color: "#10b981" },
2224
{ label: t.stats.following, value: following, color: "#6366f1" },
25+
{ label: t.stats.mutuals, value: mutuals, color: "#8b5cf6" },
2326
{ label: t.stats.unfollowers, value: unfollowers, color: "#ef4444" },
2427
{ label: t.stats.notFollowingBack, value: notFollowingBack, color: "#f59e0b" },
2528
];
2629

2730
return (
28-
<div className="stagger-children grid grid-cols-2 gap-3 sm:grid-cols-4">
31+
<div className="stagger-children grid grid-cols-2 gap-3 sm:grid-cols-5">
2932
{stats.map((stat) => (
3033
<div
3134
key={stat.label}

src/lib/github.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export interface FollowData {
117117
following: GitHubUser[];
118118
unfollowers: GitHubUser[];
119119
notFollowingBack: GitHubUser[];
120+
mutuals: GitHubUser[];
120121
rateLimit: RateLimitInfo | null;
121122
}
122123

@@ -152,8 +153,11 @@ export async function getFollowData(
152153
(u) => !followingLogins.has(u.login)
153154
);
154155

156+
// People you follow who also follow you back
157+
const mutuals = following.filter((u) => followerLogins.has(u.login));
158+
155159
// Use the lowest remaining rate limit from both requests
156160
const rateLimit = followingResult.rateLimit ?? followersResult.rateLimit;
157161

158-
return { followers, following, unfollowers, notFollowingBack, rateLimit };
162+
return { followers, following, unfollowers, notFollowingBack, mutuals, rateLimit };
159163
}

src/lib/translations/ar.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,21 @@ export const ar: Translations = {
2424
tabs: {
2525
unfollowers: "لا يتابعونك",
2626
notFollowingBack: "لا تتابعهم",
27+
mutuals: "متبادلون",
2728
following: "المتابَعون",
2829
followers: "المتابِعون",
2930
},
3031
stats: {
3132
followers: "المتابِعون",
3233
following: "المتابَعون",
34+
mutuals: "متبادلون",
3335
unfollowers: "لا يتابعونك",
3436
notFollowingBack: "لا تتابعهم",
3537
},
3638
empty: {
3739
unfollowers: "كل من تتابعهم يتابعونك! 🎉",
3840
notFollowingBack: "أنت تتابع جميع متابعيك! 🤝",
41+
mutuals: "لا توجد اتصالات متبادلة بعد.",
3942
following: "لا تتابع أحداً بعد.",
4043
followers: "لا يوجد متابعون بعد.",
4144
initial: "أدخل اسم مستخدم أعلاه",

src/lib/translations/de.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,21 @@ export const de: Translations = {
2424
tabs: {
2525
unfollowers: "Folgen nicht zurück",
2626
notFollowingBack: "Du folgst nicht",
27+
mutuals: "Gegenseitig",
2728
following: "Folge ich",
2829
followers: "Follower",
2930
},
3031
stats: {
3132
followers: "Follower",
3233
following: "Folge ich",
34+
mutuals: "Gegenseitig",
3335
unfollowers: "Folgen nicht zurück",
3436
notFollowingBack: "Du folgst nicht",
3537
},
3638
empty: {
3739
unfollowers: "Alle, denen du folgst, folgen dir zurück! 🎉",
3840
notFollowingBack: "Du folgst allen deinen Followern! 🤝",
41+
mutuals: "Noch keine gegenseitigen Verbindungen.",
3942
following: "Noch niemandem gefolgt.",
4043
followers: "Noch keine Follower.",
4144
initial: "Gib oben einen Benutzernamen ein",

src/lib/translations/en.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,21 @@ export const en = {
2323
tabs: {
2424
unfollowers: "Not Following Back",
2525
notFollowingBack: "You Don't Follow",
26+
mutuals: "Mutuals",
2627
following: "Following",
2728
followers: "Followers",
2829
},
2930
stats: {
3031
followers: "Followers",
3132
following: "Following",
33+
mutuals: "Mutuals",
3234
unfollowers: "Don't follow back",
3335
notFollowingBack: "You don't follow",
3436
},
3537
empty: {
3638
unfollowers: "Everyone you follow follows you back! 🎉",
3739
notFollowingBack: "You follow everyone who follows you! 🤝",
40+
mutuals: "No mutual connections yet.",
3841
following: "Not following anyone yet.",
3942
followers: "No followers yet.",
4043
initial: "Enter a username above to see the full picture",

src/lib/translations/es.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,21 @@ export const es: Translations = {
2424
tabs: {
2525
unfollowers: "No te siguen",
2626
notFollowingBack: "No sigues",
27+
mutuals: "Mutuos",
2728
following: "Siguiendo",
2829
followers: "Seguidores",
2930
},
3031
stats: {
3132
followers: "Seguidores",
3233
following: "Siguiendo",
34+
mutuals: "Mutuos",
3335
unfollowers: "No te siguen",
3436
notFollowingBack: "No sigues",
3537
},
3638
empty: {
3739
unfollowers: "¡Todos los que sigues te siguen de vuelta! 🎉",
3840
notFollowingBack: "¡Sigues a todos tus seguidores! 🤝",
41+
mutuals: "Aún no hay conexiones mutuas.",
3942
following: "Aún no sigues a nadie.",
4043
followers: "Aún sin seguidores.",
4144
initial: "Ingresa un nombre de usuario arriba",

src/lib/translations/fr.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,21 @@ export const fr: Translations = {
2424
tabs: {
2525
unfollowers: "Ne suivent pas",
2626
notFollowingBack: "Vous ne suivez pas",
27+
mutuals: "Mutuels",
2728
following: "Abonnements",
2829
followers: "Abonnés",
2930
},
3031
stats: {
3132
followers: "Abonnés",
3233
following: "Abonnements",
34+
mutuals: "Mutuels",
3335
unfollowers: "Ne suivent pas",
3436
notFollowingBack: "Vous ne suivez pas",
3537
},
3638
empty: {
3739
unfollowers: "Tous ceux que vous suivez vous suivent en retour ! 🎉",
3840
notFollowingBack: "Vous suivez tous vos abonnés ! 🤝",
41+
mutuals: "Pas encore de connexions mutuelles.",
3942
following: "Aucun abonnement pour le moment.",
4043
followers: "Aucun abonné pour le moment.",
4144
initial: "Entrez un nom d'utilisateur ci-dessus",

src/lib/translations/hi.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,21 @@ export const hi: Translations = {
2424
tabs: {
2525
unfollowers: "वापस फॉलो नहीं करते",
2626
notFollowingBack: "आप फॉलो नहीं करते",
27+
mutuals: "म्यूचुअल",
2728
following: "फॉलोइंग",
2829
followers: "फॉलोअर्स",
2930
},
3031
stats: {
3132
followers: "फॉलोअर्स",
3233
following: "फॉलोइंग",
34+
mutuals: "म्यूचुअल",
3335
unfollowers: "वापस फॉलो नहीं करते",
3436
notFollowingBack: "आप फॉलो नहीं करते",
3537
},
3638
empty: {
3739
unfollowers: "आप जिन्हें फॉलो करते हैं वे सभी आपको वापस फॉलो करते हैं! 🎉",
3840
notFollowingBack: "आप अपने सभी फॉलोअर्स को फॉलो करते हैं! 🤝",
41+
mutuals: "अभी तक कोई म्यूचुअल कनेक्शन नहीं।",
3942
following: "अभी तक किसी को फॉलो नहीं किया।",
4043
followers: "अभी तक कोई फॉलोअर नहीं।",
4144
initial: "ऊपर यूज़रनेम दर्ज करें",

0 commit comments

Comments
 (0)