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); +};