diff --git a/src/app/api/dashboard/year/route.test.ts b/src/app/api/dashboard/year/route.test.ts
index ff42b61f..954656b1 100644
--- a/src/app/api/dashboard/year/route.test.ts
+++ b/src/app/api/dashboard/year/route.test.ts
@@ -76,7 +76,7 @@ describe("GET /api/dashboard/year validation", () => {
it("returns 200 and fetches data when year is valid", async () => {
vi.mocked(getAuthenticatedUser).mockResolvedValueOnce({ username: "alice", token: "token" });
- vi.mocked(fetchYearInReviewData).mockResolvedValueOnce({ data: "ok" } as unknown as YearInReviewData);
+ vi.mocked(fetchYearInReviewData).mockResolvedValueOnce({ data: "ok", mostActiveDay: null } as unknown as YearInReviewData);
const { GET } = await import("./route");
const currentYear = new Date().getUTCFullYear();
@@ -85,14 +85,14 @@ describe("GET /api/dashboard/year validation", () => {
expect(response.status).toBe(200);
const data = await response.json();
- expect(data).toEqual({ data: "ok" });
+ expect(data).toEqual({ data: "ok", mostActiveDay: null });
expect(fetchYearInReviewData).toHaveBeenCalledWith("alice", currentYear, "token");
});
it("returns 200 and falls back to current year when year is not provided", async () => {
vi.mocked(getAuthenticatedUser).mockResolvedValueOnce({ username: "alice", token: "token" });
- vi.mocked(fetchYearInReviewData).mockResolvedValueOnce({ data: "ok" } as unknown as YearInReviewData);
+ vi.mocked(fetchYearInReviewData).mockResolvedValueOnce({ data: "ok", mostActiveDay: null } as unknown as YearInReviewData);
const { GET } = await import("./route");
const req = createMockRequest(`http://localhost/api/dashboard/year`);
@@ -100,7 +100,7 @@ describe("GET /api/dashboard/year validation", () => {
expect(response.status).toBe(200);
const data = await response.json();
- expect(data).toEqual({ data: "ok" });
+ expect(data).toEqual({ data: "ok", mostActiveDay: null });
const currentYear = new Date().getUTCFullYear();
expect(fetchYearInReviewData).toHaveBeenCalledWith("alice", currentYear, "token");
diff --git a/src/components/ContributionsCard.tsx b/src/components/ContributionsCard.tsx
index 295cd5b6..860693f0 100644
--- a/src/components/ContributionsCard.tsx
+++ b/src/components/ContributionsCard.tsx
@@ -139,8 +139,6 @@ export default function ContributionsCard({ contributions }: Props) {
}
const stats = getStats(contributions);
- const showMostActiveDay = contributions.mostActiveDay.length > 0;
-
return (
@@ -153,7 +151,7 @@ export default function ContributionsCard({ contributions }: Props) {
{stats.map((stat, i) => (
))}
- {showMostActiveDay && (
+ {contributions.mostActiveDay && (
)}
diff --git a/src/components/YearInReviewCarousel.test.tsx b/src/components/YearInReviewCarousel.test.tsx
index 0535d7d2..d51f69e2 100644
--- a/src/components/YearInReviewCarousel.test.tsx
+++ b/src/components/YearInReviewCarousel.test.tsx
@@ -58,6 +58,15 @@ describe("YearInReviewCarousel", () => {
expect(screen.getByText("Most active on Monday around 14:00 UTC.")).toBeDefined();
});
+ it("does not stringify null mostActiveDay in the rhythm caption", () => {
+ render();
+
+ fireEvent.click(screen.getByRole("button", { name: "Prev" }));
+
+ expect(screen.getByText("No most active day yet; your peak activity hour is 14:00 UTC.")).toBeDefined();
+ expect(screen.queryByText(/Most active on null/)).toBeNull();
+ });
+
it("wraps around to the first slide when 'Next' is clicked on the last slide", () => {
render();
diff --git a/src/components/YearInReviewCarousel.tsx b/src/components/YearInReviewCarousel.tsx
index 326a1fe3..8c3a56d1 100644
--- a/src/components/YearInReviewCarousel.tsx
+++ b/src/components/YearInReviewCarousel.tsx
@@ -28,7 +28,9 @@ export default function YearInReviewCarousel({ data }: Props) {
{
key: "rhythm",
title: "Your Working Rhythm",
- caption: `Most active on ${data.mostActiveDay} around ${data.mostActiveHour}:00 UTC.`,
+ caption: data.mostActiveDay
+ ? `Most active on ${data.mostActiveDay} around ${data.mostActiveHour}:00 UTC.`
+ : `No most active day yet; your peak activity hour is ${data.mostActiveHour}:00 UTC.`,
},
],
[data],
diff --git a/src/components/YearInReviewSlide.test.tsx b/src/components/YearInReviewSlide.test.tsx
index c7e96ece..ef86b012 100644
--- a/src/components/YearInReviewSlide.test.tsx
+++ b/src/components/YearInReviewSlide.test.tsx
@@ -62,4 +62,10 @@ describe("YearInReviewSlide", () => {
expect(screen.getByText("Most active day: Monday")).toBeInTheDocument();
expect(screen.queryByText(/Top repo:/)).not.toBeInTheDocument();
});
+
+ it("does not render the most active day badge when mostActiveDay is null", () => {
+ render();
+
+ expect(screen.queryByText(/Most active day:/)).not.toBeInTheDocument();
+ });
});
diff --git a/src/components/YearInReviewSlide.tsx b/src/components/YearInReviewSlide.tsx
index ae2d3c36..88f296cb 100644
--- a/src/components/YearInReviewSlide.tsx
+++ b/src/components/YearInReviewSlide.tsx
@@ -57,9 +57,11 @@ export default function YearInReviewSlide({ title, caption, data }: Props) {
-
- Most active day: {data.mostActiveDay}
-
+ {data.mostActiveDay ? (
+
+ Most active day: {data.mostActiveDay}
+
+ ) : null}
{data.topRepository ? (
Top repo: {data.topRepository.name} (
diff --git a/src/components/__tests__/ContributionsCard.test.tsx b/src/components/__tests__/ContributionsCard.test.tsx
index 7c74729f..7537127f 100644
--- a/src/components/__tests__/ContributionsCard.test.tsx
+++ b/src/components/__tests__/ContributionsCard.test.tsx
@@ -16,7 +16,7 @@ describe("ContributionsCard", () => {
totalReviews: 0,
longestStreak: 0,
currentStreak: 0,
- mostActiveDay: "",
+ mostActiveDay: null,
calendar: [],
}}
/>
@@ -44,4 +44,26 @@ describe("ContributionsCard", () => {
expect(screen.getByText("Contributions")).toBeInTheDocument();
});
+
+ it("does not render MostActiveDayCard when mostActiveDay is null", () => {
+ render(
+
+ );
+
+ expect(screen.getByText("Contributions")).toBeInTheDocument();
+ expect(screen.queryByText("Most Active Day")).not.toBeInTheDocument();
+ });
+
});
diff --git a/src/lib/__tests__/github/fetchContributions.test.ts b/src/lib/__tests__/github/fetchContributions.test.ts
index 3cff75c0..31e5cc3a 100644
--- a/src/lib/__tests__/github/fetchContributions.test.ts
+++ b/src/lib/__tests__/github/fetchContributions.test.ts
@@ -104,4 +104,42 @@ describe("fetchContributions", () => {
// monthly is 7 days times 10 plus 23 days times 5 equals 185
expect(result.monthlyContributions).toBe(185);
});
+
+ it("空の履歴(contributionDaysが空または全て0)を正しく処理する", async () => {
+ const mockEmptyContributions = {
+ data: {
+ user: {
+ contributionsCollection: {
+ totalCommitContributions: 0,
+ totalPullRequestContributions: 0,
+ totalIssueContributions: 0,
+ totalPullRequestReviewContributions: 0,
+ contributionCalendar: {
+ totalContributions: 0,
+ weeks: [
+ {
+ contributionDays: []
+ }
+ ],
+ },
+ },
+ },
+ },
+ };
+
+ mockFetch.mockResolvedValueOnce(jsonResponse(mockEmptyContributions));
+
+ const { fetchContributions } = await import("../../github");
+ const result = await fetchContributions("testuser", "fake-token");
+
+ expect(result.totalCommits).toBe(0);
+ expect(result.totalContributions).toBe(0);
+ expect(result.weeklyContributions).toBe(0);
+ expect(result.monthlyContributions).toBe(0);
+ expect(result.longestStreak).toBe(0);
+ expect(result.currentStreak).toBe(0);
+ expect(result.mostActiveDay).toBeNull();
+ expect(result.calendar.length).toBe(0);
+ });
+
});
diff --git a/src/lib/__tests__/yearInReviewUtils.test.ts b/src/lib/__tests__/yearInReviewUtils.test.ts
index c964d7bc..2dc09118 100644
--- a/src/lib/__tests__/yearInReviewUtils.test.ts
+++ b/src/lib/__tests__/yearInReviewUtils.test.ts
@@ -133,8 +133,8 @@ describe("getMostActiveHour", () => {
});
describe("getMostActiveDayFromCalendar", () => {
- it("returns 'Sunday' when the calendar is empty", () => {
- expect(getMostActiveDayFromCalendar([])).toBe("Sunday");
+ it("returns null when the calendar is empty", () => {
+ expect(getMostActiveDayFromCalendar([])).toBeNull();
});
it("correctly identifies the most active day of the week", () => {
diff --git a/src/lib/github.ts b/src/lib/github.ts
index 93489f7e..ed7dd37f 100644
--- a/src/lib/github.ts
+++ b/src/lib/github.ts
@@ -88,7 +88,7 @@ function calculateStreaks(calendar: { count: number }[]): { longestStreak: numbe
return { longestStreak, currentStreak };
}
-function calculateMostActiveDay(calendar: { date: string; count: number }[]): string {
+function calculateMostActiveDay(calendar: { date: string; count: number }[]): string | null {
const weekdayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
const weekdayTotals = Array.from({ length: 7 }, () => 0);
@@ -103,7 +103,7 @@ function calculateMostActiveDay(calendar: { date: string; count: number }[]): st
const maxWeekdayTotal = Math.max(...weekdayTotals);
return maxWeekdayTotal > 0
? weekdayNames[weekdayTotals.findIndex((count) => count === maxWeekdayTotal)]
- : "";
+ : null;
}
async function graphql(query: string, token?: string, variables?: Record): Promise {
diff --git a/src/lib/types.ts b/src/lib/types.ts
index e55b3d26..6c6e2af5 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -59,7 +59,7 @@ export type ContributionData = {
weeklyContributions: number;
longestStreak: number;
currentStreak: number;
- mostActiveDay: string;
+ mostActiveDay: string | null;
calendar: { date: string; count: number }[];
};
@@ -91,7 +91,7 @@ export type YearInReviewData = {
totalPRs: number;
totalIssues: number;
totalReviews: number;
- mostActiveDay: string;
+ mostActiveDay: string | null;
mostActiveHour: number;
topRepository: {
name: string;
diff --git a/src/lib/yearInReviewUtils.ts b/src/lib/yearInReviewUtils.ts
index 52cee3ea..a128daf9 100644
--- a/src/lib/yearInReviewUtils.ts
+++ b/src/lib/yearInReviewUtils.ts
@@ -96,9 +96,9 @@ export function getMostActiveHour(heatmap: number[][]): number {
* Returns the most active day of the week from the contribution calendar data.
*
* @param calendar Array of objects containing date and contribution count.
- * @returns The name of the most active day (e.g., "Monday").
+ * @returns The name of the most active day (e.g., "Monday"), or null if there are no contributions.
*/
-export function getMostActiveDayFromCalendar(calendar: { date: string; count: number }[]): string {
+export function getMostActiveDayFromCalendar(calendar: { date: string; count: number }[]): string | null {
const weekdayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
const totals = Array.from({ length: 7 }, () => 0);
@@ -121,5 +121,5 @@ export function getMostActiveDayFromCalendar(calendar: { date: string; count: nu
}
}
- return weekdayNames[maxDay];
+ return totals[maxDay] > 0 ? weekdayNames[maxDay] : null;
}