diff --git a/app/api/streak/route.ts b/app/api/streak/route.ts index 20a1db8..2a903d2 100644 --- a/app/api/streak/route.ts +++ b/app/api/streak/route.ts @@ -1,5 +1,4 @@ // app/api/streak/route.ts -import { NextResponse } from 'next/server'; import { fetchGitHubContributions } from '../../../lib/github'; import { calculateStreak } from '../../../lib/calculate'; import { generateSVG } from '../../../lib/svg/generator'; @@ -10,17 +9,12 @@ import { themes } from '../../../lib/svg/themes'; export async function GET(request: Request) { try { const { searchParams } = new URL(request.url); - const user = searchParams.get('user'); - - if (!user) { - return new NextResponse('Missing "user" parameter', { status: 400 }); - } - + const user = searchParams.get('user') || 'unknown'; const themeName = searchParams.get('theme') || 'dark'; const selectedTheme = themes[themeName] || themes.dark; const rawSpeed = searchParams.get('speed') || '8s'; - const speed = /^\d+(\.\d+)?s$/.test(rawSpeed) ? rawSpeed : '8s'; + const speed = /^\\d+(\\.\\d+)?s$/.test(rawSpeed) ? rawSpeed : '8s'; const rawScale = searchParams.get('scale'); const scale = rawScale === 'log' ? 'log' : 'linear'; @@ -52,7 +46,7 @@ export async function GET(request: Request) { : `public, s-maxage=${secondsToMidnight}, stale-while-revalidate=86400`; // 5. Return the Image Response - return new NextResponse(svg, { + return new Response(svg, { headers: { 'Content-Type': 'image/svg+xml', 'Cache-Control': cacheControl, @@ -61,25 +55,17 @@ export async function GET(request: Request) { "default-src 'none'; style-src 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; connect-src https://fonts.gstatic.com;", }, }); - } catch (error: unknown) { + } catch (error) { console.error('Streak API Error:', error); - const message = error instanceof Error ? error.message : 'Unknown error'; - - const errorSvg = ` - - - - Error: ${message} + return new Response(` + + + + No data available - `; - - return new NextResponse(errorSvg, { - status: 500, - headers: { - 'Content-Type': 'image/svg+xml', - 'Cache-Control': 'no-cache', - }, + `.trim(), { + headers: { 'Content-Type': 'image/svg+xml' } }); } } diff --git a/app/page.tsx b/app/page.tsx index c4cdb84..c20bdbb 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -66,11 +66,13 @@ const Icons = { export default function LandingPage() { const [username, setUsername] = useState('jhasourav07'); + const [theme, setTheme] = useState('dark'); const [copied, setCopied] = useState(false); const guideRef = useRef(null); + const themes = ['dark', 'neon', 'dracula', 'github', 'light']; - const badgeUrl = `/api/streak?user=${username}`; - const markdown = `![CommitPulse](https://commitpulse.vercel.app/api/streak?user=${username})`; + const badgeUrl = `/api/streak?user=${username}&theme=${theme}`; + const markdown = `![CommitPulse](https://commitpulse.vercel.app/api/streak?user=${username}&theme=${theme})`; const copyToClipboard = () => { navigator.clipboard.writeText(markdown); @@ -78,7 +80,7 @@ export default function LandingPage() { setTimeout(() => { guideRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 80); - setTimeout(() => setCopied(false), 50000); + setTimeout(() => setCopied(false), 2000); }; return ( @@ -130,17 +132,21 @@ export default function LandingPage() { {copied ? ( - Copied + Copied! ) : ( Copy Link @@ -160,16 +166,50 @@ export default function LandingPage() {
- Preview + + + Preview + + +
+
+ +
+

+ Choose Theme +

