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: `
+ 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: ``,
+ })
+ : "";
+ const twitterIcon = tweetHref
+ ? createHtmlElement("a", {
+ href: tweetHref,
+ innerHTML: ``,
+ })
+ : "";
+ const linkedInIcon = linkedinHref
+ ? createHtmlElement("a", {
+ href: linkedinHref,
+ innerHTML: ``,
+ })
+ : "";
+
+ 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: `
+
+
+
+ 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: [],