|
1 | 1 | import { ImageResponse } from 'next/og'; |
2 | 2 | import { NextRequest } from 'next/server'; |
3 | 3 |
|
4 | | -// Must be Edge runtime — Vercel's OG image generation requires it, |
5 | | -// and it's much faster than Node (no cold-start overhead). |
6 | 4 | export const runtime = 'edge'; |
7 | 5 |
|
8 | | -// X/Twitter crawlers have a ~5 s hard timeout. |
9 | | -// We use the lightweight REST endpoint (no GraphQL, no rate-limit headers) |
10 | | -// and fall back gracefully so the image always renders. |
11 | | -async function fetchUserFast(username: string) { |
12 | | - const headers: HeadersInit = { Accept: 'application/vnd.github+json' }; |
13 | | - const pat = process.env.GITHUB_PAT; |
14 | | - if (pat) headers.Authorization = `Bearer ${pat}`; |
15 | | - |
16 | | - const res = await fetch(`https://api.github.com/users/${username}`, { |
17 | | - headers, |
18 | | - // next: { revalidate: 3600 }, // not supported on edge — use Cache-Control header instead |
19 | | - }); |
20 | | - |
21 | | - if (!res.ok) return null; |
22 | | - return res.json() as Promise<{ |
23 | | - name: string | null; |
24 | | - login: string; |
25 | | - avatar_url: string; |
26 | | - bio: string | null; |
27 | | - public_repos: number; |
28 | | - followers: number; |
29 | | - }>; |
30 | | -} |
| 6 | +export async function GET(req: NextRequest) { |
| 7 | + const { searchParams } = new URL(req.url); |
| 8 | + const user = searchParams.get('user') ?? 'unknown'; |
| 9 | + |
| 10 | + let totalCommits = 0; |
| 11 | + let longestStreak = 0; |
| 12 | + let currentStreak = 0; |
31 | 13 |
|
32 | | -export async function GET(request: NextRequest) { |
33 | | - const { searchParams } = new URL(request.url); |
34 | | - const username = searchParams.get('username') ?? 'ghost'; |
| 14 | + try { |
| 15 | + const baseUrl = req.nextUrl.origin; |
35 | 16 |
|
36 | | - // Fetch with a hard 4 s budget so we never exceed X's timeout |
37 | | - const user = await Promise.race([ |
38 | | - fetchUserFast(username), |
39 | | - new Promise<null>((res) => setTimeout(() => res(null), 4000)), |
40 | | - ]); |
| 17 | + const res = await fetch(`${baseUrl}/api/streak?user=${user}&refresh=true`, { |
| 18 | + cache: 'no-store', |
| 19 | + }); |
41 | 20 |
|
42 | | - const name = user?.name || username; |
43 | | - const avatarUrl = user?.avatar_url ?? `https://github.com/${username}.png`; |
44 | | - const bio = (user?.bio ?? '').slice(0, 90); |
45 | | - const repos = user?.public_repos ?? 0; |
46 | | - const followers = user?.followers ?? 0; |
| 21 | + if (res.ok) { |
| 22 | + const data = (await res.json()) as { |
| 23 | + totalContributions?: number; |
| 24 | + longestStreak?: number; |
| 25 | + currentStreak?: number; |
| 26 | + }; |
| 27 | + |
| 28 | + totalCommits = data.totalContributions ?? 0; |
| 29 | + longestStreak = data.longestStreak ?? 0; |
| 30 | + currentStreak = data.currentStreak ?? 0; |
| 31 | + } |
| 32 | + } catch { |
| 33 | + // fallback |
| 34 | + } |
47 | 35 |
|
48 | 36 | return new ImageResponse( |
49 | 37 | <div |
50 | 38 | style={{ |
51 | 39 | width: '1200px', |
52 | 40 | height: '630px', |
53 | | - background: '#000000', |
| 41 | + background: '#0d1117', |
54 | 42 | display: 'flex', |
55 | 43 | flexDirection: 'column', |
56 | 44 | alignItems: 'center', |
57 | 45 | justifyContent: 'center', |
58 | | - fontFamily: 'ui-sans-serif, system-ui, sans-serif', |
| 46 | + fontFamily: 'sans-serif', |
59 | 47 | position: 'relative', |
60 | | - overflow: 'hidden', |
61 | 48 | }} |
62 | 49 | > |
63 | | - {/* Subtle top border glow */} |
64 | 50 | <div |
65 | 51 | style={{ |
66 | 52 | position: 'absolute', |
67 | | - top: 0, |
68 | | - left: 0, |
69 | | - right: 0, |
70 | | - height: '1px', |
71 | | - background: 'rgba(255,255,255,0.12)', |
| 53 | + width: '600px', |
| 54 | + height: '300px', |
| 55 | + background: 'radial-gradient(ellipse, #58a6ff22 0%, transparent 70%)', |
| 56 | + top: '50px', |
| 57 | + left: '300px', |
72 | 58 | }} |
73 | 59 | /> |
74 | 60 |
|
75 | | - {/* Faint radial glow behind card */} |
76 | 61 | <div |
77 | 62 | style={{ |
78 | | - position: 'absolute', |
79 | | - top: '50%', |
80 | | - left: '50%', |
81 | | - transform: 'translate(-50%, -50%)', |
82 | | - width: '700px', |
83 | | - height: '700px', |
84 | | - borderRadius: '50%', |
85 | | - background: 'radial-gradient(circle, rgba(99,102,241,0.08) 0%, transparent 70%)', |
| 63 | + fontSize: '48px', |
| 64 | + color: '#58a6ff', |
| 65 | + fontWeight: 'bold', |
| 66 | + marginBottom: '24px', |
86 | 67 | }} |
87 | | - /> |
| 68 | + > |
| 69 | + ⚡ CommitPulse |
| 70 | + </div> |
| 71 | + |
| 72 | + <div |
| 73 | + style={{ |
| 74 | + fontSize: '32px', |
| 75 | + color: '#c9d1d9', |
| 76 | + marginBottom: '48px', |
| 77 | + }} |
| 78 | + > |
| 79 | + @{user} |
| 80 | + </div> |
88 | 81 |
|
89 | | - {/* Card */} |
90 | 82 | <div |
91 | 83 | style={{ |
92 | 84 | display: 'flex', |
93 | | - flexDirection: 'column', |
94 | | - alignItems: 'center', |
95 | | - gap: '28px', |
96 | | - padding: '56px 80px', |
97 | | - background: 'rgba(255,255,255,0.03)', |
98 | | - border: '1px solid rgba(255,255,255,0.09)', |
99 | | - borderRadius: '24px', |
100 | | - width: '920px', |
| 85 | + gap: '48px', |
101 | 86 | }} |
102 | 87 | > |
103 | | - {/* Avatar + name */} |
104 | | - <div style={{ display: 'flex', alignItems: 'center', gap: '36px' }}> |
105 | | - {/* eslint-disable-next-line @next/next/no-img-element */} |
106 | | - <img |
107 | | - src={avatarUrl} |
108 | | - width={104} |
109 | | - height={104} |
110 | | - style={{ borderRadius: '50%', border: '1px solid rgba(255,255,255,0.15)' }} |
111 | | - alt={name} |
112 | | - /> |
113 | | - <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}> |
114 | | - <span style={{ color: '#ffffff', fontSize: '40px', fontWeight: 700, lineHeight: 1 }}> |
115 | | - {name} |
116 | | - </span> |
117 | | - <span style={{ color: '#A1A1AA', fontSize: '20px', fontWeight: 400 }}>@{username}</span> |
118 | | - {bio && ( |
119 | | - <span style={{ color: 'rgba(255,255,255,0.45)', fontSize: '15px', marginTop: '2px' }}> |
120 | | - {bio} |
121 | | - </span> |
122 | | - )} |
| 88 | + <div |
| 89 | + style={{ |
| 90 | + display: 'flex', |
| 91 | + flexDirection: 'column', |
| 92 | + alignItems: 'center', |
| 93 | + background: '#161b22', |
| 94 | + border: '1px solid #30363d', |
| 95 | + borderRadius: '16px', |
| 96 | + padding: '32px 48px', |
| 97 | + }} |
| 98 | + > |
| 99 | + <div |
| 100 | + style={{ |
| 101 | + fontSize: '56px', |
| 102 | + fontWeight: 'bold', |
| 103 | + color: '#58a6ff', |
| 104 | + }} |
| 105 | + > |
| 106 | + {totalCommits} |
| 107 | + </div> |
| 108 | + |
| 109 | + <div |
| 110 | + style={{ |
| 111 | + fontSize: '18px', |
| 112 | + color: '#8b949e', |
| 113 | + marginTop: '8px', |
| 114 | + }} |
| 115 | + > |
| 116 | + Total Commits |
| 117 | + </div> |
| 118 | + </div> |
| 119 | + |
| 120 | + <div |
| 121 | + style={{ |
| 122 | + display: 'flex', |
| 123 | + flexDirection: 'column', |
| 124 | + alignItems: 'center', |
| 125 | + background: '#161b22', |
| 126 | + border: '1px solid #30363d', |
| 127 | + borderRadius: '16px', |
| 128 | + padding: '32px 48px', |
| 129 | + }} |
| 130 | + > |
| 131 | + <div |
| 132 | + style={{ |
| 133 | + fontSize: '56px', |
| 134 | + fontWeight: 'bold', |
| 135 | + color: '#f78166', |
| 136 | + }} |
| 137 | + > |
| 138 | + {longestStreak} |
| 139 | + </div> |
| 140 | + |
| 141 | + <div |
| 142 | + style={{ |
| 143 | + fontSize: '18px', |
| 144 | + color: '#8b949e', |
| 145 | + marginTop: '8px', |
| 146 | + }} |
| 147 | + > |
| 148 | + Longest Streak 🔥 |
123 | 149 | </div> |
124 | 150 | </div> |
125 | 151 |
|
126 | | - {/* Divider */} |
127 | 152 | <div |
128 | 153 | style={{ |
129 | | - width: '100%', |
130 | | - height: '1px', |
131 | | - background: 'rgba(255,255,255,0.08)', |
| 154 | + display: 'flex', |
| 155 | + flexDirection: 'column', |
| 156 | + alignItems: 'center', |
| 157 | + background: '#161b22', |
| 158 | + border: '1px solid #30363d', |
| 159 | + borderRadius: '16px', |
| 160 | + padding: '32px 48px', |
132 | 161 | }} |
133 | | - /> |
134 | | - |
135 | | - {/* Stats */} |
136 | | - <div style={{ display: 'flex', gap: '56px', alignItems: 'center' }}> |
137 | | - {[ |
138 | | - { value: repos, label: 'Repositories' }, |
139 | | - { value: followers, label: 'Followers' }, |
140 | | - ].map((stat, i, arr) => ( |
141 | | - <div key={stat.label} style={{ display: 'flex', alignItems: 'center', gap: '56px' }}> |
142 | | - <div |
143 | | - style={{ |
144 | | - display: 'flex', |
145 | | - flexDirection: 'column', |
146 | | - alignItems: 'center', |
147 | | - gap: '6px', |
148 | | - }} |
149 | | - > |
150 | | - <span |
151 | | - style={{ fontSize: '52px', fontWeight: 700, color: '#ffffff', lineHeight: 1 }} |
152 | | - > |
153 | | - {stat.value.toLocaleString()} |
154 | | - </span> |
155 | | - <span |
156 | | - style={{ |
157 | | - fontSize: '12px', |
158 | | - color: '#A1A1AA', |
159 | | - letterSpacing: '2px', |
160 | | - textTransform: 'uppercase', |
161 | | - }} |
162 | | - > |
163 | | - {stat.label} |
164 | | - </span> |
165 | | - </div> |
166 | | - {i < arr.length - 1 && ( |
167 | | - <div |
168 | | - style={{ width: '1px', height: '56px', background: 'rgba(255,255,255,0.08)' }} |
169 | | - /> |
170 | | - )} |
171 | | - </div> |
172 | | - ))} |
| 162 | + > |
| 163 | + <div |
| 164 | + style={{ |
| 165 | + fontSize: '56px', |
| 166 | + fontWeight: 'bold', |
| 167 | + color: '#3fb950', |
| 168 | + }} |
| 169 | + > |
| 170 | + {currentStreak} |
| 171 | + </div> |
| 172 | + |
| 173 | + <div |
| 174 | + style={{ |
| 175 | + fontSize: '18px', |
| 176 | + color: '#8b949e', |
| 177 | + marginTop: '8px', |
| 178 | + }} |
| 179 | + > |
| 180 | + Current Streak ⚡ |
| 181 | + </div> |
173 | 182 | </div> |
174 | 183 | </div> |
175 | 184 |
|
176 | | - {/* Branding */} |
177 | 185 | <div |
178 | 186 | style={{ |
179 | 187 | position: 'absolute', |
180 | | - bottom: '28px', |
181 | | - display: 'flex', |
182 | | - alignItems: 'center', |
183 | | - gap: '8px', |
| 188 | + bottom: '32px', |
| 189 | + fontSize: '16px', |
| 190 | + color: '#484f58', |
184 | 191 | }} |
185 | 192 | > |
186 | | - <span |
187 | | - style={{ |
188 | | - fontSize: '13px', |
189 | | - color: 'rgba(255,255,255,0.25)', |
190 | | - letterSpacing: '3px', |
191 | | - textTransform: 'uppercase', |
192 | | - }} |
193 | | - > |
194 | | - commitpulse.vercel.app |
195 | | - </span> |
| 193 | + commitpulse.vercel.app |
196 | 194 | </div> |
197 | 195 | </div>, |
198 | 196 | { |
|
0 commit comments