From c8da2c157cd9c6040f57a91c1bd7326b4f373e92 Mon Sep 17 00:00:00 2001 From: abhimanyurajeesh Date: Wed, 2 Apr 2025 22:08:24 +0530 Subject: [PATCH 01/10] Add weekly grouping option to EOD settings and update activity display logic --- src/app/page.tsx | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index efab6da..ea23645 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -106,7 +106,7 @@ export default function Home() { }); const includeInEOD = ["Pull Requests Created", "Issues Created", "Issues Assigned", "Commits Made"]; - const EODSettings = ["Group by day"]; + const EODSettings = ["Group by day", "Group by week"]; const CheckboxGroup = Checkbox.Group; const [checkedList, setCheckedList] = useState(includeInEOD); const [eodSettingsCheckedList, setEodSettingsCheckedList] = useState([]); @@ -118,6 +118,7 @@ export default function Home() { const [settings, setSettings] = useState({ fetchGithubToken: githubTokenInput, splitByDay: false, + splitByWeek: false, }); const searchParams = useSearchParams(); @@ -201,6 +202,7 @@ export default function Home() { setSettings({ ...settings, splitByDay: eodSettingsCheckedList.includes("Group by day"), + splitByWeek: eodSettingsCheckedList.includes("Group by week"), }); setShowEODSettings(false); }; @@ -527,7 +529,24 @@ export default function Home() { let groupedActivitiesText = ""; - if (eodSettingsCheckedList.includes("Group by day")) { + if (eodSettingsCheckedList.includes("Group by week")) { + const weekGroups: { [key: string]: string[] } = {}; + Object.keys(groupedActivities).forEach((date) => { + if (groupedActivities[date].length === 0) return; + const weekStart = dayjs(date).startOf("week").format("YYYY-MM-DD"); + weekGroups[weekStart] ??= []; + weekGroups[weekStart].push(...groupedActivities[date]); + }); + + Object.keys(weekGroups).forEach((weekStart) => { + if (weekGroups[weekStart].length === 0) return; + const weekEnd = dayjs(weekStart).endOf("week").format("YYYY-MM-DD"); + groupedActivitiesText += `**Week of ${dayjs(weekStart).format("DD/MM/YYYY")} - ${dayjs(weekEnd).format( + "DD/MM/YYYY" + )}**\n${weekGroups[weekStart].join("\n")}\n\n`; + }); + eodMessage = eodMessage.replace("{{TODAY_ACTIVITIES}}", "\n" + groupedActivitiesText.trimEnd()); + } else if (eodSettingsCheckedList.includes("Group by day")) { Object.keys(groupedActivities).forEach((date) => { if (groupedActivities[date].length === 0) return; groupedActivitiesText += `**${dayjs(date).format("DD/MM/YYYY")}**\n${groupedActivities[date].join("\n")}\n\n`; From 75559cbbe24d16db8adf26fd1b86b3bbefe1f5e9 Mon Sep 17 00:00:00 2001 From: abhimanyurajeesh Date: Tue, 22 Apr 2025 11:33:25 +0530 Subject: [PATCH 02/10] Add PRs Merged tracking to activity report and update EOD settings --- src/app/page.tsx | 52 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index ea23645..04cee19 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -102,10 +102,11 @@ export default function Home() { issues_assigned: [], merged: [], commits: [], + prs_merged: [], error: "", }); - const includeInEOD = ["Pull Requests Created", "Issues Created", "Issues Assigned", "Commits Made"]; + const includeInEOD = ["Pull Requests Created", "Issues Created", "Issues Assigned", "Commits Made", "PRs Merged"]; const EODSettings = ["Group by day", "Group by week"]; const CheckboxGroup = Checkbox.Group; const [checkedList, setCheckedList] = useState(includeInEOD); @@ -323,7 +324,7 @@ export default function Home() { } notify("info", "Fetching", "Fetching your GitHub stats"); - const params = new URLSearchParams(searchParams); + const params = new URLSearchParams(searchParams.toString()); const numDays = dayjs(dateField.value.endDate).diff(dayjs(dateField.value.startDate), "day"); params.set("username", usernameField.value); params.set("org", orgFilter.value); @@ -351,6 +352,15 @@ export default function Home() { pr.type = "pr-created"; } + // Fetch merged PRs + const mergedPRData = await ghfetch( + `https://api.github.com/search/issues?q=author:${usernameField.value}+is:pr+is:merged+merged:${dateField.value.startDate}..${dateField.value.endDate}${orgFilterQuery}&per_page=100` + ); + + for (const pr of mergedPRData.items) { + pr.type = "pr-merged"; + } + const issuesAssignedData = await ghfetch( `https://api.github.com/search/issues?q=assignee:${usernameField.value}+is:issue+created:${dateField.value.startDate}..${dateField.value.endDate}${orgFilterQuery}&per_page=100` ); @@ -405,9 +415,22 @@ export default function Home() { if (!isLinkedPRPresent) commits.push(commit); } - const mergedTimeline: any = [...issuesData.items, ...prData.items, ...assignedIssues, ...commits].sort( - (a: { created_at: string; assigned_at?: string }, b: { created_at: string; assigned_at?: string }) => - dayjs(a.assigned_at || a.created_at).isAfter(dayjs(b.assigned_at || b.created_at)) ? -1 : 1 + const mergedTimeline: any = [ + ...issuesData.items, + ...prData.items, + ...mergedPRData.items, + ...assignedIssues, + ...commits, + ].sort( + ( + a: { created_at: string; assigned_at?: string; merged_at?: string }, + b: { created_at: string; assigned_at?: string; merged_at?: string } + ) => + dayjs(a.merged_at || a.assigned_at || a.created_at).isAfter( + dayjs(b.merged_at || b.assigned_at || b.created_at) + ) + ? -1 + : 1 ); setActivity({ @@ -416,6 +439,7 @@ export default function Home() { issues_assigned: assignedIssues, merged: mergedTimeline, commits: commits, + prs_merged: mergedPRData.items, error: "", }); setFetchBtnState("success"); @@ -445,6 +469,12 @@ export default function Home() { ); + case "pr-merged": + return ( + + + + ); case "commit-created": return ( @@ -526,6 +556,18 @@ export default function Home() { }) ); } + if (checkedList.includes("PRs Merged")) { + todayActivities = todayActivities.concat( + activity.prs_merged.map((pr: any) => { + const key = dayjs(pr.merged_at || pr.created_at).format("YYYY-MM-DD"); + groupedActivities[key] ??= []; + groupedActivities[key].push( + `- Merged PR [${getRepoName(pr.html_url)}#${pr.number}](${pr.html_url}): ${pr.title}` + ); + return `- Merged PR [${getRepoName(pr.html_url)}#${pr.number}](${pr.html_url}): ${pr.title}`; + }) + ); + } let groupedActivitiesText = ""; From 5652e9f3fc5ec87ea46f8252804a1ca1e913928a Mon Sep 17 00:00:00 2001 From: abhimanyurajeesh Date: Thu, 29 May 2025 11:33:42 +0530 Subject: [PATCH 03/10] Add repository and branch filters to GitHub activity report; enhance error handling and UI elements --- src/app/page.tsx | 221 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 194 insertions(+), 27 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 04cee19..d123632 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -96,6 +96,26 @@ export default function Home() { error: "", }); + const [repoFilter, setRepoFilter] = useState<{ + list: { name: string }[]; + value: string; + error: string; + }>({ + list: [], + value: "", + error: "", + }); + + const [branchFilter, setBranchFilter] = useState<{ + list: { name: string }[]; + value: string; + error: string; + }>({ + list: [], + value: "", + error: "", + }); + const [activity, setActivity] = useState({ prs: [], issues_created: [], @@ -130,14 +150,18 @@ export default function Home() { const headers: { [key: string]: string } = {}; if (settings.fetchGithubToken) { headers["Authorization"] = `token ${settings.fetchGithubToken}`; + headers["Accept"] = "application/vnd.github.v3+json"; + console.log("Making request to:", url); } const res = await fetch(url, { headers, }); - const data = await res.json(); - if (data?.message) { - throw new Error(data.message); + if (!res.ok) { + const errorData = await res.json(); + throw new Error(`GitHub API Error: ${errorData.message}`); } + const data = await res.json(); + console.log("Response data:", data); return data; }; @@ -191,12 +215,25 @@ export default function Home() { }; const saveFetchSettings = () => { + const token = githubTokenInput.trim(); + if (!token) { + notify("error", "Invalid Token", "Please enter a valid GitHub token"); + return; + } + setSettings({ ...settings, - fetchGithubToken: githubTokenInput, + fetchGithubToken: token, }); - localStorage.setItem("githubToken", githubTokenInput); + localStorage.setItem("githubToken", token); setShowFetchSettings(false); + + // Recheck username if it exists + if (usernameField.value) { + setTimeout(() => { + checkGithubUsername(usernameField.value); + }, 100); + } }; const saveEODSettings = () => { @@ -253,17 +290,18 @@ export default function Home() { state: "checking", }); try { - const orgRes = await ghfetch(`https://api.github.com/users/${username}/orgs`); - if (orgRes?.message === "Not Found") { - setUsernameField({ - ...usernameField, - value: username, - error: "Username not found", - state: "checked", - }); - return false; - } else { + // First verify the user + const userRes = await ghfetch(`https://api.github.com/user`); + console.log("User data:", userRes); + + // Then get their organizations + const orgRes = await ghfetch(`https://api.github.com/user/orgs`); + console.log("Org data:", orgRes); + + if (Array.isArray(orgRes)) { const orgList = orgRes.map((org: { login: string }) => ({ login: org.login })); + console.log("Processed org list:", orgList); + if (defaultOrg && orgList.find((org: { login: string }) => org.login === defaultOrg)) { setOrgFilter({ value: defaultOrg, @@ -277,20 +315,63 @@ export default function Home() { error: "", }); } + setUsernameField({ ...usernameField, value: username, error: "", state: "checked", }); + + notify("success", "Organizations Found", `Found ${orgList.length} organizations`); return true; + } else { + throw new Error("Invalid response from GitHub API"); } } catch (error: any) { - notify("error", "Error", "Something went wrong while fetching your organizations: " + error.message); + console.error("GitHub API Error:", error); + notify("error", "Error", `Failed to fetch organizations: ${error.message}`); + setUsernameField({ + ...usernameField, + value: username, + error: error.message, + state: "checked", + }); } return false; }; + const fetchRepositories = async (org: string) => { + try { + const repoRes = await ghfetch(`https://api.github.com/orgs/${org}/repos`); + setRepoFilter({ + value: "", + list: repoRes.map((repo: { name: string }) => ({ name: repo.name })), + error: "", + }); + setBranchFilter({ + value: "", + list: [], + error: "", + }); + } catch (error: any) { + notify("error", "Error", "Something went wrong while fetching repositories: " + error.message); + } + }; + + const fetchBranches = async (org: string, repo: string) => { + try { + const branchRes = await ghfetch(`https://api.github.com/repos/${org}/${repo}/branches`); + setBranchFilter({ + value: "", + list: branchRes.map((branch: { name: string }) => ({ name: branch.name })), + error: "", + }); + } catch (error: any) { + notify("error", "Error", "Something went wrong while fetching branches: " + error.message); + } + }; + async function getLinkedPRs(issue_url: string) { if (!issue_url) return []; const linkedPRQuery = await fetch("/api/linkedprs", { @@ -389,10 +470,22 @@ export default function Home() { } } - const commitsData = await ghfetch( - `https://api.github.com/search/commits?q=author:${usernameField.value}+committer-date:${dateField.value.startDate}..${dateField.value.endDate}${orgFilterQuery}&per_page=100` - ); - const myCommits = commitsData.items.filter( + // Update commit fetching to use repository and branch if selected + let commitsUrl = `https://api.github.com/search/commits?q=author:${usernameField.value}+committer-date:${dateField.value.startDate}..${dateField.value.endDate}`; + + if (orgFilter.value && repoFilter.value) { + commitsUrl = `https://api.github.com/repos/${orgFilter.value}/${repoFilter.value}/commits?author=${usernameField.value}&since=${dateField.value.startDate}&until=${dateField.value.endDate}`; + if (branchFilter.value) { + commitsUrl += `&sha=${branchFilter.value}`; + } + } else if (orgFilter.value) { + commitsUrl += `+org:${orgFilter.value}`; + } + + commitsUrl += "&per_page=100"; + + const commitsData = await ghfetch(commitsUrl); + const myCommits = (commitsData.items || commitsData).filter( (commit: any) => commit.committer?.login?.toLowerCase() === usernameField.value.toLowerCase() ); const commits: any = []; @@ -494,6 +587,25 @@ export default function Home() { return url.replace("https://github.com/", "")?.split("/")?.[1] || ""; } + function getRepoNameFromUrl(url: string | undefined): string { + if (!url) return ""; + const parts = url.split("/"); + if (url.includes("/repos/")) { + const repoIndex = parts.indexOf("repos"); + if (repoIndex >= 0 && parts.length > repoIndex + 2) { + return parts[repoIndex + 2]; + } + } + // For html_urls + if (url.includes("github.com/")) { + const githubIndex = parts.indexOf("github.com"); + if (githubIndex >= 0 && parts.length > githubIndex + 2) { + return parts[githubIndex + 2]; + } + } + return ""; + } + function getEODMessage() { let eodMessage = EOD_TEMPLATE; if (dayjs(dateField.value.startDate).format("YYYY-MM-DD") === dayjs(dateField.value.endDate).format("YYYY-MM-DD")) @@ -628,10 +740,32 @@ export default function Home() { This is a simple EOD update generator that will help you to create a summary of your GitHub activity +
+
+ GitHub Username + Enter your GitHub username +
+
+ Organization + Select organization +
+
+ Repository + Select repository +
+
+ Branch + Select branch +
+
+ Date Range + Select time period +
+
{ setUsernameField({ ...usernameField, value: e.target.value, error: "" }); @@ -656,17 +790,48 @@ export default function Home() { /> { + setRepoFilter({ ...repoFilter, value: e }); + fetchBranches(orgFilter.value, e); + }} + value={repoFilter.value} + filterOption={(input, option) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase())} + options={repoFilter.list.map((repo: { name: string }) => ({ label: repo.name, value: repo.name }))} + disabled={!orgFilter.value || repoFilter.list.length === 0} + /> + { - setUsernameField({ ...usernameField, value: e.target.value, error: "" }); - }} - status={usernameField.error ? "error" : undefined} - suffix={ - (usernameField.state === "checking" && ) || - (usernameField.value && - usernameField.state === "checked" && - (usernameField.error ? ( - - ) : ( - - ))) || - } - onBlur={() => { - checkGithubUsername(usernameField.value); - }} - onFocus={() => { - setUsernameField({ ...usernameField, state: "focus" }); - }} - /> - { - setRepoFilter({ ...repoFilter, value: e }); - fetchBranches(orgFilter.value, e); - }} - value={repoFilter.value} - filterOption={(input, option) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase())} - options={repoFilter.list.map((repo: { name: string }) => ({ label: repo.name, value: repo.name }))} - disabled={!orgFilter.value || repoFilter.list.length === 0} - /> - { + setUsernameField({ ...usernameField, value: e.target.value, error: "" }); + }} + status={usernameField.error ? "error" : undefined} + suffix={ + (usernameField.state === "checking" && ) || + (usernameField.value && + usernameField.state === "checked" && + (usernameField.error ? ( + + ) : ( + + ))) || + } + onBlur={() => { + checkGithubUsername(usernameField.value); + }} + onFocus={() => { + setUsernameField({ ...usernameField, state: "focus" }); + }} + /> - )} +
+
+ Organization + Select organization +
+ { + setRepoFilter({ ...repoFilter, value: e }); + fetchBranches(orgFilter.value, e); + }} + value={repoFilter.value} + filterOption={(input, option) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase())} + options={repoFilter.list.map((repo: { name: string }) => ({ label: repo.name, value: repo.name }))} + disabled={!orgFilter.value || repoFilter.list.length === 0} + /> +
+
+
+ Branch + Select branch +
+ { + const now = dayjs(); + let startDate; + switch (value) { + case "1": + startDate = now.subtract(1, "day"); + break; + case "7": + startDate = now.subtract(7, "days"); + break; + case "14": + startDate = now.subtract(14, "days"); + break; + case "90": + startDate = now.subtract(90, "days"); + break; + default: + return; + } + setDateField({ + ...dateField, + error: "", + value: { + startDate: startDate.startOf("day").toISOString(), + endDate: now.endOf("day").toISOString(), + }, + }); + }} + options={[ + { label: "Last 1 Day", value: "1" }, + { label: "Last 7 Days", value: "7" }, + { label: "Last 14 Days", value: "14" }, + { label: "Last 90 Days", value: "90" }, + ]} + /> +
+ + +
+ + +
+
+ + {/* Error messages */} +
+ + {usernameField.error} + + + {orgFilter.error} + + + {dateField.error} + +
+ + + {/* Activity Timeline */} {fetchBtnState === "success" && (activity.merged.length > 0 ? ( -
+
({ label: ( -
+
{getRepoNameFromUrl(activity.repository_url || activity.html_url) || "-"} {"#" + (activity.number || activity.sha?.slice(0, 6))} {" "} -

+

{dayjs(activity.merged_at || activity.assigned_at || activity.created_at).format( - "Do MMMM YYYY h:mm A" + "DD MMM, h:mm A" )}

), color: "green", dot: getTimelineDot(activity.type), - children: activity.title, + children:
{activity.title}
, }))} /> @@ -923,19 +1024,28 @@ export default function Home() { No activity found
))} + + {/* EOD Options */} {activity.merged.length > 0 && ( <>

Include in EOD

-
-
- - Include All - - +
+
+
+ + Include All + + +
- +
)} + + {/* EOD Preview */} {EODMessage && ( -
-