Skip to content

Commit

Permalink
feat: Invite to OpenSauced (#20)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* 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 <[email protected]>
Co-authored-by: Brian Douglas <[email protected]>
  • Loading branch information
3 people authored Apr 21, 2023
1 parent d19034d commit 0c8f222
Show file tree
Hide file tree
Showing 14 changed files with 236 additions and 42 deletions.
4 changes: 4 additions & 0 deletions src/assets/linkedin-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/mail-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/twitter-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions src/components/InviteToOpenSauced/InviteToOpenSaucedButton.ts
Original file line number Diff line number Diff line change
@@ -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: `<img
class="mx-2 inline-block align-top"
src="${chrome.runtime.getURL(logoIcon)}"
alt="OpenSauced Logo"
width="20"
height="20"
/>
<span>Invite to OpenSauced</span>
`,
});
return inviteToOpenSaucedButton;
};
97 changes: 97 additions & 0 deletions src/components/InviteToOpenSauced/InviteToOpenSaucedModal.ts
Original file line number Diff line number Diff line change
@@ -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: `<img src=${chrome.runtime.getURL(
emailSocialIcon
)} alt="Email">`,
})
: "";
const twitterIcon = tweetHref
? createHtmlElement("a", {
href: tweetHref,
innerHTML: `<img src=${chrome.runtime.getURL(
twitterSocialIcon
)} alt="Twitter">`,
})
: "";
const linkedInIcon = linkedinHref
? createHtmlElement("a", {
href: linkedinHref,
innerHTML: `<img src=${chrome.runtime.getURL(
linkedInSocailIcon
)} alt="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: `
<h3 class="text-2xl leading-6 font-bold">Invite ${username} to <a href="https://hot.opensauced.pizza/"><span class="hover:text-orange hover:underline">OpenSauced!</span></a></h3>
<div class="mt-2">
<p class="text-md">
Use the links below to invite them.
</p>
</div>
`,
});

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;
};
18 changes: 10 additions & 8 deletions src/components/ViewOnOpenSaucedButton/ViewOnOpenSaucedButton.ts
Original file line number Diff line number Diff line change
@@ -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: `
<img
class="mx-2 inline-block align-top"
src="${chrome.runtime.getURL(logoIcon)}"
Expand All @@ -17,6 +18,7 @@ export const ViewOnOpenSaucedButton = (username: string) => {
height="20"
/>
<span>View On OpenSauced</span>
`;
`,
});
return viewOnOpenSaucedButton;
};
32 changes: 9 additions & 23 deletions src/content-scripts/profileScreen.ts
Original file line number Diff line number Diff line change
@@ -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();
21 changes: 21 additions & 0 deletions src/utils/createHtmlElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { CSSProperties } from "react";

type ElementProps = {
style?: CSSProperties;
[key: string]: any;
};

type CssDeclaration = keyof Omit<CSSStyleDeclaration, "length" | "parentRule">;

export function createHtmlElement<T extends keyof HTMLElementTagNameMap>(
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;
}
32 changes: 32 additions & 0 deletions src/utils/dom-utils/inviteToOpenSauced.ts
Original file line number Diff line number Diff line change
@@ -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;
13 changes: 13 additions & 0 deletions src/utils/dom-utils/viewOnOpenSauced.ts
Original file line number Diff line number Diff line change
@@ -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;
13 changes: 7 additions & 6 deletions src/utils/fetchOpenSaucedApiData.ts
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down
5 changes: 0 additions & 5 deletions src/utils/getDetailsFromGithubUrl.ts

This file was deleted.

18 changes: 18 additions & 0 deletions src/utils/urlMatchers.ts
Original file line number Diff line number Diff line change
@@ -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;
};
3 changes: 3 additions & 0 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
Expand Down

0 comments on commit 0c8f222

Please sign in to comment.