diff --git a/.eslintrc.js b/.eslintrc.js
index 9a9d75a3..23110d32 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -43,14 +43,6 @@ module.exports = {
rules: {
// eslint:recommended
"arrow-body-style": ["error", "as-needed"],
- "capitalized-comments": [
- "error",
- "never",
- {
- ignorePattern: "pragma|ignored",
- ignoreInlineComments: true,
- },
- ],
curly: ["error", "all"],
"dot-notation": "error",
eqeqeq: ["error", "always"],
diff --git a/src/constants.ts b/src/constants.ts
index d6f628b2..6c0e6105 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -1,11 +1,18 @@
+// OpenSauced constants
export const SUPABASE_LOGIN_URL = "https://ibcwmlhcimymasokhgvn.supabase.co/auth/v1/authorize?provider=github&redirect_to=https://insights.opensauced.pizza/";
export const SUPABASE_AUTH_COOKIE_NAME = "supabase-auth-token";
export const OPEN_SAUCED_AUTH_TOKEN_KEY = "os-access-token";
+export const OPEN_SAUCED_INSIGHTS_DOMAIN = "insights.opensauced.pizza";
+
+// API endpoints
export const OPEN_SAUCED_USERS_ENDPOINT = "https://api.opensauced.pizza/v1/users";
+export const OPEN_SAUCED_REPOS_ENDPOINT = "https://api.opensauced.pizza/v1/repos";
export const OPEN_SAUCED_SESSION_ENDPOINT = "https://api.opensauced.pizza/v1/auth/session";
-export const OPEN_SAUCED_INSIGHTS_DOMAIN = "insights.opensauced.pizza";
+
+// GitHub constants/selectors
export const GITHUB_PROFILE_MENU_SELECTOR = ".p-nickname.vcard-username.d-block";
export const GITHUB_PROFILE_EDIT_MENU_SELECTOR = "button.js-profile-editable-edit-button";
export const GITHUB_PR_AUTHOR_USERNAME_SELECTOR = "author Link--primary text-bold css-overflow-wrap-anywhere";
export const GITHUB_LOGGED_IN_USER_USERNAME_SELECTOR = "meta[name=\"user-login\"]";
export const GITHUB_PR_COMMENT_HEADER_SELECTOR = "timeline-comment-header clearfix d-flex";
+export const GITHUB_REPO_ACTIONS_SELECTOR = ".pagehead-actions";
diff --git a/src/content-scripts/components/RepoVoting/RepoUnvoteButton.ts b/src/content-scripts/components/RepoVoting/RepoUnvoteButton.ts
new file mode 100644
index 00000000..c82c124f
--- /dev/null
+++ b/src/content-scripts/components/RepoVoting/RepoUnvoteButton.ts
@@ -0,0 +1,36 @@
+import "../../content-scripts.css";
+import { createHtmlElement } from "../../../utils/createHtmlElement";
+import { getAuthToken } from "../../../utils/checkAuthentication";
+import { VoteRepoButton } from "./RepoVoteButton";
+import { voteOrUnvoteRepo } from "../../../utils/fetchOpenSaucedApiData";
+
+export const RepoUnvoteButton = (ownerName: string, repoName: string) => {
+ const repoUnvoteButton = createHtmlElement("li", {
+ className:
+ "text-white text-center hover:shadow-button bg-gradient-to-r from-[#e67e22] to-[#d35400] btn btn-sm",
+ innerHTML: `
+ Unvote
+
+ `,
+ });
+
+ repoUnvoteButton.addEventListener("click", async () => {
+ repoUnvoteButton.innerHTML = `
+ Loading...
+ `;
+ const userToken = await getAuthToken();
+
+ const unvoted = await voteOrUnvoteRepo(userToken, ownerName, repoName, false);
+
+ if (unvoted) {
+ const voteRepoButton = VoteRepoButton(ownerName, repoName);
+
+ repoUnvoteButton.replaceWith(voteRepoButton);
+ } else {
+ console.log("Something went wrong");
+ }
+ });
+
+
+ return repoUnvoteButton;
+};
diff --git a/src/content-scripts/components/RepoVoting/RepoVoteButton.ts b/src/content-scripts/components/RepoVoting/RepoVoteButton.ts
new file mode 100644
index 00000000..70b8e0fe
--- /dev/null
+++ b/src/content-scripts/components/RepoVoting/RepoVoteButton.ts
@@ -0,0 +1,35 @@
+import "../../content-scripts.css";
+import { createHtmlElement } from "../../../utils/createHtmlElement";
+import { getAuthToken } from "../../../utils/checkAuthentication";
+import { RepoUnvoteButton } from "./RepoUnvoteButton";
+import { voteOrUnvoteRepo } from "../../../utils/fetchOpenSaucedApiData";
+
+export const VoteRepoButton = (ownerName: string, repoName: string) => {
+ const voteRepoButton = createHtmlElement("li", {
+ className:
+ "text-white text-center hover:shadow-button bg-gradient-to-r from-[#e67e22] to-[#d35400] btn btn-sm",
+ innerHTML: `
+ Upvote
+
+ `,
+ });
+
+ voteRepoButton.addEventListener("click", async () => {
+ voteRepoButton.innerHTML = `
+ Loading...
+ `;
+ const userToken = await getAuthToken();
+
+ const voted = await voteOrUnvoteRepo(userToken, ownerName, repoName, false);
+
+ if (voted) {
+ const unvoteRepoButton = RepoUnvoteButton(ownerName, repoName);
+
+ voteRepoButton.replaceWith(unvoteRepoButton);
+ } else {
+ console.log("Something went wrong");
+ }
+ });
+
+ return voteRepoButton;
+};
diff --git a/src/content-scripts/github.ts b/src/content-scripts/github.ts
index f9d29d0a..fff6d6a2 100644
--- a/src/content-scripts/github.ts
+++ b/src/content-scripts/github.ts
@@ -2,12 +2,14 @@ import {
getGithubUsername,
isGithubProfilePage,
isGithubPullRequestPage,
+ isGithubRepoPage,
} from "../utils/urlMatchers";
import { isOpenSaucedUser } from "../utils/fetchOpenSaucedApiData";
import injectViewOnOpenSauced from "../utils/dom-utils/viewOnOpenSauced";
import injectInviteToOpenSauced from "../utils/dom-utils/inviteToOpenSauced";
import { prefersDarkMode } from "../utils/colorPreference";
import injectAddPRToHighlightsButton from "../utils/dom-utils/addPRToHighlights";
+import injectRepoVotingButtons from "../utils/dom-utils/repoVotingButtons";
import domUpdateWatch from "../utils/dom-utils/domUpdateWatcher";
const processGithubPage = async () => {
@@ -16,7 +18,7 @@ const processGithubPage = async () => {
}
if (isGithubPullRequestPage(window.location.href)) {
- injectAddPRToHighlightsButton();
+ await injectAddPRToHighlightsButton();
} else if (isGithubProfilePage(window.location.href)) {
const username = getGithubUsername(window.location.href);
@@ -28,6 +30,11 @@ const processGithubPage = async () => {
} else {
injectInviteToOpenSauced(username);
}
+ } else if (isGithubRepoPage(window.location.href)) {
+ const ownerName = getGithubUsername(window.location.href) ?? "";
+ const repoName = window.location.href.split("/").pop() ?? "";
+
+ await injectRepoVotingButtons(ownerName, repoName);
}
domUpdateWatch(processGithubPage, 25);
diff --git a/src/utils/checkAuthentication.ts b/src/utils/checkAuthentication.ts
index 741b39e8..91959f10 100644
--- a/src/utils/checkAuthentication.ts
+++ b/src/utils/checkAuthentication.ts
@@ -37,3 +37,6 @@ export const isLoggedIn = async () =>
// only a valid auth token can exist in the storage due to the check in line 23
Object.entries(await chrome.storage.sync.get(OPEN_SAUCED_AUTH_TOKEN_KEY)).length !== 0;
+export const getAuthToken = async () => (await chrome.storage.sync.get(OPEN_SAUCED_AUTH_TOKEN_KEY))[OPEN_SAUCED_AUTH_TOKEN_KEY];
+
+
diff --git a/src/utils/dom-utils/repoVotingButtons.ts b/src/utils/dom-utils/repoVotingButtons.ts
new file mode 100644
index 00000000..effc7a96
--- /dev/null
+++ b/src/utils/dom-utils/repoVotingButtons.ts
@@ -0,0 +1,42 @@
+import { GITHUB_REPO_ACTIONS_SELECTOR } from "../../constants";
+import { VoteRepoButton } from "../../content-scripts/components/RepoVoting/RepoVoteButton";
+import { RepoUnvoteButton } from "../../content-scripts/components/RepoVoting/RepoUnvoteButton";
+import { isLoggedIn, getAuthToken } from "../checkAuthentication";
+import {
+ checkUserVotedRepo,
+ repoExistsOnOpenSauced,
+} from "../fetchOpenSaucedApiData";
+
+const injectRepoVotingButtons = async (ownerName: string, repoName: string) => {
+ if (!(await isLoggedIn())) {
+ return;
+ }
+ if (!(await repoExistsOnOpenSauced(ownerName, repoName))) {
+ return;
+ }
+ const repoActions = document.querySelector(GITHUB_REPO_ACTIONS_SELECTOR);
+
+ if (!repoActions) {
+ return;
+ }
+
+ const voteRepoButton = VoteRepoButton(ownerName, repoName);
+ const repoUnvoteButton = RepoUnvoteButton(ownerName, repoName);
+ const userToken = await getAuthToken();
+
+ const userVotedRepo = await checkUserVotedRepo(userToken, repoName);
+
+ if (userVotedRepo) {
+ if (repoActions.lastChild?.isEqualNode(repoUnvoteButton)) {
+ return;
+ }
+ repoActions.appendChild(repoUnvoteButton);
+ } else {
+ if (repoActions.lastChild?.isEqualNode(voteRepoButton)) {
+ return;
+ }
+ repoActions.appendChild(voteRepoButton);
+ }
+};
+
+export default injectRepoVotingButtons;
diff --git a/src/utils/fetchOpenSaucedApiData.ts b/src/utils/fetchOpenSaucedApiData.ts
index 6509d388..2356ce47 100644
--- a/src/utils/fetchOpenSaucedApiData.ts
+++ b/src/utils/fetchOpenSaucedApiData.ts
@@ -1,5 +1,5 @@
import { cachedFetch } from "./cache";
-import { OPEN_SAUCED_USERS_ENDPOINT, OPEN_SAUCED_SESSION_ENDPOINT } from "../constants";
+import { OPEN_SAUCED_USERS_ENDPOINT, OPEN_SAUCED_SESSION_ENDPOINT, OPEN_SAUCED_REPOS_ENDPOINT } from "../constants";
export const isOpenSaucedUser = async (username: string) => {
try {
@@ -50,3 +50,52 @@ export const getUserPRData = async (userName: string, forceRefresh: boolean = fa
return resp?.json();
})
.then(json => json);
+
+const getUserVotes = async (userToken: string, page: number = 1, limit: number = 1000, repos: any[] = []): Promise => {
+ const response = await fetch(
+ `${OPEN_SAUCED_REPOS_ENDPOINT}/listUserVoted?page=${page}&limit=${limit}`,
+ {
+ method: "GET",
+ headers: { Authorization: `Bearer ${userToken}` },
+ },
+ );
+
+ if (response.status === 200) {
+ const votesData = await response.json();
+
+ const newRepos = repos.concat(votesData.data);
+
+ if (votesData.data.length === limit) {
+ return getUserVotes(userToken, page + 1, limit, newRepos);
+ }
+ return newRepos;
+ }
+ return repos;
+};
+
+
+export const checkUserVotedRepo = async (userToken: string, repoName: string) => {
+ const userVotes = await getUserVotes(userToken);
+
+ return userVotes.some((repo: any) => repo.name === repoName);
+};
+
+export const repoExistsOnOpenSauced = async (ownerName: string, repoName: string) => {
+ const response = await fetch(
+ `${OPEN_SAUCED_REPOS_ENDPOINT}/${ownerName}/${repoName}`,
+ );
+
+ return response.status === 200;
+};
+
+export const voteOrUnvoteRepo = async (userToken: string, ownerName: string, repoName: string, vote: boolean) => {
+ const response = await fetch(
+ `${OPEN_SAUCED_REPOS_ENDPOINT}/${ownerName}/${repoName}/vote`,
+ {
+ method: vote ? "POST" : "DELETE",
+ headers: { Authorization: `Bearer ${userToken}` },
+ },
+ );
+
+ return response.status === 200;
+};
diff --git a/src/utils/urlMatchers.ts b/src/utils/urlMatchers.ts
index 6961d269..9d3f8933 100644
--- a/src/utils/urlMatchers.ts
+++ b/src/utils/urlMatchers.ts
@@ -31,3 +31,10 @@ export const isGithubProfilePage = (url: string) => {
return githubProfilePattern.test(url);
};
+
+
+export const isGithubRepoPage = (url: string) => {
+ const githubRepoPattern = /github\.com\/[^/]+\/[^/]+$/;
+
+ return githubRepoPattern.test(url);
+};