From 37a1d15d17b6d0f641e0b36454a14cb3fccbb588 Mon Sep 17 00:00:00 2001 From: Shanidhya01 Date: Tue, 23 Jun 2026 19:41:08 +0530 Subject: [PATCH 01/36] test: add unit tests for streak calculation utilities --- package-lock.json | 32 +-- package.json | 1 + test/streak.test.ts | 614 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 621 insertions(+), 26 deletions(-) create mode 100644 test/streak.test.ts diff --git a/package-lock.json b/package-lock.json index cb25d49e5..ede2359ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "react-markdown": "^10.1.0", "recharts": "^2.15.4", "rehype-sanitize": "^6.0.0", + "rolldown": "^1.0.3", "server-only": "^0.0.1", "sharp": "^0.35.1", "sonner": "^2.0.7", @@ -81,7 +82,7 @@ "web-tree-sitter": "^0.26.9" }, "engines": { - "node": ">=20.0.0" + "node": "20.x" }, "optionalDependencies": { "@esbuild/linux-x64": "^0.28.0", @@ -2394,7 +2395,7 @@ "version": "1.11.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz", "integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@emnapi/wasi-threads": "1.2.2", @@ -2415,7 +2416,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "tslib": "^2.4.0" @@ -3718,7 +3719,6 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -4041,7 +4041,6 @@ }, "node_modules/@oxc-project/types": { "version": "0.133.0", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -4404,7 +4403,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4419,7 +4417,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4436,7 +4433,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4453,7 +4449,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4470,7 +4465,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4487,7 +4481,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4504,7 +4497,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4521,7 +4513,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4538,7 +4529,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4555,7 +4545,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4572,7 +4561,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4589,7 +4577,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4606,7 +4593,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -4622,7 +4608,6 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -4634,7 +4619,6 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -4645,7 +4629,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -4659,7 +4642,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4676,7 +4658,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4688,7 +4669,6 @@ }, "node_modules/@rolldown/pluginutils": { "version": "1.0.1", - "dev": true, "license": "MIT" }, "node_modules/@rollup/plugin-commonjs": { @@ -6823,7 +6803,6 @@ "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -17033,7 +17012,8 @@ }, "node_modules/rolldown": { "version": "1.0.3", - "dev": true, + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", "license": "MIT", "dependencies": { "@oxc-project/types": "=0.133.0", diff --git a/package.json b/package.json index c1a71b827..635d48703 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "react-markdown": "^10.1.0", "recharts": "^2.15.4", "rehype-sanitize": "^6.0.0", + "rolldown": "^1.0.3", "server-only": "^0.0.1", "sharp": "^0.35.1", "sonner": "^2.0.7", diff --git a/test/streak.test.ts b/test/streak.test.ts new file mode 100644 index 000000000..53c8eab10 --- /dev/null +++ b/test/streak.test.ts @@ -0,0 +1,614 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { + calculateStreakFromDates, + calculateCurrentStreak as currentStreakFromSet, + calculateStreak, +} from "@/lib/streak"; +import { + calculateCurrentStreak, + calculateLongestStreak, +} from "@/lib/streak-utils"; + +// ────────────────────────────────────────────────────────────────────────────── +// Helpers +// ────────────────────────────────────────────────────────────────────────────── + +function days(...dates: string[]): Set { + return new Set(dates); +} + +// ────────────────────────────────────────────────────────────────────────────── +// calculateStreakFromDates (src/lib/streak.ts) +// ────────────────────────────────────────────────────────────────────────────── + +describe("calculateStreakFromDates", () => { + // Fake clock: 2026-06-15 12:00 UTC + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-06-15T12:00:00Z")); + }); + afterEach(() => vi.useRealTimers()); + + it("returns 0 for empty commit history", () => { + const result = calculateStreakFromDates(new Set(), new Set()); + expect(result.current).toBe(0); + expect(result.longest).toBe(0); + expect(result.totalActiveDays).toBe(0); + expect(result.lastCommitDate).toBeNull(); + expect(result.freezeDates).toEqual([]); + }); + + it("returns 1 for a single active date today", () => { + const result = calculateStreakFromDates(days("2026-06-15")); + expect(result.current).toBe(1); + expect(result.longest).toBe(1); + expect(result.lastCommitDate).toBe("2026-06-15"); + }); + + it("returns 1 for a single active date yesterday", () => { + const result = calculateStreakFromDates(days("2026-06-14")); + expect(result.current).toBe(1); + expect(result.longest).toBe(1); + }); + + it("returns 0 when the most recent date is older than yesterday", () => { + const result = calculateStreakFromDates(days("2026-06-12", "2026-06-13")); + expect(result.current).toBe(0); + expect(result.longest).toBe(2); + }); + + it("counts consecutive days correctly", () => { + const result = calculateStreakFromDates( + days("2026-06-13", "2026-06-14", "2026-06-15") + ); + expect(result.current).toBe(3); + expect(result.longest).toBe(3); + }); + + it("resets current streak on a missed day", () => { + // run1: 06-10→06-12 (3 days), run2: 06-14→06-15 (2 days, ending today) + const result = calculateStreakFromDates( + days("2026-06-10", "2026-06-11", "2026-06-12", "2026-06-14", "2026-06-15") + ); + expect(result.current).toBe(2); + expect(result.longest).toBe(3); + }); + + it("handles streak freeze: freeze date bridges a one-day gap", () => { + const activeDates = days("2026-06-13", "2026-06-15"); + const freezeDates = days("2026-06-14"); + const result = calculateStreakFromDates(activeDates, freezeDates); + expect(result.current).toBe(3); + expect(result.longest).toBe(3); + expect(result.freezeDates).toContain("2026-06-14"); + }); + + it("handles streak freeze with only freeze dates and no active commits", () => { + const result = calculateStreakFromDates( + new Set(), + days("2026-06-14", "2026-06-15") + ); + expect(result.current).toBe(2); + expect(result.longest).toBe(2); + expect(result.totalActiveDays).toBe(2); + }); + + it("calculates longest streak from history spanning multiple runs", () => { + // run1: 5 days; run2: 2 days (06-13→06-14, ending yesterday) + const result = calculateStreakFromDates( + days( + "2026-06-01", + "2026-06-02", + "2026-06-03", + "2026-06-04", + "2026-06-05", + "2026-06-13", + "2026-06-14" + ) + ); + expect(result.longest).toBe(5); + expect(result.current).toBe(2); + }); + + // ── Streak-at-risk detection ──────────────────────────────────────────────── + // "At risk" = streak is alive (current > 0) but the last commit was yesterday, + // not today, so the streak will expire if no commit is made before midnight. + + it("detects streak at risk: last commit yesterday, no commit today", () => { + const result = calculateStreakFromDates(days("2026-06-13", "2026-06-14")); + // Streak is alive via the grace-period (yesterday counts) + expect(result.current).toBe(2); + // The last commit is yesterday, not today → at risk + expect(result.lastCommitDate).toBe("2026-06-14"); + const isAtRisk = + result.current > 0 && result.lastCommitDate !== "2026-06-15"; + expect(isAtRisk).toBe(true); + }); + + it("is not at risk when a commit exists today", () => { + const result = calculateStreakFromDates(days("2026-06-14", "2026-06-15")); + expect(result.current).toBe(2); + const isAtRisk = + result.current > 0 && result.lastCommitDate !== "2026-06-15"; + expect(isAtRisk).toBe(false); + }); + + // ── Timezone boundaries ───────────────────────────────────────────────────── + + it("handles timezone boundaries: same wall-clock moment is different calendar dates across zones", () => { + // 2026-06-16 02:00 UTC = 2026-06-15 22:00 America/New_York (UTC-4) + vi.setSystemTime(new Date("2026-06-16T02:00:00Z")); + const activeDates = days("2026-06-13", "2026-06-14"); + + // UTC: today=2026-06-16, yesterday=2026-06-15 → last date 06-14 < yesterday → expired + const utcResult = calculateStreakFromDates(activeDates, new Set(), "UTC"); + expect(utcResult.current).toBe(0); + + // NY: today=2026-06-15, yesterday=2026-06-14 → last date 06-14 = yesterday → alive + const nyResult = calculateStreakFromDates( + activeDates, + new Set(), + "America/New_York" + ); + expect(nyResult.current).toBe(2); + }); + + it("correctly identifies today in a UTC+12 timezone (far ahead of UTC)", () => { + // 2026-06-15 20:00 UTC = 2026-06-16 08:00 in Pacific/Auckland (UTC+12) + vi.setSystemTime(new Date("2026-06-15T20:00:00Z")); + const activeDates = days("2026-06-15", "2026-06-16"); + + // Auckland: today=2026-06-16 → last date 06-16 = today → alive + const nzResult = calculateStreakFromDates( + activeDates, + new Set(), + "Pacific/Auckland" + ); + expect(nzResult.current).toBe(2); + + // UTC: today=2026-06-15, yesterday=2026-06-14 → last date 06-16 > today → current=0 + // (future date can't be "today or yesterday") + const utcResult = calculateStreakFromDates(activeDates, new Set(), "UTC"); + expect(utcResult.current).toBe(0); + }); + + // ── Shortly-after-midnight ────────────────────────────────────────────────── + + it("counts today correctly when a commit occurs shortly after UTC midnight", () => { + vi.setSystemTime(new Date("2026-06-15T00:05:00Z")); + const result = calculateStreakFromDates(days("2026-06-14", "2026-06-15")); + expect(result.current).toBe(2); + expect(result.lastCommitDate).toBe("2026-06-15"); + }); + + // ── Leap year ─────────────────────────────────────────────────────────────── + + it("handles leap year: Feb 28 → Feb 29 → Mar 1 are consecutive (2024)", () => { + const result = calculateStreakFromDates( + days("2024-02-28", "2024-02-29", "2024-03-01") + ); + expect(result.longest).toBe(3); + }); + + it("does not treat Feb 28 → Mar 1 as consecutive in a non-leap year (2025)", () => { + const result = calculateStreakFromDates( + days("2025-02-28", "2025-03-01") + ); + // Gap of 1 day: Feb has 28 days in 2025, so these ARE consecutive + expect(result.longest).toBe(2); + }); + + // ── Month transitions ─────────────────────────────────────────────────────── + + it("handles 28-day February to March transition (non-leap year)", () => { + const result = calculateStreakFromDates( + days("2025-02-27", "2025-02-28", "2025-03-01") + ); + expect(result.longest).toBe(3); + }); + + it("handles 29-day February to March transition (leap year)", () => { + const result = calculateStreakFromDates( + days("2024-02-29", "2024-03-01") + ); + expect(result.longest).toBe(2); + }); + + it("handles 30-day month transition: April 30 → May 1", () => { + const result = calculateStreakFromDates( + days("2026-04-29", "2026-04-30", "2026-05-01") + ); + expect(result.longest).toBe(3); + }); + + it("handles 31-day month transition: January 31 → February 1", () => { + const result = calculateStreakFromDates( + days("2026-01-30", "2026-01-31", "2026-02-01") + ); + expect(result.longest).toBe(3); + }); + + it("handles year boundary: December 31 → January 1", () => { + const result = calculateStreakFromDates( + days("2025-12-30", "2025-12-31", "2026-01-01") + ); + expect(result.longest).toBe(3); + }); + + // ── Edge cases: overlap and deduplication ─────────────────────────────────── + + it("does not double-count dates that appear in both active and freeze sets", () => { + // active: 06-13, 06-14; freeze: 06-14, 06-15 → combined unique: 3 days + const result = calculateStreakFromDates( + days("2026-06-13", "2026-06-14"), + days("2026-06-14", "2026-06-15") + ); + expect(result.totalActiveDays).toBe(3); + expect(result.current).toBe(3); + }); + + it("reports lastCommitDate as the chronologically latest date across active and freeze", () => { + const result = calculateStreakFromDates( + days("2026-06-13"), + days("2026-06-15") + ); + expect(result.lastCommitDate).toBe("2026-06-15"); + }); +}); + +// ────────────────────────────────────────────────────────────────────────────── +// calculateCurrentStreak — Set/string[] wrapper (src/lib/streak.ts) +// ────────────────────────────────────────────────────────────────────────────── + +describe("calculateCurrentStreak — Set/string[] wrapper (streak.ts)", () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-06-15T12:00:00Z")); + }); + afterEach(() => vi.useRealTimers()); + + it("returns 0 for an empty Set", () => { + expect(currentStreakFromSet(new Set())).toBe(0); + }); + + it("returns 0 for an empty array", () => { + expect(currentStreakFromSet([])).toBe(0); + }); + + it("accepts a Set of YYYY-MM-DD dates", () => { + expect(currentStreakFromSet(new Set(["2026-06-14", "2026-06-15"]))).toBe(2); + }); + + it("accepts a string[] and slices full ISO timestamps to the date part", () => { + expect( + currentStreakFromSet(["2026-06-14T10:00:00Z", "2026-06-15T08:30:00Z"]) + ).toBe(2); + }); + + it("returns 0 when the latest date is older than yesterday", () => { + expect( + currentStreakFromSet(["2026-06-10", "2026-06-11", "2026-06-12"]) + ).toBe(0); + }); + + it("handles a single YYYY-MM-DD string matching today", () => { + expect(currentStreakFromSet(["2026-06-15"])).toBe(1); + }); +}); + +// ────────────────────────────────────────────────────────────────────────────── +// calculateStreak — Date[] adapter (src/lib/streak.ts) +// ────────────────────────────────────────────────────────────────────────────── + +describe("calculateStreak — Date[] adapter (streak.ts)", () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-06-15T12:00:00Z")); + }); + afterEach(() => vi.useRealTimers()); + + it("returns 0/0 for empty input", () => { + expect(calculateStreak([])).toEqual({ currentStreak: 0, longestStreak: 0 }); + }); + + it("computes current and longest streak from Date objects", () => { + const result = calculateStreak([ + new Date("2026-06-13T10:00:00Z"), + new Date("2026-06-14T10:00:00Z"), + new Date("2026-06-15T10:00:00Z"), + ]); + expect(result.currentStreak).toBe(3); + expect(result.longestStreak).toBe(3); + }); + + it("deduplicates multiple commits on the same UTC day", () => { + const result = calculateStreak([ + new Date("2026-06-14T00:01:00Z"), + new Date("2026-06-14T23:59:00Z"), + new Date("2026-06-15T10:00:00Z"), + ]); + expect(result.currentStreak).toBe(2); + expect(result.longestStreak).toBe(2); + }); + + it("reports only the latest consecutive run as the current streak", () => { + const result = calculateStreak([ + new Date("2026-06-10T10:00:00Z"), + new Date("2026-06-14T10:00:00Z"), + new Date("2026-06-15T10:00:00Z"), + ]); + expect(result.currentStreak).toBe(2); + expect(result.longestStreak).toBe(2); + }); + + it("computes the all-time longest streak regardless of recency", () => { + const result = calculateStreak([ + new Date("2026-01-01T10:00:00Z"), + new Date("2026-01-02T10:00:00Z"), + new Date("2026-01-03T10:00:00Z"), + new Date("2026-01-04T10:00:00Z"), + new Date("2026-06-14T10:00:00Z"), + new Date("2026-06-15T10:00:00Z"), + ]); + expect(result.longestStreak).toBe(4); + expect(result.currentStreak).toBe(2); + }); + + it("counts today correctly when a commit arrives just after UTC midnight", () => { + vi.setSystemTime(new Date("2026-06-15T00:10:00Z")); + const result = calculateStreak([ + new Date("2026-06-14T10:00:00Z"), + new Date("2026-06-15T00:02:00Z"), + ]); + expect(result.currentStreak).toBe(2); + }); + + it("handles leap year Feb 29 as a UTC day in a consecutive run", () => { + const result = calculateStreak([ + new Date("2024-02-28T12:00:00Z"), + new Date("2024-02-29T12:00:00Z"), + new Date("2024-03-01T12:00:00Z"), + ]); + expect(result.longestStreak).toBe(3); + }); + + it("handles year boundary December 31 → January 1", () => { + const result = calculateStreak([ + new Date("2025-12-30T12:00:00Z"), + new Date("2025-12-31T12:00:00Z"), + new Date("2026-01-01T12:00:00Z"), + ]); + expect(result.longestStreak).toBe(3); + }); +}); + +// ────────────────────────────────────────────────────────────────────────────── +// calculateCurrentStreak (src/lib/streak-utils.ts) +// ────────────────────────────────────────────────────────────────────────────── + +describe("calculateCurrentStreak (streak-utils.ts)", () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-06-15T12:00:00Z")); + }); + afterEach(() => vi.useRealTimers()); + + it("returns 0 for empty commit history", () => { + expect(calculateCurrentStreak([])).toBe(0); + }); + + it("returns 1 for a single commit today", () => { + expect(calculateCurrentStreak(["2026-06-15"])).toBe(1); + }); + + it("returns 1 for a single commit yesterday", () => { + expect(calculateCurrentStreak(["2026-06-14"])).toBe(1); + }); + + it("returns 0 when the latest commit is older than yesterday", () => { + expect(calculateCurrentStreak(["2026-06-13"])).toBe(0); + }); + + it("counts consecutive days correctly ending today", () => { + expect( + calculateCurrentStreak(["2026-06-13", "2026-06-14", "2026-06-15"]) + ).toBe(3); + }); + + it("counts consecutive days correctly ending yesterday", () => { + expect( + calculateCurrentStreak(["2026-06-12", "2026-06-13", "2026-06-14"]) + ).toBe(3); + }); + + it("resets streak on a missed day, returning only the latest consecutive run", () => { + expect( + calculateCurrentStreak([ + "2026-06-10", + "2026-06-11", + "2026-06-13", + "2026-06-14", + "2026-06-15", + ]) + ).toBe(3); + }); + + it("deduplicates multiple commits on the same day", () => { + expect( + calculateCurrentStreak(["2026-06-14", "2026-06-14", "2026-06-15"]) + ).toBe(2); + }); + + it("handles unsorted input", () => { + expect( + calculateCurrentStreak(["2026-06-15", "2026-06-13", "2026-06-14"]) + ).toBe(3); + }); + + it("accepts Date objects as input", () => { + expect( + calculateCurrentStreak([ + new Date("2026-06-13T10:00:00Z"), + new Date("2026-06-14T10:00:00Z"), + new Date("2026-06-15T10:00:00Z"), + ]) + ).toBe(3); + }); + + it("detects streak at risk: last commit is yesterday, no commit today", () => { + const streak = calculateCurrentStreak(["2026-06-13", "2026-06-14"]); + // Streak is alive via the one-day grace period + expect(streak).toBe(2); + // Caller infers at-risk by comparing lastCommitDate to today independently + }); + + it("counts today correctly when a commit occurs shortly after UTC midnight", () => { + vi.setSystemTime(new Date("2026-06-15T00:05:00Z")); + expect(calculateCurrentStreak(["2026-06-14", "2026-06-15"])).toBe(2); + }); + + it("handles year boundary: December 31 → January 1", () => { + vi.setSystemTime(new Date("2026-01-01T12:00:00Z")); + expect(calculateCurrentStreak(["2025-12-31", "2026-01-01"])).toBe(2); + }); + + it("handles leap year Feb 29 in a consecutive run", () => { + vi.setSystemTime(new Date("2024-03-01T12:00:00Z")); + expect( + calculateCurrentStreak(["2024-02-28", "2024-02-29", "2024-03-01"]) + ).toBe(3); + }); + + it("handles 28-day February to March transition (non-leap year 2025)", () => { + vi.setSystemTime(new Date("2025-03-01T12:00:00Z")); + expect( + calculateCurrentStreak(["2025-02-27", "2025-02-28", "2025-03-01"]) + ).toBe(3); + }); + + it("handles 30-day month transition: April 30 → May 1", () => { + vi.setSystemTime(new Date("2026-05-01T12:00:00Z")); + expect( + calculateCurrentStreak(["2026-04-29", "2026-04-30", "2026-05-01"]) + ).toBe(3); + }); + + it("handles 31-day month transition: January 31 → February 1", () => { + vi.setSystemTime(new Date("2026-02-01T12:00:00Z")); + expect( + calculateCurrentStreak(["2026-01-30", "2026-01-31", "2026-02-01"]) + ).toBe(3); + }); + + it("filters out invalid date strings silently", () => { + expect(calculateCurrentStreak(["not-a-date", "2026-06-15"])).toBe(1); + }); + + it("filters out invalid Date objects (NaN) silently", () => { + expect( + calculateCurrentStreak([ + new Date("invalid"), + new Date("2026-06-15T10:00:00Z"), + ]) + ).toBe(1); + }); +}); + +// ────────────────────────────────────────────────────────────────────────────── +// calculateLongestStreak (src/lib/streak-utils.ts) +// ────────────────────────────────────────────────────────────────────────────── + +describe("calculateLongestStreak (streak-utils.ts)", () => { + it("returns 0 for empty input", () => { + expect(calculateLongestStreak([])).toBe(0); + }); + + it("returns 1 for a single active day", () => { + expect(calculateLongestStreak(["2026-06-15"])).toBe(1); + }); + + it("finds the all-time longest streak across gaps", () => { + expect( + calculateLongestStreak([ + "2026-06-01", + "2026-06-02", + "2026-06-03", + "2026-06-04", + "2026-06-10", + "2026-06-11", + ]) + ).toBe(4); + }); + + it("deduplicates dates before calculating", () => { + expect( + calculateLongestStreak(["2026-06-01", "2026-06-01", "2026-06-02"]) + ).toBe(2); + }); + + it("handles unsorted input correctly", () => { + expect( + calculateLongestStreak([ + "2026-06-05", + "2026-06-01", + "2026-06-03", + "2026-06-02", + "2026-06-04", + ]) + ).toBe(5); + }); + + it("correctly picks the longer of two disjoint runs", () => { + expect( + calculateLongestStreak([ + "2026-01-01", + "2026-01-02", + "2026-06-01", + "2026-06-02", + "2026-06-03", + "2026-06-04", + "2026-06-05", + ]) + ).toBe(5); + }); + + it("handles year boundary: December 31 → January 1 as consecutive", () => { + expect( + calculateLongestStreak(["2025-12-30", "2025-12-31", "2026-01-01"]) + ).toBe(3); + }); + + it("handles leap year: Feb 29 as a consecutive day", () => { + expect( + calculateLongestStreak(["2024-02-28", "2024-02-29", "2024-03-01"]) + ).toBe(3); + }); + + it("handles 28-day February to March transition (non-leap year 2025)", () => { + expect( + calculateLongestStreak(["2025-02-27", "2025-02-28", "2025-03-01"]) + ).toBe(3); + }); + + it("handles 30-day month transition: April 30 → May 1", () => { + expect( + calculateLongestStreak(["2026-04-29", "2026-04-30", "2026-05-01"]) + ).toBe(3); + }); + + it("handles 31-day month transition: January 31 → February 1", () => { + expect( + calculateLongestStreak(["2026-01-30", "2026-01-31", "2026-02-01"]) + ).toBe(3); + }); + + it("accepts Date objects", () => { + expect( + calculateLongestStreak([ + new Date("2026-06-01T10:00:00Z"), + new Date("2026-06-02T10:00:00Z"), + new Date("2026-06-03T10:00:00Z"), + ]) + ).toBe(3); + }); +}); From d8ae8098f10f44d9d05fd0fdcf253b41f838648c Mon Sep 17 00:00:00 2001 From: Priyanshu Doshi Date: Wed, 24 Jun 2026 15:40:25 +0530 Subject: [PATCH 02/36] docs: replace static project stats with live badges and add view counter Swap hardcoded stats table for dynamic shields.io badges (stars, forks), add komarev view counter, and remove good-first-issues badge. --- README.md | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 14a4301b4..4b0e7c09f 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,11 @@ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](./CONTRIBUTING.md) [![Stack](https://img.shields.io/badge/stack-Next.js%2016%20%7C%20Supabase%20%7C%20TypeScript-blue)](./DEVELOPMENT.md) -[![Good First Issues](https://img.shields.io/github/issues/Umbrella-io/devtrack/good%20first%20issue?label=good%20first%20issues&color=7c3aed)](https://github.com/Umbrella-io/devtrack/issues?q=label%3A%22good+first+issue%22) +[![Stars](https://img.shields.io/github/stars/Umbrella-io/devtrack?style=flat&color=yellow)](https://github.com/Umbrella-io/devtrack/stargazers) +[![Forks](https://img.shields.io/github/forks/Umbrella-io/devtrack?style=flat&color=blue)](https://github.com/Umbrella-io/devtrack/network/members) [![Contributors](https://img.shields.io/github/contributors/Umbrella-io/devtrack?color=brightgreen)](https://github.com/Umbrella-io/devtrack/graphs/contributors) [![Last Commit](https://img.shields.io/github/last-commit/Umbrella-io/devtrack)](https://github.com/Umbrella-io/devtrack/commits/main) +[![Views](https://komarev.com/ghpvc/?username=Priyanshu-byte-coder&label=Views&color=blueviolet)](https://github.com/Priyanshu-byte-coder/devtrack) [![GitHub Sponsors](https://img.shields.io/github/sponsors/Priyanshu-byte-coder?label=sponsors&color=ea4aaa)](https://github.com/sponsors/Priyanshu-byte-coder) **[Live Demo](https://devtrack-delta.vercel.app)** · **[Dev Guide](./DEVELOPMENT.md)** · **[Report Bug](https://github.com/Umbrella-io/devtrack/issues/new?template=bug_report.md)** · **[Request Feature](https://github.com/Umbrella-io/devtrack/issues/new?template=feature_request.md)** · **[Discussions](https://github.com/Umbrella-io/devtrack/discussions)** · **[Sponsor](https://github.com/sponsors/Priyanshu-byte-coder)** @@ -21,17 +23,6 @@ --- -## Project Stats - -| Metric | Count | -|---|---| -| GitHub Stars | 130+ | -| Forks | 369 | -| Merged PRs | 800+ | -| Contributors | 224 | - ---- - ## Demo
@@ -65,7 +56,6 @@ ## Table of Contents - [Why DevTrack?](#why-devtrack) -- [Project Stats](#project-stats) - [Demo](#demo) - [Features](#features) - [Tech Stack](#tech-stack) From 7c76640c7ffd8b195685f72e2e14c54c157b40f5 Mon Sep 17 00:00:00 2001 From: Priyanshu Doshi Date: Wed, 24 Jun 2026 15:52:45 +0530 Subject: [PATCH 03/36] fix(ui): remove duplicate theme toggle, fix landing footer styling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove duplicate ThemeToggle from hero section (navbar-only now) - Fix hardcoded #222 footer color → use MUTED CSS variable for dark mode - Add missing lnd-footer-link CSS class - Deduplicate lnd-cta-secondary CSS rule - Remove unused ThemeToggle import --- src/app/globals.css | 30 +++++++++++++++----------- src/components/landing/LandingPage.tsx | 4 +--- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 65f4e6e21..9c0ed747d 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -572,18 +572,6 @@ body { transform: scale(0.97); } -.lnd-cta-secondary { - transition: all 0.3s ease; -} - -.lnd-cta-secondary:hover { - background: #818cf8; - color: #fff; - border-color: #818cf8; - transform: translateY(-2px); -} - - .lnd-cta-secondary { display: inline-flex; align-items: center; @@ -600,6 +588,24 @@ body { transition: all 0.2s; } +.lnd-cta-secondary:hover { + background: #818cf8; + color: #fff; + border-color: #818cf8; + transform: translateY(-2px); +} + +.lnd-footer-link { + font-family: var(--font-jetbrains, ui-monospace, monospace); + font-size: 11px; + color: var(--muted-foreground); + text-decoration: none; + transition: color 0.2s; +} + +.lnd-footer-link:hover { + color: var(--foreground); +} .lnd-cell { background: color-mix(in srgb, var(--card) 60%, transparent); diff --git a/src/components/landing/LandingPage.tsx b/src/components/landing/LandingPage.tsx index 4a15a6994..2288ae65d 100644 --- a/src/components/landing/LandingPage.tsx +++ b/src/components/landing/LandingPage.tsx @@ -4,7 +4,6 @@ import { useEffect, useRef, useState } from 'react'; import Image from "next/image"; import Link from 'next/link'; import { Activity, GitPullRequest, Goal, Share2, Flame, FolderGit2, LogIn, LayoutDashboard, Target, Brain, Trophy, Users, Server, type LucideIcon } from "lucide-react"; -import ThemeToggle from "@/components/ThemeToggle"; /* ═══════════════════════════════════════════════════════════ PUBLIC TYPES @@ -624,7 +623,6 @@ function HeroSection() { {/* Right: bento window frame */}
-
- + © {new Date().getFullYear()} DEVTRACK
From 72a3e76285ccbac3dace04ae8ff39c8b6dab5aa2 Mon Sep 17 00:00:00 2001 From: Priyanshu Doshi Date: Wed, 24 Jun 2026 16:14:54 +0530 Subject: [PATCH 04/36] fix(dashboard): restructure layout, remove duplicates, fix cramped cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove duplicate widget section that rendered everything twice - Move orphaned sidebar into proper 2-column grid (1fr + 320px) - Standardize spacing with space-y-8 wrapper - Fix duplicate id="goals" → id="goals-insights" - Fix StreakTracker break-all → tabular-nums (numbers no longer break mid-digit) - Fix PRMetrics lg:grid-cols-6 → lg:grid-cols-3 (readable stat cards) - Make CodingActivityInsightsCard chart height responsive - Remove 7 dead imports and 2 unused dynamic components --- src/app/dashboard/page.tsx | 277 +++++++----------- src/components/CodingActivityInsightsCard.tsx | 4 +- src/components/PRMetrics.tsx | 6 +- src/components/StreakTracker.tsx | 2 +- 4 files changed, 111 insertions(+), 178 deletions(-) diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index c33b4034c..f510e8efd 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,26 +1,18 @@ import StreakTracker from "@/components/StreakTracker"; import RepoAnalyticsExplorer from "@/components/repo-analytics/RepoAnalyticsExplorer"; -import PinnedReposWidget from "@/components/PinnedReposWidget"; -import InactiveRepositoriesCard from "@/components/InactiveRepositoriesCard"; -import TopRepos from "@/components/TopRepos"; import dynamic from "next/dynamic"; import StreakAtRiskBanner from "@/components/StreakAtRiskBanner"; import ThrottleBanner from "@/components/ThrottleBanner"; import CustomizableDashboard from "@/components/dashboard/CustomizableDashboard"; import MilestonePlanner from "@/components/MilestonePlanner"; import LazyWidget from "@/components/LazyWidget"; -import DiscussionsWidget from "@/components/DiscussionsWidget"; import CommunityMetrics from "@/components/CommunityMetrics"; -import GoalTracker from "@/components/GoalTracker"; import TodayFocusHero from "@/components/TodayFocusHero"; import DashboardHeader from "@/components/DashboardHeader"; import ExportButton from "@/components/ExportButton"; import Link from "next/link"; -import PersonalRecords from "@/components/PersonalRecords"; import LocalCodingTime from "@/components/LocalCodingTime"; import CodingTimeWidget from "@/components/CodingTimeWidget"; -import RecentActivity from "@/components/RecentActivity"; -import FriendComparison from "@/components/FriendComparison"; import { ChevronRight } from "lucide-react"; import { authOptions } from "@/lib/auth"; import { getServerSession } from "next-auth"; @@ -43,13 +35,6 @@ const SkeletonCard = () => (
); -const ContributionGraphSkeleton = () => ( -
-

Your Commits

-
-
-); - const PRMetricsSkeleton = () => (

PR Analytics

@@ -67,16 +52,6 @@ const ActivityRingChart = dynamic( { loading: () => }, ); -const ContributionGraph = dynamic( - () => import("@/components/ContributionGraph"), - { loading: () => }, -); - -const ContributionHeatmap = dynamic( - () => import("@/components/ContributionHeatmap"), - { loading: () => }, -); - const PRMetrics = dynamic(() => import("@/components/PRMetrics"), { loading: () => , }); @@ -132,167 +107,122 @@ export default async function DashboardPage() {
- {/* Quick actions */} -
- - ✨ Year in Code - - - - - - Compare Friends - - - Settings - -
- -
-
- - {/* Info Banners */} -
- - -
- - {/* Today Focus Section */} -
- -
- - {/* Featured Section */} -
-
-
-
- - New Feature - - - AI Resume Generator - -
- -

- Generate an ATS-Friendly CV Backed by Your Real Code -

+
+ {/* Quick actions */} +
+ + ✨ Year in Code + + + + + + Compare Friends + + + Settings + +
+
-
- {/* Right: streak + coding time */} -
- - - - + {/* Info Banners */} +
+ +
- {/* Repo analytics explorer — full width */} -
- }> - - -
- - {/* -- Row 2: PR metrics + Community metrics -- */} -
- - -
- - {/* PR breakdown + commit time — 2-col so charts have room */} -
- }> - - - }> - - -
- - {/* Activity ring — full width */} -
- }> - - -
+ {/* Today Focus */} +
+ +
+ + {/* Featured Section */} +
+
+
+
+ + New Feature + + + AI Resume Generator + +
+

