Skip to content

Commit 03c7c2d

Browse files
feat: optimize API performance with caching & parallel fetching, overhaul RepoDetail UI with analytics widgets, and implement Bento ID Profile page
1 parent 60c3691 commit 03c7c2d

17 files changed

Lines changed: 2162 additions & 130 deletions

package-lock.json

Lines changed: 1223 additions & 63 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@
1919
"@tanstack/react-query": "^5.95.2",
2020
"clsx": "^2.1.1",
2121
"framer-motion": "^12.38.0",
22-
"lucide-react": "^1.7.0",
22+
"lucide-react": "^1.8.0",
2323
"react": "^19.2.4",
2424
"react-dom": "^19.2.4",
25+
"react-markdown": "^10.1.0",
2526
"react-router-dom": "^7.13.2",
2627
"tailwind-merge": "^3.5.0"
2728
},

server/package-lock.json

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"@supabase/supabase-js": "^2.42.0",
1616
"dotenv": "^16.4.5",
1717
"fastify": "^4.26.2",
18+
"node-cache": "^5.1.2",
1819
"octokit": "^3.2.1",
1920
"zod": "^3.22.4"
2021
},

server/src/routes/profile.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { FastifyInstance } from 'fastify';
2+
import { supabaseAdmin } from '../server';
3+
4+
export async function profileRoutes(fastify: FastifyInstance) {
5+
6+
fastify.get('/profile/summary', async (request) => {
7+
const { data: repos, error } = await supabaseAdmin
8+
.from('repositories')
9+
.select('stars, forks, language, updated_at, name')
10+
.eq('owner_id', request.userId);
11+
12+
if (error || !repos) {
13+
return { stats: null, persona: null };
14+
}
15+
16+
const totalStars = repos.reduce((acc, r) => acc + (r.stars || 0), 0);
17+
const totalForks = repos.reduce((acc, r) => acc + (r.forks || 0), 0);
18+
const totalProjects = repos.length;
19+
20+
// Aggregate languages
21+
const languages: Record<string, number> = {};
22+
repos.forEach(r => {
23+
if (r.language) {
24+
languages[r.language] = (languages[r.language] || 0) + 1;
25+
}
26+
});
27+
28+
// Calculate Persona
29+
let persona = "The Code Voyager";
30+
const topLang = Object.entries(languages).sort((a, b) => b[1] - a[1])[0]?.[0];
31+
32+
if (totalStars > 50) persona = "The Star Magnet";
33+
else if (totalForks > 25) persona = "The Architect of Collaboration";
34+
else if (topLang === 'TypeScript' || topLang === 'JavaScript') persona = "The Frontend Alchemist";
35+
else if (topLang === 'Rust' || topLang === 'C++') persona = "The Performance Sage";
36+
else if (totalProjects > 10) persona = "The Prolific Builder";
37+
38+
// Simple Rhythm Analysis (Mocked for now, in a real app would use commit times)
39+
const rhythm = {
40+
type: "Night Owl",
41+
description: "Your signal peaks in the quiet hours of the night."
42+
};
43+
44+
return {
45+
stats: {
46+
totalStars,
47+
totalForks,
48+
totalProjects,
49+
languages
50+
},
51+
persona: {
52+
title: persona,
53+
level: Math.floor(totalProjects / 2) + 1
54+
},
55+
rhythm
56+
};
57+
});
58+
}

server/src/routes/repos.ts

Lines changed: 92 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { FastifyInstance } from 'fastify';
22
import { supabaseAdmin } from '../server';
3+
import { Octokit } from 'octokit';
4+
import { env } from '../env';
5+
import { getCache, setCache } from '../utils/cache';
36

47
export async function repoRoutes(fastify: FastifyInstance) {
58

@@ -18,23 +21,103 @@ export async function repoRoutes(fastify: FastifyInstance) {
1821
return { repos: data };
1922
});
2023