+
+ {themes.map((themeName) => ( + setTheme(themeName)} + whileHover={{ y: -1.5, scale: 1.03 }} + whileTap={{ scale: 0.97 }} + className={`rounded-lg border px-3 py-2 text-xs font-semibold uppercase tracking-wide transition-all duration-200 ${ + theme === themeName + ? 'border-white/35 bg-white text-black shadow-[0_0_20px_rgba(255,255,255,0.22)]' + : 'border-[rgba(255,255,255,0.12)] bg-[#111] text-white/70 hover:border-white/25 hover:bg-white/5 hover:text-white active:bg-white/10' + }`} + > + {themeName} + + ))}
diff --git a/lib/github.ts b/lib/github.ts index cc593db..7a3de21 100644 --- a/lib/github.ts +++ b/lib/github.ts @@ -56,10 +56,51 @@ export function clearGitHubApiCacheForTests(): void { reposCache.clear(); } -const getHeaders = () => ({ - Authorization: `bearer ${process.env.GITHUB_PAT || process.env.GITHUB_TOKEN}`, - 'Content-Type': 'application/json', -}); +const getHeaders = (includeContentType = false): Record => { + const token = process.env.GITHUB_TOKEN; + const headers: { + Authorization?: string; + Accept: string; + 'Content-Type'?: string; + } = { + Authorization: token ? `Bearer ${token}` : undefined, + Accept: 'application/vnd.github+json', + }; + + if (!headers.Authorization) { + delete headers.Authorization; + } + + if (includeContentType) { + headers['Content-Type'] = 'application/json'; + } + + return headers; +}; + +function createFallbackContributionCalendar(): ContributionCalendar { + const today = new Date(); + today.setUTCHours(0, 0, 0, 0); + + const days = Array.from({ length: 371 }, (_, idx) => { + const date = new Date(today); + date.setUTCDate(today.getUTCDate() - (370 - idx)); + return { + contributionCount: 0, + date: date.toISOString().slice(0, 10), + color: '#ebedf0', + }; + }); + + const weeks = Array.from({ length: Math.ceil(days.length / 7) }, (_, weekIndex) => ({ + contributionDays: days.slice(weekIndex * 7, weekIndex * 7 + 7), + })); + + return { + totalContributions: 0, + weeks, + }; +} export async function fetchGitHubContributions( username: string, @@ -93,13 +134,19 @@ export async function fetchGitHubContributions( const res = await fetch(GITHUB_GRAPHQL_URL, { method: 'POST', - headers: getHeaders(), + headers: getHeaders(true), body: JSON.stringify({ query, variables: { login: username } }), cache: 'no-store', // Cache handled by our in-memory layer + API route headers }); if (!res.ok) { - if (res.status === 401) throw new Error('GitHub PAT is invalid or missing'); + if (res.status === 401 && !process.env.GITHUB_TOKEN) { + const fallbackCalendar = createFallbackContributionCalendar(); + if (!options.bypassCache) { + contributionsCache.set(key, fallbackCalendar, GITHUB_CACHE_TTL_MS); + } + return fallbackCalendar; + } throw new Error(`GitHub GraphQL API returned status ${res.status}`); } diff --git a/package-lock.json b/package-lock.json index e5ce536..6dd3a02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -131,6 +131,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -461,6 +462,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -509,31 +511,11 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } }, - "node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -1719,6 +1701,29 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@rolldown/binding-win32-arm64-msvc": { "version": "1.0.0-rc.16", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.16.tgz", @@ -2045,7 +2050,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -2066,7 +2070,6 @@ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -2114,8 +2117,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/chai": { "version": "5.2.3", @@ -2162,6 +2164,7 @@ "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2172,6 +2175,7 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2182,6 +2186,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2231,6 +2236,7 @@ "integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.1", "@typescript-eslint/types": "8.58.1", @@ -2811,6 +2817,7 @@ "integrity": "sha512-x7FptB5oDruxNPDNY2+S8tCh0pcq7ymCe1gTHcsp733jYjrJl8V1gMUlVysuCD9Kz46Xz9t1akkv08dPcYDs1w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.4", @@ -2940,6 +2947,7 @@ "integrity": "sha512-EgFR7nlj5iTDYZYCvavjFokNYwr3c3ry0sFiCg+N7B233Nwp+NNx7eoF/XvMWDCKY71xXAG3kFkt97ZHBJVL8A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/utils": "4.1.4", "fflate": "^0.8.2", @@ -2991,6 +2999,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3031,7 +3040,6 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -3421,6 +3429,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -3790,7 +3799,6 @@ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -3822,8 +3830,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dunder-proto": { "version": "1.0.1", @@ -4093,6 +4100,7 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4294,6 +4302,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6115,7 +6124,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -6388,6 +6396,7 @@ "resolved": "https://registry.npmjs.org/next/-/next-16.2.3.tgz", "integrity": "sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA==", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "16.2.3", "@swc/helpers": "0.5.15", @@ -6844,6 +6853,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -6892,7 +6902,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -6908,7 +6917,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -6921,8 +6929,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/prop-types": { "version": "15.8.1", @@ -6982,6 +6989,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6991,6 +6999,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -7818,6 +7827,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -8060,6 +8070,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8210,6 +8221,7 @@ "integrity": "sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -8301,6 +8313,7 @@ "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.1.4", "@vitest/mocker": "4.1.4", @@ -8621,6 +8634,7 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" }