Repository: open-sauced/insights (ts)

Issue:
feat: use opengraph images for highlights
Today the highlights card is presented as a custom UI element. We should be fetching directly from the OpenGraph url. This enables us to display images for more than just GitHub. But for now, this should displayed as an image and not customer elements. 

```
img src="https://opengraph.githubassets.com/1/open-sauced/hot/pull/448"
```

Not that any github image url will render an OG image using the prefix `https://opengraph.githubassets.com/1/` and adding the url minus the `https://github.com/`

Pull request description:
refactor: replace `PRSocialCard` with githubs Opengraph image
<!--
  For Work In Progress Pull Requests, please use the Draft PR feature,
  see https://github.blog/2019-02-14-introducing-draft-pull-requests/ for further details.
  
  For a timely review/response, please avoid force-pushing additional
  commits if your PR already received reviews or comments.
  
  Before submitting a Pull Request, please ensure you've done the following:
  - 📖 Read the Open Sauced Contributing Guide: https://github.com/open-sauced/open-sauced/blob/HEAD/CONTRIBUTING.md#create-a-pull-request.
  - 📖 Read the Open Sauced Code of Conduct: https://github.com/open-sauced/open-sauced/blob/HEAD/CODE_OF_CONDUCT.md.
  - 👷‍♀️ Create small PRs. In most cases, this will be possible.
  - ✅ Provide tests for your changes.
  - 📝 Use descriptive commit messages.
  - 📗 Update any related documentation and include any relevant screenshots.
-->

## What type of PR is this? (check all applicable)

- [ ] 🍕 Feature
- [ ] 🐛 Bug Fix
- [ ] 📝 Documentation Update
- [ ] 🎨 Style
- [x] 🧑‍💻 Code Refactor
- [ ] 🔥 Performance Improvements
- [ ] ✅ Test
- [ ] 🤖 Build
- [ ] 🔁 CI
- [ ] 📦 Chore (Release)
- [ ] ⏩ Revert

## Description
This PR:

- refactors the highlight social image to leverage GitHub [open graph image](https://github.blog/2021-06-22-framework-building-open-graph-images/)

<!-- 
Please do not leave this blank 
This PR [adds/removes/fixes/replaces] the [feature/bug/etc]. 
-->

## Related Tickets & Documents
Fixes #883 
<!-- 
Please use this format link issue numbers: Fixes #123
https://docs.github.com/en/free-pro-team@latest/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword 
-->

## Mobile & Desktop Screenshots/Recordings
![image](https://user-images.githubusercontent.com/62995161/220189222-91d19496-a588-41f9-8997-733e1c10348f.png)

<!-- Visual changes require screenshots -->


## Added tests?

- [x] 👍 yes
- [ ] 🙅 no, because they aren't needed
- [ ] 🙋 no, because I need help

## Added to documentation?

- [ ] 📜 README.md
- [ ] 📓 docs.opensauced.pizza
- [ ] 🍕 dev.to/opensauced
- [ ] 📕 storybook
- [x] 🙅 no documentation needed

## [optional] Are there any post-deployment tasks we need to perform?



## [optional] What gif best describes this PR or how it makes you feel?



<!-- note: PRs with deleted sections will be marked invalid -->



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b71ca3aa..d54b8609 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,27 @@
 
 > All notable changes to this project will be documented in this file
 
+## [1.30.0-beta.7](https://github.com/open-sauced/insights/compare/v1.30.0-beta.6...v1.30.0-beta.7) (2023-02-21)
+
+
+### 🐛 Bug Fixes
+
+* hide non-functional elements in highlights card ([#881](https://github.com/open-sauced/insights/issues/881)) ([ba1bd5e](https://github.com/open-sauced/insights/commit/ba1bd5e32c8645e3b99f019afd00c20ea9c0e15d))
+
+## [1.30.0-beta.6](https://github.com/open-sauced/insights/compare/v1.30.0-beta.5...v1.30.0-beta.6) (2023-02-21)
+
+
+### 🐛 Bug Fixes
+
+* disable create page button only if insight page name is empty ([#893](https://github.com/open-sauced/insights/issues/893)) ([ed52399](https://github.com/open-sauced/insights/commit/ed52399acc13f5cb05abb62beabaac016a951729)), closes [#892](https://github.com/open-sauced/insights/issues/892)
+
+## [1.30.0-beta.5](https://github.com/open-sauced/insights/compare/v1.30.0-beta.4...v1.30.0-beta.5) (2023-02-20)
+
+
+### 🐛 Bug Fixes
+
+* user profile interest pill UI fix ([#890](https://github.com/open-sauced/insights/issues/890)) ([4cbff36](https://github.com/open-sauced/insights/commit/4cbff36fc104be46143c31b5ccd3b732179206d7))
+
 ## [1.30.0-beta.4](https://github.com/open-sauced/insights/compare/v1.30.0-beta.3...v1.30.0-beta.4) (2023-02-17)
 
 
diff --git a/components/molecules/ContributorHighlight/contributor-highlight-card.tsx b/components/molecules/ContributorHighlight/contributor-highlight-card.tsx
index 4c9d5e00..94f71ed2 100644
--- a/components/molecules/ContributorHighlight/contributor-highlight-card.tsx
+++ b/components/molecules/ContributorHighlight/contributor-highlight-card.tsx
@@ -1,7 +1,6 @@
 import Title from "components/atoms/Typography/title";
 import { generateApiPrUrl } from "lib/utils/github";
 import React, { useEffect, useState } from "react";
-import PullRequestHighlightCard from "../PullRequestHighlightCard/pull-request-highlight-card";
 import { fetchGithubPRInfo } from "lib/hooks/fetchGithubPRInfo";
 import {
   DropdownMenu,
@@ -17,6 +16,7 @@ import { FaUserPlus } from "react-icons/fa";
 import { GrFlag } from "react-icons/gr";
 
 import { ToastTrigger } from "lib/utils/toast-trigger";
+import GhOpenGraphImg from "../GhOpenGraphImg/gh-open-graph-img";
 
 interface ContributorHighlightCardProps {
   title?: string;
@@ -47,7 +47,7 @@ const ContributorHighlightCard = ({ title, desc, prLink, user }: ContributorHigh
         )}
         <div className="flex ml-auto lg:gap-3 gap-3 items-center">
           <DropdownMenu>
-            <DropdownMenuTrigger className="py-2 px-2 rounded-full data-[state=open]:bg-light-slate-7">
+            <DropdownMenuTrigger className="py-2 px-2 hidden rounded-full data-[state=open]:bg-light-slate-7">
               <HiOutlineEmojiHappy size={20} />
             </DropdownMenuTrigger>
             <DropdownMenuContent className="flex flex-row gap-2 rounded-3xl" side="left">
@@ -96,13 +96,13 @@ const ContributorHighlightCard = ({ title, desc, prLink, user }: ContributorHigh
                   <span>Copy link</span>
                 </div>
               </DropdownMenuItem>
-              <DropdownMenuItem className="rounded-md">
+              <DropdownMenuItem className="rounded-md hidden">
                 <div className="flex gap-2.5 py-1  items-center pl-3 pr-7">
                   <FaUserPlus size={22} />
                   <span>Follow user</span>
                 </div>
               </DropdownMenuItem>
-              <DropdownMenuItem className="rounded-md">
+              <DropdownMenuItem className="rounded-md hidden">
                 <div className="flex gap-2.5 py-1  items-center pl-3 pr-7">
                   <GrFlag size={22} />
                   <span>Report content</span>
@@ -126,7 +126,7 @@ const ContributorHighlightCard = ({ title, desc, prLink, user }: ContributorHigh
       </div>
 
       {/* Generated OG card section */}
-      <PullRequestHighlightCard prLink={prLink} />
+      <GhOpenGraphImg githubLink={prLink} />
     </article>
   );
 };
diff --git a/components/molecules/ContributorProfileInfo/contributor-profile-info.tsx b/components/molecules/ContributorProfileInfo/contributor-profile-info.tsx
index a939d526..de29a3af 100644
--- a/components/molecules/ContributorProfileInfo/contributor-profile-info.tsx
+++ b/components/molecules/ContributorProfileInfo/contributor-profile-info.tsx
@@ -8,7 +8,7 @@ import { HiOutlineMail } from "react-icons/hi";
 import LanguagePill from "components/atoms/LanguagePill/LanguagePill";
 import Title from "components/atoms/Typography/title";
 import Badge from "components/atoms/Badge/badge";
-import { getTimezone } from "lib/utils/timezones";
+import { getTimeByTimezone, getTimezone } from "lib/utils/timezones";
 
 interface ContributorProfileInfoProps {
   githubName: string;
@@ -69,16 +69,26 @@ const ContributorProfileInfo = ({
                 "I am an open source developer with a passion for music and video games. I strive to improve the open source community and am always looking for new ways to contribute."}
             </p>
             <div className="flex flex-col text-sm mt-2 text-light-slate-9 gap-2">
-              {displayLocalTime && <span className="flex gap-2 items-center">
-                <FiClock className="text-light-slate-9" /> Local time: 12:32 PM
-              </span>}
+              {displayLocalTime && (
+                <span className="flex gap-2 items-center">
+                  <FiClock className="text-light-slate-9" /> Local time:
+                  <span> {getTimeByTimezone(timezone ? getTimezone(timezone) : 1)}</span>
+                </span>
+              )}
 
-              {twitterUsername && <span className="flex gap-2 items-center">
-                <FiTwitter className="text-light-slate-9" />
-                <Link href={`https://twitter.com/${twitterUsername}`} target="_blank" rel="noreferrer" className="w-max hover:text-orange-500 ">
-                  {twitterUsername}
-                </Link>
-              </span>}
+              {twitterUsername && (
+                <span className="flex gap-2 items-center">
+                  <FiTwitter className="text-light-slate-9" />
+                  <Link
+                    href={`https://twitter.com/${twitterUsername}`}
+                    target="_blank"
+                    rel="noreferrer"
+                    className="w-max hover:text-orange-500 "
+                  >
+                    {twitterUsername}
+                  </Link>
+                </span>
+              )}
 
               {/* <span className="flex gap-2 items-center">
                 <HiOutlineMail className="text-light-slate-9" />
@@ -93,7 +103,7 @@ const ContributorProfileInfo = ({
               <Title className="!text-base !text-light-slate-12" level={5}>
                 Current Interests
               </Title>
-              <div className="flex gap-2">
+              <div className="flex gap-1.5 flex-wrap">
                 {interestArray.map((interest, index) => (
                   <LanguagePill key={index} topic={interest} />
                 ))}
diff --git a/components/molecules/GhOpenGraphImg/gh-open-graph-img.tsx b/components/molecules/GhOpenGraphImg/gh-open-graph-img.tsx
new file mode 100644
index 00000000..0dad6226
--- /dev/null
+++ b/components/molecules/GhOpenGraphImg/gh-open-graph-img.tsx
@@ -0,0 +1,24 @@
+import { generateGhOgImage } from "lib/utils/github";
+import Image from "next/image";
+import InvalidImage from "img/404 Image.svg";
+import { useEffect, useState } from "react";
+
+interface GhOpenGraphImgProps {
+  githubLink: string;
+}
+
+const GhOpenGraphImg = ({ githubLink }: GhOpenGraphImgProps): JSX.Element => {
+  const { isValid, url } = generateGhOgImage(githubLink);
+
+  return (
+    <>
+      {url && (
+        <picture>
+          <img src={isValid ? url : InvalidImage} alt={isValid ? "github og image" : "invalid url image"} />
+        </picture>
+      )}
+    </>
+  );
+};
+
+export default GhOpenGraphImg;
diff --git a/components/molecules/HighlightInput/highlight-input-form.tsx b/components/molecules/HighlightInput/highlight-input-form.tsx
index 90d88aa8..3a4468c4 100644
--- a/components/molecules/HighlightInput/highlight-input-form.tsx
+++ b/components/molecules/HighlightInput/highlight-input-form.tsx
@@ -4,9 +4,9 @@ import { createHighlights } from "lib/hooks/createHighlights";
 
 import { ToastTrigger } from "lib/utils/toast-trigger";
 import { ChangeEvent, useEffect, useRef, useState } from "react";
-import PullRequestHighlightCard from "../PullRequestHighlightCard/pull-request-highlight-card";
 import { useSWRConfig } from "swr";
 import useSupabaseAuth from "lib/hooks/useSupabaseAuth";
+import GhOpenGraphImg from "../GhOpenGraphImg/gh-open-graph-img";
 
 const HighlightInputForm = (): JSX.Element => {
   const { user } = useSupabaseAuth();
@@ -116,7 +116,7 @@ const HighlightInputForm = (): JSX.Element => {
         />
       </div>
 
-      {pullrequestLink && isDivFocused && <PullRequestHighlightCard prLink={pullrequestLink} />}
+      {pullrequestLink && isDivFocused && <GhOpenGraphImg githubLink={pullrequestLink} />}
 
       {isDivFocused && (
         <Button disabled={!bodyText} className="ml-auto" type="primary">
diff --git a/components/molecules/PullRequestHighlightCard/pull-request-highlight-card.tsx b/components/molecules/PullRequestHighlightCard/pull-request-highlight-card.tsx
deleted file mode 100644
index d4fc3f3b..00000000
--- a/components/molecules/PullRequestHighlightCard/pull-request-highlight-card.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-import Image from "next/image";
-
-import Avatar from "components/atoms/Avatar/avatar";
-import React, { useEffect, useState } from "react";
-import { AiOutlineGithub } from "react-icons/ai";
-import { TbMessages } from "react-icons/tb";
-import CardHorizontalBarChart from "../CardHorizontalBarChart/card-horizontal-bar-chart";
-import { getFormattedDate } from "lib/utils/date-utils";
-import { fetchGithubPRInfo } from "lib/hooks/fetchGithubPRInfo";
-import { generateApiPrUrl } from "lib/utils/github";
-import SkeletonWrapper from "components/atoms/SkeletonLoader/skeleton-wrapper";
-
-import InvalidImage from "img/404 Image.svg";
-
-interface PullRequestHighlightCardProps {
-  prLink: string;
-}
-const PullRequestHighlightCard = ({ prLink }: PullRequestHighlightCardProps) => {
-  const { isValidUrl, apiPaths } = generateApiPrUrl(prLink);
-  const [PRInfo, setPRInfo] = useState<GhPRInfoResponse>();
-  const [isLoading, setLoading] = useState(false);
-  const [isError, setError] = useState(false);
-
-  const fetchGhPullRequest = async (orgName: string | null, repoName: string | null, issueId: string | null) => {
-    setLoading(true);
-    const { data, isError } = await fetchGithubPRInfo(orgName, repoName, issueId);
-    setLoading(false);
-    if (data) {
-      setPRInfo(data);
-    }
-    if (isError) {
-      setError(true);
-    }
-  };
-
-  useEffect(() => {
-    const { orgName, repoName, issueId } = apiPaths;
-    fetchGhPullRequest(orgName, repoName, issueId);
-  }, []);
-  return (
-    <>
-      {isLoading && <SkeletonWrapper height={250} />}
-      {isValidUrl && isError && (
-        <div className="relative w-full h-[390px]">
-          <Image src={InvalidImage} fill alt="invalid url image" />
-        </div>
-      )}
-      {PRInfo && (
-        <div className="flex max-w-[45rem] flex-1 flex-col gap-8 lg:gap-16 bg-white pt-8 pb-4">
-          <div className="lg:px-8 px-3 flex justify-between">
-            <div className="flex flex-col gap-2">
-              <span className="text-sm font-normal">{`${PRInfo.head.user.login}/${PRInfo.head.repo.name}`}</span>
-              <h3 className="text-base lg:text-3xl font-semibold flex items-center gap-1">
-                <span className="text-light-slate-9 font-normal">{`#${PRInfo.number}`}</span> <p>{PRInfo.title}</p>
-              </h3>
-              <div className="flex gap-2 text-light-slate-9 text-sm">
-                <TbMessages className="text-lg" />
-                {PRInfo.comments} comment
-              </div>
-            </div>
-            <div className="md:hidden">
-              <Avatar isCircle size="base" avatarURL={PRInfo.user.avatar_url} />
-            </div>
-            <div className="hidden md:inline-flex">
-              <Avatar isCircle size="lg" avatarURL={PRInfo.user.avatar_url} />
-            </div>
-          </div>
-
-          <div>
-            <div className="lg:px-8 px-3 flex justify-between items-center">
-              <div className="flex text-sm items-center gap-2">
-                <Avatar isCircle size="sm" avatarURL={PRInfo?.user.avatar_url} />
-                <p className="text-light-slate-9 font-normal">
-                  <span className="text-light-slate-12 font-semibold">{PRInfo.user.login}</span> opened on{" "}
-                  {getFormattedDate(PRInfo.created_at)}
-                </p>
-              </div>
-              <AiOutlineGithub />
-            </div>
-            <div className="flex flex-col mt-3 gap-4">
-              <div>
-                <CardHorizontalBarChart
-                  languageList={[{ languageName: "JavaScript", percentageUsed: 100 }]}
-                  withDescription={false}
-                />
-              </div>
-              <div className="lg:px-8 px-3">
-                <p className="font-semibold">
-                  {PRInfo?.title}· Issue {`#${PRInfo.number}`} · {`${PRInfo.head.user.login}/${PRInfo.head.repo.name}`}
-                </p>
-                <span>GITHUB.COM</span>
-              </div>
-            </div>
-          </div>
-        </div>
-      )}
-    </>
-  );
-};
-
-export default React.memo(PullRequestHighlightCard);
diff --git a/components/organisms/InsightPage/InsightPage.tsx b/components/organisms/InsightPage/InsightPage.tsx
index d291a90b..14cb0cbc 100644
--- a/components/organisms/InsightPage/InsightPage.tsx
+++ b/components/organisms/InsightPage/InsightPage.tsx
@@ -69,15 +69,23 @@ const InsightPage = ({ edit, insight, pageRepos }: InsightPageProps) => {
     };
   });
 
-  const handleOnNameChange = (value: string) => {
+  const validateName = (name: string) => {
+    if (!name || name.trim().length <= 3) return false;
+
+    return true;
+  };
 
+  const handleOnNameChange = (value: string) => {
     setName(value);
+    setIsNameValid(validateName(value));
+  };
 
-    if (!value || value.trim().length <= 3) setIsNameValid(false);
+  const disableCreateButton = () => {
+    if (insight?.name && validateName(name)) return false;
+    if (submitted) return true;
+    if (!isNameValid) return true;
 
-    if (value.trim().length > 3) {
-      setIsNameValid(true);
-    }
+    return false;
   };
 
   const handleCreateInsightPage = async () => {
@@ -279,7 +287,7 @@ const InsightPage = ({ edit, insight, pageRepos }: InsightPageProps) => {
           handleUpdatePage={handleUpdateInsightPage}
           handleAddToCart={handleReAddRepository}
           history={reposRemoved}
-          createPageButtonDisabled={submitted || !isNameValid}
+          createPageButtonDisabled={disableCreateButton()}
         >
           {repos.map((repo) => {
             const totalPrs =
diff --git a/lib/utils/github.ts b/lib/utils/github.ts
index d0a7d6e9..62cd7c6c 100644
--- a/lib/utils/github.ts
+++ b/lib/utils/github.ts
@@ -39,4 +39,20 @@ const generateApiPrUrl = (
   }
 };
 
-export { getAvatarById, getAvatarByUsername, getProfileLink, getRepoIssuesLink, generateApiPrUrl };
+const generateGhOgImage = (githubUrl: string): { isValid: boolean; url: string } => {
+  try {
+    const trimmedUrl = githubUrl.trim();
+    const url = new URL(trimmedUrl.includes("https://") ? trimmedUrl : `https://${trimmedUrl}`);
+    const { pathname } = url;
+
+    if (url.hostname !== "github.com") {
+      return { isValid: false, url: "" };
+    }
+
+    return { isValid: true, url: `https://opengraph.githubassets.com/1${pathname}` };
+  } catch (err) {
+    return { isValid: false, url: "" };
+  }
+};
+
+export { getAvatarById, getAvatarByUsername, getProfileLink, getRepoIssuesLink, generateApiPrUrl, generateGhOgImage };
diff --git a/lib/utils/timezones.ts b/lib/utils/timezones.ts
index 8fdc9b66..0d5e3f00 100644
--- a/lib/utils/timezones.ts
+++ b/lib/utils/timezones.ts
@@ -1179,5 +1179,20 @@ export const timezones = [
 ];
 
 export const getTimezone = (timezone: string): number => {
-  return timezones.find(tz => tz.value === timezone)?.offset || +1;
-};
\ No newline at end of file
+  return timezones.find((tz) => tz.value === timezone)?.offset || +1;
+};
+
+export const getTimeByTimezone = (offset: number) => {
+  var d = new Date();
+
+  // convert to msec
+  // subtract local time zone offset
+  // get UTC time in msec
+  const utc = d.getTime() + d.getTimezoneOffset() * 60000;
+
+  // create new Date object for different city
+  // using supplied offset
+  const time = new Date(utc + 3600000 * offset);
+
+  return `${time.getHours()}:${time.getMinutes()}${time.getHours() > 11 ? "pm" : "am"}`;
+};
diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json
index 61a3c3f5..62145831 100644
--- a/npm-shrinkwrap.json
+++ b/npm-shrinkwrap.json
@@ -1,12 +1,12 @@
 {
   "name": "@open-sauced/insights",
-  "version": "1.30.0-beta.4",
+  "version": "1.30.0-beta.7",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "@open-sauced/insights",
-      "version": "1.30.0-beta.4",
+      "version": "1.30.0-beta.7",
       "hasInstallScript": true,
       "license": "MIT",
       "dependencies": {
diff --git a/package.json b/package.json
index 552f3976..279991d8 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
   "name": "@open-sauced/insights",
   "description": "🍕The dashboard for open source discovery.",
   "keywords": [],
-  "version": "1.30.0-beta.4",
+  "version": "1.30.0-beta.7",
   "author": "TED Vortex <ted.vortex@gmail.com>",
   "private": true,
   "license": "MIT",


Test patch:
diff --git a/tests/lib/utils/github.test.ts b/tests/lib/utils/github.test.ts
index 88e35677..13f5b4f1 100644
--- a/tests/lib/utils/github.test.ts
+++ b/tests/lib/utils/github.test.ts
@@ -1,7 +1,12 @@
-import { getAvatarById, getAvatarByUsername, getProfileLink, getRepoIssuesLink } from "lib/utils/github";
+import {
+  getAvatarById,
+  getAvatarByUsername,
+  getProfileLink,
+  getRepoIssuesLink,
+  generateGhOgImage
+} from "lib/utils/github";
 
 describe("[lib] github methods", () => {
-
   it("Should return a valid avatar link", () => {
     const result = getAvatarById("Deadreyo", 460);
     expect(result).toEqual("https://avatars.githubusercontent.com/u/Deadreyo?size=460&v=4");
@@ -21,5 +26,12 @@ describe("[lib] github methods", () => {
     const result = getRepoIssuesLink("Deadreyo/insights");
     expect(result).toEqual("https://github.com/Deadreyo/insights/issues");
   });
-
+  it("Should return a valid image src link", () => {
+    const result = generateGhOgImage("https://github.com/open-sauced/hot/pull/448");
+    expect(result).toEqual({ isValid: true, url: "https://opengraph.githubassets.com/1/open-sauced/hot/pull/448" });
+  });
+  it("Should return an object with isValid set to false", () => {
+    const result = generateGhOgImage("https://gitub.com/open-sauced/hot/pull/448");
+    expect(result).toEqual({ isValid: false, url: "" });
+  });
 });