+ Generate an ATS-Friendly CV Backed by Your Real Code +

+
+
+
- {/* Coding activity insights — full width */} -
- }> - - -
+ {/* Main content + sidebar */} +
+ {/* Left: main widgets */} +
+ }> + + - {/* PR review trend — full width */} -
- }> - - -
+
+ + +
- {/* -- Row 3: Issues (2/3) + CI analytics (1/3) -- */} -
-
- }> - - -
+
+ }> + + + }> + + +
-
-
- - }> - - - }> - - - }> - - -
-
- }> - + + }> - + + }> - +
-
-
- {/* 4. GOALS & INSIGHTS */} -
-
-
-

Goals & Insights

+ {/* Right: sidebar */} +
+ + + + +
+ {/* Goals & Insights */} +
+
+
+

Goals & Insights

+
-
-
- -
- +
+ +
+ +
+ + +
diff --git a/src/components/CodingActivityInsightsCard.tsx b/src/components/CodingActivityInsightsCard.tsx index 4887d9b3c..269f6dd9d 100644 --- a/src/components/CodingActivityInsightsCard.tsx +++ b/src/components/CodingActivityInsightsCard.tsx @@ -279,7 +279,7 @@ export default function CodingActivityInsightsCard() { className="space-y-4" > Loading coding activity insights - +
{[1, 2, 3].map((item) => ( @@ -305,7 +305,7 @@ export default function CodingActivityInsightsCard() {
) : (
-
+
Loading PR analytics -
+
{[1, 2, 3, 4, 5, 6, 7].map((i) => (
-
+
{githubStats .filter((stat) => { if (prFilter === "all") return true; @@ -347,7 +347,7 @@ export default function PRMetrics() { {metrics?.gitlab && (

GitLab MRs

-
+
{gitlabStats .filter((stat) => { if (prFilter === "all") return true; diff --git a/src/components/StreakTracker.tsx b/src/components/StreakTracker.tsx index 3d531c72f..3b0d5e0fd 100644 --- a/src/components/StreakTracker.tsx +++ b/src/components/StreakTracker.tsx @@ -638,7 +638,7 @@ export default function StreakTracker() {
{stat.value} From 123ccc70edea7b54d6033a4426d73d0464771e92 Mon Sep 17 00:00:00 2001 From: Shanidhya01 Date: Wed, 24 Jun 2026 17:05:14 +0530 Subject: [PATCH 05/36] =?UTF-8?q?fix:=20address=20PR=20review=20=E2=80=94?= =?UTF-8?q?=20remove=20rolldown=20prod=20dep,=20restore=20Node=20engine=20?= =?UTF-8?q?range,=20fix=20test=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove rolldown from dependencies (build tool, not a runtime lib) - Restore engines.node to >=20.0.0 to avoid excluding Node 22/24 - Rename misleading test that said does not treat Feb 28 → Mar 1 as consecutive when the assertion proves they are consecutive in a non-leap year --- package-lock.json | 30 ++++++++++++++++++++++++++---- package.json | 3 +-- test/streak.test.ts | 2 +- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index ede2359ee..a23f8b7a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,6 @@ "react-markdown": "^10.1.0", "recharts": "^2.15.4", "rehype-sanitize": "^6.0.0", - "rolldown": "^1.0.3", "server-only": "^0.0.1", "sharp": "^0.35.1", "sonner": "^2.0.7", @@ -82,7 +81,7 @@ "web-tree-sitter": "^0.26.9" }, "engines": { - "node": "20.x" + "node": ">=20.0.0" }, "optionalDependencies": { "@esbuild/linux-x64": "^0.28.0", @@ -2395,7 +2394,7 @@ "version": "1.11.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz", "integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@emnapi/wasi-threads": "1.2.2", @@ -2416,7 +2415,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.4.0" @@ -3719,6 +3718,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -4041,6 +4041,7 @@ }, "node_modules/@oxc-project/types": { "version": "0.133.0", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -4403,6 +4404,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4417,6 +4419,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4433,6 +4436,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4449,6 +4453,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4465,6 +4470,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4481,6 +4487,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4497,6 +4504,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4513,6 +4521,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4529,6 +4538,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4545,6 +4555,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4561,6 +4572,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4577,6 +4589,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4593,6 +4606,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -4608,6 +4622,7 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -4619,6 +4634,7 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -4629,6 +4645,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -4642,6 +4659,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4658,6 +4676,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4669,6 +4688,7 @@ }, "node_modules/@rolldown/pluginutils": { "version": "1.0.1", + "dev": true, "license": "MIT" }, "node_modules/@rollup/plugin-commonjs": { @@ -6803,6 +6823,7 @@ "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -17014,6 +17035,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, "license": "MIT", "dependencies": { "@oxc-project/types": "=0.133.0", diff --git a/package.json b/package.json index 635d48703..286eb8cf1 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ "react-markdown": "^10.1.0", "recharts": "^2.15.4", "rehype-sanitize": "^6.0.0", - "rolldown": "^1.0.3", "server-only": "^0.0.1", "sharp": "^0.35.1", "sonner": "^2.0.7", @@ -102,7 +101,7 @@ "js-yaml": ">=4.1.0" }, "engines": { - "node": "20.x" + "node": ">=20.0.0" }, "optionalDependencies": { "@esbuild/linux-x64": "^0.28.0", diff --git a/test/streak.test.ts b/test/streak.test.ts index 53c8eab10..ccd3e8b8d 100644 --- a/test/streak.test.ts +++ b/test/streak.test.ts @@ -190,7 +190,7 @@ describe("calculateStreakFromDates", () => { expect(result.longest).toBe(3); }); - it("does not treat Feb 28 → Mar 1 as consecutive in a non-leap year (2025)", () => { + it("treats Feb 28 → Mar 1 as consecutive in a non-leap year (28-day February)", () => { const result = calculateStreakFromDates( days("2025-02-28", "2025-03-01") ); From 12f0cec986e498967c864ccba5de023963906b4d Mon Sep 17 00:00:00 2001 From: Priyanshu Doshi Date: Wed, 24 Jun 2026 18:55:33 +0530 Subject: [PATCH 06/36] refactor(dashboard): remove 11 duplicate widgets, delete dead code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All 11 widgets that were rendered directly in page.tsx are already handled by CustomizableDashboard with better skeletons, error boundaries, and ssr:false. Removed duplicates: - RepoAnalyticsExplorer, PRMetrics, CommunityMetrics - PRBreakdownChart, CommitTimeChart, ActivityRingChart - CodingActivityInsightsCard, PRReviewTrendChart - StreakTracker, LocalCodingTime, CodingTimeWidget Kept RoastHypeWidget (not in CustomizableDashboard). Deleted DashboardWidgets.tsx (dead code, zero importers). page.tsx: 245 → 152 lines. --- src/app/dashboard/page.tsx | 119 +++------------------ src/components/DashboardWidgets.tsx | 155 ---------------------------- 2 files changed, 13 insertions(+), 261 deletions(-) delete mode 100644 src/components/DashboardWidgets.tsx diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index f510e8efd..8cef45594 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,18 +1,11 @@ -import StreakTracker from "@/components/StreakTracker"; -import RepoAnalyticsExplorer from "@/components/repo-analytics/RepoAnalyticsExplorer"; -import dynamic from "next/dynamic"; import StreakAtRiskBanner from "@/components/StreakAtRiskBanner"; import ThrottleBanner from "@/components/ThrottleBanner"; import CustomizableDashboard from "@/components/dashboard/CustomizableDashboard"; import MilestonePlanner from "@/components/MilestonePlanner"; -import LazyWidget from "@/components/LazyWidget"; -import CommunityMetrics from "@/components/CommunityMetrics"; import TodayFocusHero from "@/components/TodayFocusHero"; import DashboardHeader from "@/components/DashboardHeader"; import ExportButton from "@/components/ExportButton"; import Link from "next/link"; -import LocalCodingTime from "@/components/LocalCodingTime"; -import CodingTimeWidget from "@/components/CodingTimeWidget"; import { ChevronRight } from "lucide-react"; import { authOptions } from "@/lib/auth"; import { getServerSession } from "next-auth"; @@ -23,54 +16,6 @@ import DashboardSSEProvider from "@/components/DashboardSSEProvider"; import { DashboardWidgetA11yProvider } from "@/components/dashboard/DashboardWidgetA11yContext"; import RoastHypeWidget from "./RoastHypeWidget"; -const SkeletonCard = () => ( -
-
-
-
-); - -const PRMetricsSkeleton = () => ( -
-

PR Analytics

-
-
-); - -const CodingActivityInsightsCard = dynamic( - () => import("@/components/CodingActivityInsightsCard"), - { loading: () => }, -); - -const ActivityRingChart = dynamic( - () => import("@/components/ActivityRingChart"), - { loading: () => }, -); - -const PRMetrics = dynamic(() => import("@/components/PRMetrics"), { - loading: () => , -}); - -const PRBreakdownChart = dynamic( - () => import("@/components/PRBreakdownChart"), - { loading: () => }, -); - -const CommitTimeChart = dynamic( - () => import("@/components/CommitTimeChart"), - { loading: () => }, -); - -const PRReviewTrendChart = dynamic( - () => import("@/components/PRReviewTrendChart"), - { loading: () => }, -); - export default async function DashboardPage() { // In the production standalone Playwright build, getServerSession can fail to // read the test JWT cookie. Decode the cookie directly as a fallback so that @@ -166,56 +111,20 @@ export default async function DashboardPage() {
- {/* Main content + sidebar */} -
- {/* Left: main widgets */} -
- }> - - - -
- - -
- -
- }> - - - }> - - -
- - }> - - - - }> - - - - }> - - -
+ {/* Roast/Hype Widget */} +
+ +
- {/* Right: sidebar */} -
- - - - -
-
+ {/* All dashboard widgets (drag-and-drop customizable) */} + {/* Goals & Insights */}
@@ -235,8 +144,6 @@ export default async function DashboardPage() {
- -
diff --git a/src/components/DashboardWidgets.tsx b/src/components/DashboardWidgets.tsx deleted file mode 100644 index 059425860..000000000 --- a/src/components/DashboardWidgets.tsx +++ /dev/null @@ -1,155 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { memo } from "react"; - - -import ContributionGraph from "@/components/ContributionGraph"; -import ContributionHeatmap from "@/components/ContributionHeatmap"; -import PRMetrics from "@/components/PRMetrics"; -import PRBreakdownChart from "@/components/PRBreakdownChart"; -import GoalTracker from "@/components/GoalTracker"; -import DashboardHeader from "@/components/DashboardHeader"; -import StreakTracker from "@/components/StreakTracker"; -import TopRepos from "@/components/TopRepos"; -import PinnedRepos from "@/components/PinnedRepos"; -import LanguageBreakdown from "@/components/LanguageBreakdown"; -import CommitTimeChart from "@/components/CommitTimeChart"; -import IssueMetrics from "@/components/IssueMetrics"; -import StreakAtRiskBanner from "@/components/StreakAtRiskBanner"; -import FriendComparison from "@/components/FriendComparison"; -import WeeklySummaryCard from "@/components/WeeklySummaryCard"; -import ExportButton from "@/components/ExportButton"; -import PersonalRecords from "@/components/PersonalRecords"; -import SponsorAnalytics from "@/components/SponsorAnalytics"; -import WidgetErrorBoundary from "@/components/WidgetErrorBoundary"; -import LazyWidget from "@/components/LazyWidget"; -import { Skeleton } from "@/components/Skeleton"; - -function DashboardWidgets() { return ( - <> - - - - -
- - - Settings - - - - - - -
- - - - - - - - - -
- - - -
- -
- - - -
- - - {/* Row 1 */} -
-
- - - - -
- - - -
-
- -
- - - - - - - -
-
- - {/* Row 2 */} -
- - - - - - - - - - - -
- - {/* Row 3 */} -
- }> - - - - -
- - - - {/* Row 4 */} -
- }> - - - - -
- - {/* Row 5 */} -
- }> - - - - - - }> - - - - - - }> - - - - -
- - ); -} - -export default memo(DashboardWidgets); From c93e16443a95ee15e14f77d0370955222d836f60 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 23:29:27 +0530 Subject: [PATCH 07/36] chore(deps): bump actions/setup-node from 4 to 6 (#2709) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/automated-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/automated-tests.yml b/.github/workflows/automated-tests.yml index e01c71bcd..8adac3355 100644 --- a/.github/workflows/automated-tests.yml +++ b/.github/workflows/automated-tests.yml @@ -20,7 +20,7 @@ jobs: uses: actions/checkout@v4 - name: Initialize Node.js Environment (v${{ matrix.node-version }}) - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} cache: 'npm' From 2b7f4c0e353d9c632c9beb7203403b60d71f9c9d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 23:29:31 +0530 Subject: [PATCH 08/36] chore(deps): bump github/codeql-action from 3 to 4 (#2711) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v3...v4) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 83440e159..1c03ae1bc 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -27,15 +27,15 @@ jobs: uses: actions/checkout@v4 - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} queries: security-extended - name: Autobuild - uses: github/codeql-action/autobuild@v3 + uses: github/codeql-action/autobuild@v4 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: category: /language:${{ matrix.language }} From 45199af976846fa287194616975803d1a6744d6d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 23:29:34 +0530 Subject: [PATCH 09/36] chore(deps): bump actions/checkout from 4 to 7 (#2710) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 7. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/automated-tests.yml | 2 +- .github/workflows/ci.yml | 12 ++++++------ .github/workflows/codeql.yml | 2 +- .github/workflows/e2e.yml | 2 +- .github/workflows/gssoc-labels.yml | 2 +- .github/workflows/labeler.yml | 2 +- .github/workflows/visual-regression.yml | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/automated-tests.yml b/.github/workflows/automated-tests.yml index 8adac3355..19ce37b04 100644 --- a/.github/workflows/automated-tests.yml +++ b/.github/workflows/automated-tests.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout Code Repository Layers - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Initialize Node.js Environment (v${{ matrix.node-version }}) uses: actions/setup-node@v6 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b46e026a5..632ad9171 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: name: Type check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: @@ -34,7 +34,7 @@ jobs: name: Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: @@ -54,7 +54,7 @@ jobs: contents: read id-token: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: @@ -97,7 +97,7 @@ jobs: SUPABASE_SERVICE_ROLE_KEY: placeholder-service-key NEXT_PUBLIC_APP_URL: http://localhost:3000 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: @@ -114,7 +114,7 @@ jobs: name: Dependency audit runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: @@ -131,7 +131,7 @@ jobs: name: Environment security check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Check SERVICE_ROLE_KEY exposure run: bash scripts/check-env-security.sh diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1c03ae1bc..75e41c1a0 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Initialize CodeQL uses: github/codeql-action/init@v4 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 38a230ae2..cfde528dd 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -26,7 +26,7 @@ jobs: PLAYWRIGHT_SERVER_MODE: start CI: true steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: diff --git a/.github/workflows/gssoc-labels.yml b/.github/workflows/gssoc-labels.yml index f4c07380f..ed7795dfd 100644 --- a/.github/workflows/gssoc-labels.yml +++ b/.github/workflows/gssoc-labels.yml @@ -18,7 +18,7 @@ jobs: if: github.event_name == 'pull_request_target' steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 5174cdc46..28a0bcacb 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -9,7 +9,7 @@ jobs: issues: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/labeler@v6 continue-on-error: true with: diff --git a/.github/workflows/visual-regression.yml b/.github/workflows/visual-regression.yml index ffed7eb2a..d36943768 100644 --- a/.github/workflows/visual-regression.yml +++ b/.github/workflows/visual-regression.yml @@ -40,7 +40,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup Node uses: actions/setup-node@v6 From f9a4945baf019741ed58ec1f6be32f55d70b1dc2 Mon Sep 17 00:00:00 2001 From: Shanidhya kumar <152613465+Shanidhya01@users.noreply.github.com> Date: Wed, 24 Jun 2026 23:29:45 +0530 Subject: [PATCH 10/36] feat(a11y): add skip-to-main-content link for keyboard and screen reader users (#2783) Adds a visually-hidden skip navigation link as the first focusable element in the root layout, targeting #main-content so keyboard-only users can bypass the navbar. Wraps page children with the skip target div and promotes the dashboard's top-level container to a semantic
element. --- src/app/dashboard/page.tsx | 4 ++-- src/app/layout.tsx | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 8cef45594..2ebdcbc76 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -49,7 +49,7 @@ export default async function DashboardPage() { return ( -
+
@@ -145,7 +145,7 @@ export default async function DashboardPage() {
-
+
); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 83153039f..c22d5b1f0 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -98,6 +98,12 @@ export default async function RootLayout({ + + Skip to main content + @@ -106,7 +112,9 @@ export default async function RootLayout({ - {children} +
+ {children} +