21-
// GET /api/repos/:id — single repo detail
22-
fastify.get<{ Params: { id: string } }>('/repos/:id', async (request, reply) => {
23-
const { data, error } = await supabaseAdmin
24+
// GET /api/repos/:id — single repo detail with extended analytics
25+
fastify.get<{ Params: { id: string }; Querystring: { refresh?: string } }>('/repos/:id', async (request, reply) => {
26+
const { data: repo, error } = await supabaseAdmin
2427
.from('repositories')
2528
.select('*')
2629
.eq('id', request.params.id)
2730
.eq('owner_id', request.userId)
2831
.single();
2932

30-
if (error) {
31-
request.log.error({ err: error, repoId: request.params.id }, 'Failed to fetch repository');
32-
return reply.status(500).send({ error: 'Failed to fetch repository' });
33-
}
34-
if (!data) {
33+
if (error || !repo) {
34+
request.log.error({ err: error, repoId: request.params.id }, 'Failed to fetch repository from DB');
3535
return reply.status(404).send({ error: 'Repository not found' });
3636
}
3737

38-
return { repo: data };
38+
const refreshRequested = request.query.refresh === 'true';
39+
const cacheKey = `repo_details_${repo.github_id}`;
40+
41+
// Check cache unless refresh is requested
42+
if (!refreshRequested) {
43+
const cachedDetails = getCache<any>(cacheKey);
44+
if (cachedDetails) {
45+
request.log.info({ repoId: repo.github_id }, 'Serving repo details from cache');
46+
return { repo: { ...repo, ...cachedDetails } };
47+
}
48+
} else {
49+
request.log.info({ repoId: repo.github_id }, 'Force refresh requested, bypassing cache');
50+
}
51+
52+
// Parallel fetching from GitHub with detailed logging
53+
const octokit = new Octokit({ auth: env.GITHUB_TOKEN });
54+
55+
// Safety check for name parsing
56+
const nameParts = repo.name.split('/');
57+
const owner = nameParts.length >= 2 ? nameParts[0] : '';
58+
const name = nameParts.length >= 2 ? nameParts[1] : nameParts[0];
59+
60+
request.log.info({ owner, name, github_token_exists: !!env.GITHUB_TOKEN }, 'Starting GitHub API fetch');
61+
62+
if (!owner || !name) {
63+
request.log.error({ fullName: repo.name }, 'Invalid repository full_name format');
64+
return { repo: { ...repo, languages: {}, contributors: [], activity: [], readme: '' } };
65+
}
66+
67+
try {
68+
const safeFetch = async <T,>(promise: Promise<T>, label: string, fallback: T): Promise<T> => {
69+
try {
70+
const result = await promise;
71+
request.log.info({ label }, `Successfully fetched ${label}`);
72+
return result;
73+
} catch (err: any) {
74+
request.log.warn({ err: err.message, status: err.status, label }, `Failed to fetch ${label}`);
75+
return fallback;
76+
}
77+
};
78+
79+
const [languages, contributors, activity, readme] = await Promise.all([
80+
safeFetch(
81+
octokit.rest.repos.listLanguages({ owner, repo: name }).then(r => r.data),
82+
'languages',
83+
{}
84+
),
85+
safeFetch(
86+
octokit.rest.repos.listContributors({ owner, repo: name, per_page: 20 }).then(r => r.data.map(c => ({
87+
login: c.login,
88+
avatar_url: c.avatar_url,
89+
contributions: c.contributions,
90+
html_url: c.html_url
91+
}))),
92+
'contributors',
93+
[]
94+
),
95+
safeFetch(
96+
octokit.rest.repos.getCommitActivityStats({ owner, repo: name }).then(r => r.data),
97+
'activity',
98+
[]
99+
),
100+
safeFetch(
101+
octokit.rest.repos.getReadme({ owner, repo: name }).then(r => Buffer.from(r.data.content, 'base64').toString()),
102+
'readme',
103+
''
104+
)
105+
]);
106+
107+
const extendedDetails = {
108+
languages: languages || {},
109+
contributors: contributors || [],
110+
activity: Array.isArray(activity) ? activity.slice(-12) : [],
111+
readme: readme || ''
112+
};
113+
114+
// Cache the result
115+
setCache(cacheKey, extendedDetails);
116+
117+
return { repo: { ...repo, ...extendedDetails } };
118+
} catch (err) {
119+
request.log.error({ err }, 'Unexpected crash in parallel fetching logic');
120+
return { repo: { ...repo, languages: {}, contributors: [], activity: [], readme: '' } };
121+
}
39122
});
40123
}

server/src/server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { repoRoutes } from './routes/repos';
77
import { analyticsRoutes } from './routes/analytics';
88
import { resourceRoutes } from './routes/resources';
99
import { snippetRoutes } from './routes/snippets';
10+
import { profileRoutes } from './routes/profile';
1011
import { createClient } from '@supabase/supabase-js';
1112

1213
const fastify = Fastify({ logger: true });
@@ -50,6 +51,7 @@ fastify.register(repoRoutes, { prefix: '/api' });
5051
fastify.register(analyticsRoutes, { prefix: '/api' });
5152
fastify.register(resourceRoutes, { prefix: '/api' });
5253
fastify.register(snippetRoutes, { prefix: '/api' });
54+
fastify.register(profileRoutes, { prefix: '/api' });
5355

5456
const start = async () => {
5557
try {

server/src/utils/cache.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import NodeCache from 'node-cache';
2+
3+
// Cache for 1 hour (3600 seconds), check for expired keys every 10 minutes
4+
const cache = new NodeCache({ stdTTL: 3600, checkperiod: 600 });
5+
6+
export const getCache = <T>(key: string): T | undefined => {
7+
return cache.get<T>(key);
8+
};
9+
10+
export const setCache = <T>(key: string, value: T, ttl?: number): void => {
11+
if (ttl) {
12+
cache.set(key, value, ttl);
13+
} else {
14+
cache.set(key, value);
15+
}
16+
};
17+
18+
export const deleteCache = (key: string): void => {
19+
cache.del(key);
20+
};
21+
22+
export const clearCache = (): void => {
23+
cache.flushAll();
24+
};
25+
26+
export default cache;

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Analytics } from './pages/Analytics';
99
import { Resources } from './pages/Resources';
1010
import { Editor } from './pages/Editor';
1111
import { RepoDetail } from './pages/RepoDetail';
12+
import { Profile } from './pages/Profile';
1213
import { NotFound } from './pages/NotFound';
1314

1415
export default function App() {
@@ -26,6 +27,7 @@ export default function App() {
2627
<Route path="/repo/:id" element={<RepoDetail />} />
2728
<Route path="/resources" element={<Resources />} />
2829
<Route path="/editor" element={<Editor />} />
30+
<Route path="/profile" element={<Profile />} />
2931
</Route>
3032
<Route path="*" element={<NotFound />} />
3133
</Routes>

0 commit comments

Comments
 (0)