From 0c8f22286475edeff53f9226f71e46bbb338287f Mon Sep 17 00:00:00 2001 From: Anush Date: Fri, 21 Apr 2023 19:29:09 +0530 Subject: [PATCH] feat: Invite to OpenSauced (#20) * refactor: Moved matchers to a separate dir * chore: Updated getOpenSaucedUser() to return a bool * refactor: Moved injectViewOnOpenSauced to utils * feat: Invite to OS(WIP) * chore: Removed unused listener * chore: Fomatting * chore: Fomatting * chore: Added social icons span * chore: Removed explicit book return and err handling * Update src/components/InviteToOpenSauced/InviteToOpenSaucedModal.ts Co-authored-by: Nick Taylor * chore: Top level await for getOpenSaucedUser() * chores: Aggregated matchers, updated file-names * chore: createHtmlElement() with added typings * chore: Updated elements to use the new createHtmlElement() * refactor: restructured utilities * chore: remove config imports for now * chore: Improved typing in createHtmlElement() and formatting * chore: Added conditional rendering of social share icons * chore: updated injector functions for empty bio * chore: Updated button-text sizing * chore: Updated LinkedIn share button href * chore: updated null checks * chore: Updated inviteToOS button colors * chore: Update LinkedIn username matcher and modal sizing * Apply copy suggestions from code review * Apply injectViewOnOS rename suggestions from code review * refactor: renamedViewOnOpenSauced() * refactor: updated matcher file name and imports * refactor: Moved the modal display trigger to the component definition * chore: update matchers filename to urlMatchers --------- Co-authored-by: Nick Taylor Co-authored-by: Brian Douglas --- src/assets/linkedin-icon.svg | 4 + src/assets/mail-icon.svg | 1 + src/assets/twitter-icon.svg | 1 + .../InviteToOpenSaucedButton.ts | 20 ++++ .../InviteToOpenSaucedModal.ts | 97 +++++++++++++++++++ .../ViewOnOpenSaucedButton.ts | 18 ++-- src/content-scripts/profileScreen.ts | 32 ++---- src/utils/createHtmlElement.ts | 21 ++++ src/utils/dom-utils/inviteToOpenSauced.ts | 32 ++++++ src/utils/dom-utils/viewOnOpenSauced.ts | 13 +++ src/utils/fetchOpenSaucedApiData.ts | 13 +-- src/utils/getDetailsFromGithubUrl.ts | 5 - src/utils/urlMatchers.ts | 18 ++++ tailwind.config.js | 3 + 14 files changed, 236 insertions(+), 42 deletions(-) create mode 100644 src/assets/linkedin-icon.svg create mode 100644 src/assets/mail-icon.svg create mode 100644 src/assets/twitter-icon.svg create mode 100644 src/components/InviteToOpenSauced/InviteToOpenSaucedButton.ts create mode 100644 src/components/InviteToOpenSauced/InviteToOpenSaucedModal.ts create mode 100644 src/utils/createHtmlElement.ts create mode 100644 src/utils/dom-utils/inviteToOpenSauced.ts create mode 100644 src/utils/dom-utils/viewOnOpenSauced.ts delete mode 100644 src/utils/getDetailsFromGithubUrl.ts create mode 100644 src/utils/urlMatchers.ts diff --git a/src/assets/linkedin-icon.svg b/src/assets/linkedin-icon.svg new file mode 100644 index 00000000..8b315825 --- /dev/null +++ b/src/assets/linkedin-icon.svg @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/src/assets/mail-icon.svg b/src/assets/mail-icon.svg new file mode 100644 index 00000000..45974dcb --- /dev/null +++ b/src/assets/mail-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/twitter-icon.svg b/src/assets/twitter-icon.svg new file mode 100644 index 00000000..225fa1e5 --- /dev/null +++ b/src/assets/twitter-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/InviteToOpenSauced/InviteToOpenSaucedButton.ts b/src/components/InviteToOpenSauced/InviteToOpenSaucedButton.ts new file mode 100644 index 00000000..546f434d --- /dev/null +++ b/src/components/InviteToOpenSauced/InviteToOpenSaucedButton.ts @@ -0,0 +1,20 @@ +import logoIcon from "../../assets/opensauced-icon.svg"; +import "../../index.css"; +import { createHtmlElement } from "../../utils/createHtmlElement"; + +export const InviteToOpenSaucedButton = () => { + const inviteToOpenSaucedButton = createHtmlElement("a", { + className: + "inline-block mt-4 text-white rounded-md p-2 text-sm font-semibold text-center select-none w-full border border-solid cursor-pointer bg-gh-gray hover:bg-red-500 hover:shadow-button hover:no-underline", + innerHTML: `OpenSauced Logo + Invite to OpenSauced + `, + }); + return inviteToOpenSaucedButton; +}; diff --git a/src/components/InviteToOpenSauced/InviteToOpenSaucedModal.ts b/src/components/InviteToOpenSauced/InviteToOpenSaucedModal.ts new file mode 100644 index 00000000..554589ba --- /dev/null +++ b/src/components/InviteToOpenSauced/InviteToOpenSaucedModal.ts @@ -0,0 +1,97 @@ +import "../../index.css"; +import { createHtmlElement } from "../../utils/createHtmlElement"; +import emailSocialIcon from "../../assets/mail-icon.svg"; +import twitterSocialIcon from "../../assets/twitter-icon.svg"; +import linkedInSocailIcon from "../../assets/linkedin-icon.svg"; + +interface Socials { + emailAddress?: string; + twitterUsername?: string; + linkedInUsername?: string; +} + +export const InviteToOpenSaucedModal = ( + username: string, + { emailAddress, twitterUsername, linkedInUsername }: Socials = {}, modalDisplayTrigger?: HTMLElement +) => { + const emailBody = + typeof emailAddress === "string" && + `Hey ${username}. I'm using OpenSauced to keep track of your contributions and discover new projects. Check it out at https://hot.opensauced.pizza/`; + const emailHref = + typeof emailAddress === "string" && + `mailto:${emailAddress}?subject=${encodeURIComponent( + "Invitation to join OpenSauced!" + )}&body=${encodeURIComponent(emailBody)}`; + const tweetHref = + typeof twitterUsername === "string" && + `https://twitter.com/intent/tweet?text=${encodeURIComponent( + `Check out @saucedopen. The platform for open source contributors to find their next contribution. https://opensauced.pizza/blog/social-coding-is-back. @${twitterUsername}` + )}&hashtags=opensource,github`; + const linkedinHref = + typeof linkedInUsername === "string" && + `https://www.linkedin.com/in/${linkedInUsername}`; + + const emailIcon = emailBody + ? createHtmlElement("a", { + href: emailHref, + innerHTML: `Email`, + }) + : ""; + const twitterIcon = tweetHref + ? createHtmlElement("a", { + href: tweetHref, + innerHTML: `Twitter`, + }) + : ""; + const linkedInIcon = linkedinHref + ? createHtmlElement("a", { + href: linkedinHref, + innerHTML: `LinkedIn`, + }) + : ""; + + const socialIcons = createHtmlElement("span", { + className: "flex flex-nowrap space-x-3", + }); + + const inviteToOpenSaucedModal = createHtmlElement("div", { + className: + "fixed h-full w-full z-50 bg-gray-600 bg-opacity-50 overflow-y-auto inset-0", + style: { display: "none" }, + id: "invite-modal", + }); + + const inviteToOpenSaucedModalContainer = createHtmlElement("div", { + className: + "mt-2 min-w-[33%] relative top-60 mx-auto p-4 border w-96 rounded-md shadow-button border-solid border-orange bg-slate-800", + innerHTML: ` +

Invite ${username} to OpenSauced!

+
+

+ Use the links below to invite them. +

+
+`, + }); + + inviteToOpenSaucedModal.onclick = (e) => { + if (e.target === inviteToOpenSaucedModal) + inviteToOpenSaucedModal.style.display = "none"; + }; + + if (modalDisplayTrigger) modalDisplayTrigger.onclick = () => { + inviteToOpenSaucedModal.style.display = "block"; + }; + + socialIcons.replaceChildren(emailIcon, twitterIcon, linkedInIcon); + inviteToOpenSaucedModalContainer.appendChild(socialIcons); + inviteToOpenSaucedModal.appendChild(inviteToOpenSaucedModalContainer); + + return inviteToOpenSaucedModal; +}; diff --git a/src/components/ViewOnOpenSaucedButton/ViewOnOpenSaucedButton.ts b/src/components/ViewOnOpenSaucedButton/ViewOnOpenSaucedButton.ts index c84603e5..8489f670 100644 --- a/src/components/ViewOnOpenSaucedButton/ViewOnOpenSaucedButton.ts +++ b/src/components/ViewOnOpenSaucedButton/ViewOnOpenSaucedButton.ts @@ -1,14 +1,15 @@ import logoIcon from "../../assets/opensauced-icon.svg"; import "../../index.css"; +import { createHtmlElement } from "../../utils/createHtmlElement"; export const ViewOnOpenSaucedButton = (username: string) => { - const viewOnOpenSaucedButton = document.createElement("a"); - viewOnOpenSaucedButton.href = `https://insights.opensauced.pizza/user/${username}/contributions`; - viewOnOpenSaucedButton.className = - "inline-block mt-4 mb-1 text-white rounded-md p-2 no-underline text-md font-semibold text-center select-none w-full border border-solid cursor-pointer border-orange hover:shadow-button hover:no-underline"; - viewOnOpenSaucedButton.target = "_blank"; - viewOnOpenSaucedButton.rel = "noopener noreferrer"; - viewOnOpenSaucedButton.innerHTML = ` + const viewOnOpenSaucedButton = createHtmlElement("a", { + href: `https://insights.opensauced.pizza/user/${username}/contributions`, + className: + "inline-block mt-4 text-white rounded-md p-2 text-sm font-semibold text-center select-none w-full border border-solid cursor-pointer border-orange hover:shadow-button hover:no-underline", + target: "_blank", + rel: "noopener noreferrer", + innerHTML: ` { height="20" /> View On OpenSauced - `; + `, + }); return viewOnOpenSaucedButton; }; diff --git a/src/content-scripts/profileScreen.ts b/src/content-scripts/profileScreen.ts index b67543b2..093a0799 100644 --- a/src/content-scripts/profileScreen.ts +++ b/src/content-scripts/profileScreen.ts @@ -1,25 +1,11 @@ -import { getGithubUsername } from "../utils/getDetailsFromGithubUrl"; +import { getGithubUsername } from "../utils/urlMatchers"; import { getOpenSaucedUser } from "../utils/fetchOpenSaucedApiData"; -import { ViewOnOpenSaucedButton } from "../components/ViewOnOpenSaucedButton/ViewOnOpenSaucedButton"; - -function injectViewOnOpenSaucedButton() { - const username = getGithubUsername(window.location.href); - if (!username) { - return; - } - - const openSaucedUser = getOpenSaucedUser(username); - if (!openSaucedUser) { - return; - } - - const viewOnOpenSaucedButton = ViewOnOpenSaucedButton(username); - - const userBio = document.querySelector(".p-note.user-profile-bio"); - if (!userBio) { - return; - } - userBio.appendChild(viewOnOpenSaucedButton); +import injectViewOnOpenSauced from "../utils/dom-utils/viewOnOpenSauced"; +import injectInviteToOpenSauced from "../utils/dom-utils/inviteToOpenSauced"; + +const username = getGithubUsername(window.location.href); +if (username != null) { + const openSaucedUser = await getOpenSaucedUser(username); + if (openSaucedUser) injectViewOnOpenSauced(username); + else injectInviteToOpenSauced(username); } - -injectViewOnOpenSaucedButton(); diff --git a/src/utils/createHtmlElement.ts b/src/utils/createHtmlElement.ts new file mode 100644 index 00000000..3e775423 --- /dev/null +++ b/src/utils/createHtmlElement.ts @@ -0,0 +1,21 @@ +import { CSSProperties } from "react"; + +type ElementProps = { + style?: CSSProperties; + [key: string]: any; +}; + +type CssDeclaration = keyof Omit; + +export function createHtmlElement( + nodeName: T, + props: ElementProps +) { + const { style, ...nonStyleProps } = props; + const element = Object.assign(document.createElement(nodeName), props); + if (style != undefined) + Object.entries(style).forEach(([key, value]) => { + element.style[key as CssDeclaration] = value; + }); + return element; +} diff --git a/src/utils/dom-utils/inviteToOpenSauced.ts b/src/utils/dom-utils/inviteToOpenSauced.ts new file mode 100644 index 00000000..b53f36e9 --- /dev/null +++ b/src/utils/dom-utils/inviteToOpenSauced.ts @@ -0,0 +1,32 @@ +import { InviteToOpenSaucedButton } from "../../components/InviteToOpenSauced/InviteToOpenSaucedButton"; +import { InviteToOpenSaucedModal } from "../../components/InviteToOpenSauced/InviteToOpenSaucedModal"; +import { getTwitterUsername, getLinkedInUsername } from "../urlMatchers"; + +const injectOpenSaucedInviteButton = (username: string) => { + const emailAddress: string | undefined = ( + document.querySelector(`a[href^="mailto:"]`) as HTMLAnchorElement + )?.href.substr(7); + const twitterUrl: string | undefined = ( + document.querySelector(`a[href*="twitter.com"]`) as HTMLAnchorElement + )?.href; + const linkedInUrl: string | undefined = ( + document.querySelector(`a[href*="linkedin.com"]`) as HTMLAnchorElement + )?.href; + if (!(emailAddress || twitterUrl || linkedInUrl)) return; + + const twitterUsername = twitterUrl && getTwitterUsername(twitterUrl); + const linkedInUsername = linkedInUrl && getLinkedInUsername(linkedInUrl); + const inviteToOpenSaucedButton = InviteToOpenSaucedButton(); + const inviteToOpenSaucedModal = InviteToOpenSaucedModal(username, { + emailAddress, + twitterUsername, + linkedInUsername, + }, inviteToOpenSaucedButton); + + const userBio = document.querySelector(".p-nickname.vcard-username.d-block"); + if (!userBio || !userBio.parentNode) return; + userBio.parentNode.replaceChild(inviteToOpenSaucedButton, userBio); + document.body.appendChild(inviteToOpenSaucedModal); +}; + +export default injectOpenSaucedInviteButton; diff --git a/src/utils/dom-utils/viewOnOpenSauced.ts b/src/utils/dom-utils/viewOnOpenSauced.ts new file mode 100644 index 00000000..70aa0961 --- /dev/null +++ b/src/utils/dom-utils/viewOnOpenSauced.ts @@ -0,0 +1,13 @@ +import { ViewOnOpenSaucedButton } from "../../components/ViewOnOpenSaucedButton/ViewOnOpenSaucedButton"; + +const injectViewOnOpenSaucedButton = (username: string) => { + const viewOnOpenSaucedButton = ViewOnOpenSaucedButton(username); + + const userBio = document.querySelector( + ".p-nickname.vcard-username.d-block, button.js-profile-editable-edit-button" + ); + if (!userBio || !userBio.parentNode) return; + userBio.parentNode.replaceChild(viewOnOpenSaucedButton, userBio); +}; + +export default injectViewOnOpenSaucedButton; diff --git a/src/utils/fetchOpenSaucedApiData.ts b/src/utils/fetchOpenSaucedApiData.ts index a8e00d9b..10838092 100644 --- a/src/utils/fetchOpenSaucedApiData.ts +++ b/src/utils/fetchOpenSaucedApiData.ts @@ -1,11 +1,12 @@ export const getOpenSaucedUser = async (username: string) => { - const response = await fetch( - `https://api.opensauced.pizza/v1/users/${username}` - ); - if (response.status !== 200) { - return null; + try { + const response = await fetch( + `https://api.opensauced.pizza/v1/users/${username}` + ); + return response.status === 200; + } catch (error) { + return false; } - return await response.json(); }; export const checkTokenValidity = async (token: string) => { diff --git a/src/utils/getDetailsFromGithubUrl.ts b/src/utils/getDetailsFromGithubUrl.ts deleted file mode 100644 index 84fa4371..00000000 --- a/src/utils/getDetailsFromGithubUrl.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const getGithubUsername = (url: string) => { - const match = url.match(/github\.com\/([^/]+)/); - return match && match[1]; -}; - diff --git a/src/utils/urlMatchers.ts b/src/utils/urlMatchers.ts new file mode 100644 index 00000000..7a51e5b7 --- /dev/null +++ b/src/utils/urlMatchers.ts @@ -0,0 +1,18 @@ +export const getGithubUsername = (url: string) => { + const match = url.match(/github\.com\/([^/]+)/); + return match && match[1]; +}; + +export const getLinkedInUsername = (url: string) => { + const match = url.match( + /(?:https?:\/\/)?(?:www\.)?linkedin\.com\/in\/(?:#!\/)?@?([^\/\?\s]*)/ + ); + return match ? match[1] : undefined; +}; + +export const getTwitterUsername = (url: string) => { + const match = url.match( + /(?:https?:\/\/)?(?:www\.)?twitter\.com\/(?:#!\/)?@?([^\/\?\s]*)/ + ); + return match ? match[1] : undefined; +}; diff --git a/tailwind.config.js b/tailwind.config.js index ebaf0e24..6912a3ed 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -9,6 +9,9 @@ module.exports = { boxShadow: { button: "0 0 0.2rem 0.2rem rgb(245, 131, 106, 0.2)", }, + backgroundColor: { + "gh-gray": "#21262d", + } }, }, plugins: [],