-
Notifications
You must be signed in to change notification settings - Fork 405
add streak card widget #224
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,35 @@ | ||||||||||||||||||||
| import { changToNextGitHubToken } from "../utils/github-token-updater" | ||||||||||||||||||||
| import { getErrorMsgCard } from "../utils/error-card" | ||||||||||||||||||||
| import type { VercelRequest, VercelResponse } from "@vercel/node" | ||||||||||||||||||||
| import {getStreakCardSVGWithThemeName} from "../../src/cards/streak-cards" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| export default async (req: VercelRequest, res: VercelResponse) => { | ||||||||||||||||||||
| const { username, theme = "default" } = req.query | ||||||||||||||||||||
| if (typeof theme !== "string") { | ||||||||||||||||||||
| res.status(400).send("theme must be a string") | ||||||||||||||||||||
| return | ||||||||||||||||||||
| } | ||||||||||||||||||||
| if (typeof username !== "string") { | ||||||||||||||||||||
| res.status(400).send("username must be a string") | ||||||||||||||||||||
| return | ||||||||||||||||||||
| } | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| let tokenIndex = 0 | ||||||||||||||||||||
| while (true) { | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| const cardSVG = await getStreakCardSVGWithThemeName(username, theme) | ||||||||||||||||||||
| res.setHeader("Content-Type", "image/svg+xml") | ||||||||||||||||||||
| res.send(cardSVG) | ||||||||||||||||||||
| return | ||||||||||||||||||||
| } catch (err: any) { | ||||||||||||||||||||
| console.log(err.message) | ||||||||||||||||||||
| // We update github token and try again, until getNextGitHubToken throw an Error | ||||||||||||||||||||
| changToNextGitHubToken(tokenIndex) | ||||||||||||||||||||
| tokenIndex += 1 | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } catch (err: any) { | ||||||||||||||||||||
| console.log(err) | ||||||||||||||||||||
| res.send(getErrorMsgCard(err.message, theme)) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
+31
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Set SVG Content-Type on error responses. - } catch (err: any) {
- console.log(err)
- res.send(getErrorMsgCard(err.message, theme))
- }
+ } catch (err: any) {
+ console.log(err);
+ res.setHeader("Content-Type", "image/svg+xml");
+ res.send(getErrorMsgCard(err.message, theme));
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| } | ||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,61 @@ | ||||||||
| import { ThemeMap } from "../const/theme" | ||||||||
| import { createStreakCard, type StreakData } from "../templates/streak-card" | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Address the parsing error for type-only import syntax. The static analysis tool reports a parsing error on the type-only import. This suggests the linter or TypeScript configuration may not support TypeScript 3.8+ syntax. Option 1: Update TypeScript/ESLint configuration to support type-only imports (recommended if using TypeScript 3.8+): Ensure your Option 2: Use separate import statement if you cannot update the configuration: -import { createStreakCard, type StreakData } from "../templates/streak-card"
+import { createStreakCard } from "../templates/streak-card"
+import type { StreakData } from "../templates/streak-card"📝 Committable suggestion
Suggested change
🧰 Tools🪛 GitHub Check: lint[failure] 2-2: 🤖 Prompt for AI Agents |
||||||||
| import { writeSVG } from "../utils/file-writer" | ||||||||
| import { calculateStreaks } from "../github-api/streak-calculator" | ||||||||
| import { getContributionByYear } from "../github-api/contributions-by-year" | ||||||||
| import { getProfileDetails } from "../github-api/profile-details" | ||||||||
|
|
||||||||
| export const createStreakCardForUser = async (username: string) => { | ||||||||
| const streakData = await getStreakCardData(username) | ||||||||
| for (const themeName of ThemeMap.keys()) { | ||||||||
| const title = `` | ||||||||
| const svgString = getStreakCardSVG(title, streakData, themeName) | ||||||||
| // output to folder, use 1- prefix for sort in preview | ||||||||
| writeSVG(themeName, "1-streak-card", svgString) | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| export const getStreakCardSVGWithThemeName = async (username: string, themeName: string): Promise<string> => { | ||||||||
| if (!ThemeMap.has(themeName)) throw new Error("Theme does not exist") | ||||||||
| const streakData = await getStreakCardData(username) | ||||||||
| const title = `` | ||||||||
| return getStreakCardSVG(title, streakData, themeName) | ||||||||
| } | ||||||||
|
|
||||||||
| const getStreakCardSVG = (title: string, streakData: StreakData, themeName: string): string => { | ||||||||
| const svgString = createStreakCard(title, streakData, ThemeMap.get(themeName)!) | ||||||||
| return svgString | ||||||||
| } | ||||||||
|
|
||||||||
| const getStreakCardData = async (username: string): Promise<StreakData> => { | ||||||||
| const profileDetails = await getProfileDetails(username) | ||||||||
|
|
||||||||
| // Calculate total contributions | ||||||||
| let totalContributions = 0 | ||||||||
| if (process.env.VERCEL_I) { | ||||||||
| profileDetails.contributionYears = profileDetails.contributionYears.slice(0, 1) | ||||||||
| for (const year of profileDetails.contributionYears) { | ||||||||
| totalContributions += (await getContributionByYear(username, year)).totalContributions | ||||||||
| } | ||||||||
| } else { | ||||||||
| for (const year of profileDetails.contributionYears) { | ||||||||
| totalContributions += (await getContributionByYear(username, year)).totalContributions | ||||||||
| } | ||||||||
| } | ||||||||
|
Comment on lines
+35
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: VERCEL_I logic produces incorrect total contributions. When Additionally, the for loop logic is duplicated in both branches—the only difference is the slicing on line 36. If the intent is to limit API calls on Vercel for performance/rate-limiting reasons, consider one of these approaches: Option 1: Remove the VERCEL_I logic if accurate data is more important than performance: - let totalContributions = 0
- if (process.env.VERCEL_I) {
- profileDetails.contributionYears = profileDetails.contributionYears.slice(0, 1)
- for (const year of profileDetails.contributionYears) {
- totalContributions += (await getContributionByYear(username, year)).totalContributions
- }
- } else {
- for (const year of profileDetails.contributionYears) {
- totalContributions += (await getContributionByYear(username, year)).totalContributions
- }
- }
+ let totalContributions = 0
+ for (const year of profileDetails.contributionYears) {
+ totalContributions += (await getContributionByYear(username, year)).totalContributions
+ }Option 2: Use caching to avoid recalculating on every request while maintaining accuracy. Option 3: Clearly document the limitation and perhaps rename the metric to "Recent Contributions" when VERCEL_I is active, so users understand it's not the full history.
🤖 Prompt for AI Agents |
||||||||
|
|
||||||||
| // Calculate streaks from contribution data | ||||||||
| const streakInfo = calculateStreaks(profileDetails.contributions) | ||||||||
|
|
||||||||
| const streakData: StreakData = { | ||||||||
| currentStreak: streakInfo.currentStreak, | ||||||||
| currentStreakStart: streakInfo.currentStreakStart, | ||||||||
| currentStreakEnd: streakInfo.currentStreakEnd, | ||||||||
| maxStreak: streakInfo.maxStreak, | ||||||||
| maxStreakStart: streakInfo.maxStreakStart, | ||||||||
| maxStreakEnd: streakInfo.maxStreakEnd, | ||||||||
| totalContributions: totalContributions, | ||||||||
| joinedDate: new Date(profileDetails.createdAt), | ||||||||
| } | ||||||||
|
|
||||||||
| return streakData | ||||||||
| } | ||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,119 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import moment from "moment" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { ProfileContribution } from "./profile-details" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface StreakInfo { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentStreak: number | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentStreakStart: Date | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentStreakEnd: Date | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maxStreak: number | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maxStreakStart: Date | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maxStreakEnd: Date | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Calculate current and max streak from contribution data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param contributions Array of contribution data sorted by date | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns StreakInfo object with current and max streak information | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function calculateStreaks(contributions: ProfileContribution[]): StreakInfo { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (contributions.length === 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentStreak: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentStreakStart: new Date(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentStreakEnd: new Date(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maxStreak: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maxStreakStart: new Date(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maxStreakEnd: new Date(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Sort contributions by date ascending | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const sorted = [...contributions].sort((a, b) => a.date.getTime() - b.date.getTime()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let currentStreak = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let maxStreak = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let currentStreakStart = sorted[0].date | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let currentStreakEnd = sorted[0].date | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let maxStreakStart = sorted[0].date | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let maxStreakEnd = sorted[0].date | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let tempStreakStart = sorted[0].date | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Iterate through contributions to find streaks | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+30
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major 🧩 Analysis chainTimezone consistency (UTC vs server local). -const sorted = [...contributions].sort((a, b) => a.date.getTime() - b.date.getTime())
+const sorted = [...contributions].sort((a, b) => a.date.getTime() - b.date.getTime());
+// Option: accept utcOffset and use moment.utc().utcOffset(utcOffset) for day boundaries.🌐 Web query: 💡 Result: Short answer: GitHub's contributions calendar uses UTC day boundaries. Commits are counted by the commit's timestamp (which includes a timezone) and the profile/calendar itself is evaluated in UTC; pull requests and issues opened on GitHub use the viewer/browser timezone. [1][2] What this means for day-boundary calculations
Practical recommendation
Sources: I'll examine the streak-calculator.ts file and related code to understand the actual timezone handling: 🏁 Script executed: #!/bin/bash
# Examine the full streak-calculator.ts file
cat src/github-api/streak-calculator.tsLength of output: 3706 🏁 Script executed: #!/bin/bash
# Search for ProductiveTime references
rg -n "ProductiveTime" -C 3Length of output: 291339 🏁 Script executed: #!/bin/bash
# Search for timezone-related handling (moment, utc, offset, timezone)
rg -n "(utc|offset|timezone|getTimezoneOffset)" -C 2 --type=tsLength of output: 8742 🏁 Script executed: #!/bin/bash
# Check how this streak-calculator function is called and what data is passed
rg -n "streak-calculator\|StreakCalculator\|calculateStreak" -C 3 --type=tsLength of output: 0 🏁 Script executed: #!/bin/bash
# Look for where contributions data comes from (likely GitHub API responses)
rg -n "contributions.*date\|date.*contribution" -C 2 --type=tsLength of output: 0 🏁 Script executed: #!/bin/bash
# Check package.json to see if moment.js is a dependency
cat package.json | grep -A 20 -B 5 "moment\|dependencies"Length of output: 1127 Use UTC for day-boundary arithmetic (match GitHub contributions calendar) File: src/github-api/streak-calculator.ts — calculateStreaks GitHub groups contributions by UTC days; calculateStreaks currently uses moment(...) / moment().startOf("day") (server-local). Change day-boundary math to UTC or accept a utcOffset and apply it consistently. Minimal actionable fixes:
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (let i = 0; i < sorted.length; i++) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const current = sorted[i] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const previous = i > 0 ? sorted[i - 1] : null | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check if this is a continuation of streak (has contributions) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (current.contributionCount > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (previous && current.contributionCount > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const dayDiff = moment(current.date).diff(moment(previous.date), "days") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // If consecutive days, continue streak | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (dayDiff === 1) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentStreak++ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentStreakEnd = current.date | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Streak broken, check if it's the max | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (currentStreak > maxStreak) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maxStreak = currentStreak | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maxStreakStart = tempStreakStart | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maxStreakEnd = sorted[i - 1].date | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentStreak = 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tempStreakStart = current.date | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentStreakStart = current.date | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentStreakEnd = current.date | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+46
to
+66
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consecutive-day detection is incorrect (normalization + prev-day check).
- if (current.contributionCount > 0) {
- if (previous && current.contributionCount > 0) {
- const dayDiff = moment(current.date).diff(moment(previous.date), "days")
+ if (current.contributionCount > 0) {
+ if (previous && previous.contributionCount > 0) {
+ const dayDiff = moment(current.date).startOf("day")
+ .diff(moment(previous.date).startOf("day"), "days");
// If consecutive days, continue streak
if (dayDiff === 1) {
currentStreak++
currentStreakEnd = current.date
} else {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (!previous) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // First day | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentStreak = 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tempStreakStart = current.date | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentStreakStart = current.date | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentStreakEnd = current.date | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // No contribution on this day, streak broken | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (currentStreak > 0 && currentStreak > maxStreak) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maxStreak = currentStreak | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maxStreakStart = tempStreakStart | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maxStreakEnd = sorted[i - 1].date | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentStreak = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check final streak | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (currentStreak > maxStreak) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maxStreak = currentStreak | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maxStreakStart = tempStreakStart | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maxStreakEnd = sorted[sorted.length - 1].date | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Calculate current streak from today backwards | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const today = moment().startOf("day") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let actualCurrentStreak = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let actualCurrentStreakStart = today.toDate() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const actualCurrentStreakEnd = today.toDate() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (let i = sorted.length - 1; i >= 0; i--) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const current = sorted[i] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const currentMoment = moment(current.date).startOf("day") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const daysDiff = today.diff(currentMoment, "days") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (daysDiff === actualCurrentStreak && current.contributionCount > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| actualCurrentStreak++ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| actualCurrentStreakStart = current.date | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (daysDiff > actualCurrentStreak) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+92
to
+109
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainCurrent streak should end at last contributed day (not always “today”). - // Calculate current streak from today backwards
- const today = moment().startOf("day")
- let actualCurrentStreak = 0
- let actualCurrentStreakStart = today.toDate()
- const actualCurrentStreakEnd = today.toDate()
-
- for (let i = sorted.length - 1; i >= 0; i--) {
- const current = sorted[i]
- const currentMoment = moment(current.date).startOf("day")
- const daysDiff = today.diff(currentMoment, "days")
-
- if (daysDiff === actualCurrentStreak && current.contributionCount > 0) {
- actualCurrentStreak++
- actualCurrentStreakStart = current.date
- } else if (daysDiff > actualCurrentStreak) {
- break
- }
- }
+ // Calculate current streak from the last day with contributions backwards
+ let actualCurrentStreak = 0;
+ let actualCurrentStreakStart = sorted[sorted.length - 1].date;
+ let actualCurrentStreakEnd = sorted[sorted.length - 1].date;
+ const lastIdx = [...sorted].reverse().findIndex(c => c.contributionCount > 0);
+ if (lastIdx !== -1) {
+ const endIdx = sorted.length - 1 - lastIdx;
+ const endMoment = moment(sorted[endIdx].date).startOf("day");
+ actualCurrentStreakEnd = endMoment.toDate();
+ for (let i = endIdx; i >= 0; i--) {
+ const m = moment(sorted[i].date).startOf("day");
+ const diff = endMoment.diff(m, "days");
+ if (diff === actualCurrentStreak && sorted[i].contributionCount > 0) {
+ actualCurrentStreak++;
+ actualCurrentStreakStart = sorted[i].date;
+ } else if (diff > actualCurrentStreak) {
+ break;
+ }
+ }
+ } else {
+ actualCurrentStreakStart = new Date();
+ actualCurrentStreakEnd = new Date();
+ }🏁 Script executed: #!/bin/bash
# Quick sanity on edge cases:
# 1) No contributions today but had yesterday -> currentStreak should be >=1
# 2) Gap day in between -> currentStreak should stop at gap
cat <<'EOF' | node -e '
const moment = require("moment");
function js(d){ return new Date(d); }
const sorted = [
{date: js("2025-10-15T12:00:00Z"), contributionCount: 1},
{date: js("2025-10-16T12:00:00Z"), contributionCount: 1},
{date: js("2025-10-17T12:00:00Z"), contributionCount: 1},
{date: js("2025-10-18T12:00:00Z"), contributionCount: 0},
];
// Simulate your revised logic here if desired.
'
EOFLength of output: 833 I'll inspect the actual source file to verify the issue and proposed fix. Length of output: 3706 🏁 Script executed: #!/bin/bash
# Look for related test files
fd -e test.ts -e spec.ts -e test.js -e spec.js | xargs grep -l "streak" 2>/dev/null || trueLength of output: 0 🏁 Script executed: #!/bin/bash
# Check the full context around lines 92-109 in the file
fd "streak-calculator.ts" --exec head -150 {}Length of output: 3706 🏁 Script executed: #!/bin/bash
# Check if moment is available in package.json
cat package.json | head -50Length of output: 1752 🏁 Script executed: #!/bin/bash
# Search for any existing tests or usage of this streak calculator
rg -n "actualCurrentStreak|calculateCurrentStreak" --type=ts --type=jsLength of output: 858 Fix current streak calculation to start from last contribution day (not today). The current code assumes today has a contribution. When the last contribution was yesterday or earlier, the condition The proposed fix correctly identifies the last day with contributions and uses it as the reference point instead of today. This properly handles:
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentStreak: actualCurrentStreak, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentStreakStart: actualCurrentStreakStart, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentStreakEnd: actualCurrentStreakEnd, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maxStreak: maxStreak, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maxStreakStart: maxStreakStart, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maxStreakEnd: maxStreakEnd, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Avoid token rotation for non-auth errors (e.g., invalid theme).
Short-circuit when the error is not recoverable by changing tokens.
try { let tokenIndex = 0 while (true) { try { const cardSVG = await getStreakCardSVGWithThemeName(username, theme) res.setHeader("Content-Type", "image/svg+xml") res.send(cardSVG) return } catch (err: any) { console.log(err.message) - // We update github token and try again, until getNextGitHubToken throw an Error + // If theme is invalid or other non-auth errors, don't rotate tokens endlessly. + if (err?.message && /Theme does not exist/i.test(err.message)) { + throw err; + } + // We update GitHub token and try again, until the updater throws. changToNextGitHubToken(tokenIndex) tokenIndex += 1 } }🏁 Script executed:
Length of output: 6175
I need to examine the full context of the streak-stats.ts file and verify the token rotation pattern across other routes.
Length of output: 3415
I need to examine what errors
getStreakCardSVGWithThemeNamethrows and verify if the review comment's suggestion applies universally or is specific to streak-stats.Length of output: 920
Token rotation for non-recoverable errors should be prevented across all card routes.
The review comment correctly identifies that
getStreakCardSVGWithThemeNamethrows "Theme does not exist" (line 19 of src/cards/streak-cards.ts) for invalid themes—a non-recoverable validation error that token rotation cannot fix. The suggested diff is appropriate for streak-stats.ts.However, this pattern exists identically in all card API routes (stats.ts, repos-per-language.ts, productive-time.ts, most-commit-language.ts, profile-details.ts). Each route calls a similar SVG generation function that validates the theme name and throws the same type of error. Apply the same fix to all routes to prevent infinite token rotation for invalid inputs.
🤖 Prompt for AI Agents