Repository: open-sauced/insights
Pull request #875 (2023-02-14 18:42:05)

v1.29.0-beta.7 -> production


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d2808c31..54c39fb4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,61 @@
 
 > All notable changes to this project will be documented in this file
 
+## [1.29.0-beta.7](https://github.com/open-sauced/insights/compare/v1.29.0-beta.6...v1.29.0-beta.7) (2023-02-14)
+
+
+### 🐛 Bug Fixes
+
+* move SEOobject to next types ([4500b60](https://github.com/open-sauced/insights/commit/4500b6078f4aecfadc29a2ff0f096c265f1c9060))
+
+## [1.29.0-beta.6](https://github.com/open-sauced/insights/compare/v1.29.0-beta.5...v1.29.0-beta.6) (2023-02-13)
+
+
+### 🍕 Features
+
+* add unit tests for lib files ([#862](https://github.com/open-sauced/insights/issues/862)) ([466c70f](https://github.com/open-sauced/insights/commit/466c70f67be96008a593e91d6b74810fbc5e56a3)), closes [#546](https://github.com/open-sauced/insights/issues/546)
+
+
+### 🐛 Bug Fixes
+
+* enable update access for all user profile settings ([#838](https://github.com/open-sauced/insights/issues/838)) ([0ff49d0](https://github.com/open-sauced/insights/commit/0ff49d0bf2cff4d38a6ddb5c5b97a0500f6743dc)), closes [#837](https://github.com/open-sauced/insights/issues/837)
+* truncate text on mobile and move dropdown for repositories ([#863](https://github.com/open-sauced/insights/issues/863)) ([9c7a6f5](https://github.com/open-sauced/insights/commit/9c7a6f5bc167f55b102decb6faeceed1312a2fc7)), closes [#805](https://github.com/open-sauced/insights/issues/805) [#806](https://github.com/open-sauced/insights/issues/806)
+
+## [1.29.0-beta.5](https://github.com/open-sauced/insights/compare/v1.29.0-beta.4...v1.29.0-beta.5) (2023-02-13)
+
+
+### 🍕 Features
+
+* add support for custom SEO per page ([#835](https://github.com/open-sauced/insights/issues/835)) ([03e760e](https://github.com/open-sauced/insights/commit/03e760e7df37c918a9de41a1be0bf652b04a4606)), closes [#814](https://github.com/open-sauced/insights/issues/814)
+
+## [1.29.0-beta.4](https://github.com/open-sauced/insights/compare/v1.29.0-beta.3...v1.29.0-beta.4) (2023-02-10)
+
+
+### 🍕 Features
+
+* add utility function to camelcase topic value ([#851](https://github.com/open-sauced/insights/issues/851)) ([b805659](https://github.com/open-sauced/insights/commit/b805659e3d5c42ef6bce3fa4f1eaf33f13e21617)), closes [#822](https://github.com/open-sauced/insights/issues/822)
+
+## [1.29.0-beta.3](https://github.com/open-sauced/insights/compare/v1.29.0-beta.2...v1.29.0-beta.3) (2023-02-09)
+
+
+### 🍕 Features
+
+* add highlights feed filter card to design system ([#843](https://github.com/open-sauced/insights/issues/843)) ([b4d0eba](https://github.com/open-sauced/insights/commit/b4d0eba0ca068b03a78290d920496c8737f6813d)), closes [#827](https://github.com/open-sauced/insights/issues/827)
+
+## [1.29.0-beta.2](https://github.com/open-sauced/insights/compare/v1.29.0-beta.1...v1.29.0-beta.2) (2023-02-09)
+
+
+### 🧑‍💻 Code Refactoring
+
+* use github avatar link instead of cached one ([#848](https://github.com/open-sauced/insights/issues/848)) ([a7678c3](https://github.com/open-sauced/insights/commit/a7678c32da95adf60dce1b129aa43e165620aff3)), closes [#832](https://github.com/open-sauced/insights/issues/832)
+
+## [1.29.0-beta.1](https://github.com/open-sauced/insights/compare/v1.28.0...v1.29.0-beta.1) (2023-02-08)
+
+
+### 🍕 Features
+
+* make pr list item clickable ([#807](https://github.com/open-sauced/insights/issues/807)) ([5398ad8](https://github.com/open-sauced/insights/commit/5398ad84cd310a04ea5f7bd96feb83dedc5689c5)), closes [#799](https://github.com/open-sauced/insights/issues/799)
+
 ## [1.28.0](https://github.com/open-sauced/insights/compare/v1.27.0...v1.28.0) (2023-02-08)
 
 
diff --git a/components/atoms/ContextFilterButton/context-filter-button.tsx b/components/atoms/ContextFilterButton/context-filter-button.tsx
index 09c5bcf7..1e0d3a9c 100644
--- a/components/atoms/ContextFilterButton/context-filter-button.tsx
+++ b/components/atoms/ContextFilterButton/context-filter-button.tsx
@@ -12,9 +12,9 @@ const ContextFilterButton: React.FC<ContextFilterButtonProps> = ({ className, ch
   return (
     <button
       onClick={onClick}
-      className="flex items-center gap-1 py-1 px-2 bg-slate-100 border border-slate-300 text-sm text-slate-800   whitespace-nowrap rounded-md drop-shadow-sm hover:bg-slate-50 focus:outline-none focus:border-orange-500 focus:ring-orange-100 focus-visible:border-orange-500 focus:ring focus-visible:ring focus-visible:ring-orange-100 transition"
+      className="flex w-full items-center gap-1 py-1 px-2 bg-slate-100 border border-slate-300 text-sm text-slate-800   whitespace-nowrap rounded-md drop-shadow-sm hover:bg-slate-50 focus:outline-none focus:border-orange-500 focus:ring-orange-100 focus-visible:border-orange-500 focus:ring focus-visible:ring focus-visible:ring-orange-100 transition"
     >
-      <Text className="!text-sm font-semibold tracking-tight !text-slate-900">{children}</Text>
+      <Text className="!text-sm font-semibold truncate tracking-tight !text-slate-900">{children}</Text>
       {!isSelected && <FiPlus onClick={onClick} />}
     </button>
   );
diff --git a/components/atoms/EChartWrapper/echart-wrapper.tsx b/components/atoms/EChartWrapper/echart-wrapper.tsx
index da1b96c0..a1069771 100644
--- a/components/atoms/EChartWrapper/echart-wrapper.tsx
+++ b/components/atoms/EChartWrapper/echart-wrapper.tsx
@@ -1,7 +1,7 @@
 import React from "react";
 import ReactECharts from "echarts-for-react";
 
-/* 
+/*
 Docs for eChart: https://echarts.apache.org/en/feature.html
 eChart Examples:  https://echarts.apache.org/examples/en/editor.html?c=scatter-simple&lang=ts
 Docs for ReactECharts: https://github.com/hustcc/echarts-for-react
@@ -9,12 +9,14 @@ Docs for ReactECharts: https://github.com/hustcc/echarts-for-react
 
 interface EChartWrapperProps {
     option: Object;
+    className?: string;
 }
 
-const EChartWrapper: React.FC<EChartWrapperProps> = ({ option }) => {
+const EChartWrapper: React.FC<EChartWrapperProps> = ({ option, className }) => {
   return (
     <ReactECharts
       option={option}
+      className={className || ""}
       notMerge={true}
       lazyUpdate={true}
       theme={"theme_name"}
@@ -22,4 +24,4 @@ const EChartWrapper: React.FC<EChartWrapperProps> = ({ option }) => {
   );
 };
 
-export default EChartWrapper;
\ No newline at end of file
+export default EChartWrapper;
diff --git a/components/atoms/LanguagePill/LanguagePill.tsx b/components/atoms/LanguagePill/LanguagePill.tsx
index 33e2c90b..f28e90ed 100644
--- a/components/atoms/LanguagePill/LanguagePill.tsx
+++ b/components/atoms/LanguagePill/LanguagePill.tsx
@@ -7,6 +7,7 @@ import PythonIcon from "/img/icons/🐍.svg";
 import AIIcon from "/img/icons/🤖.svg";
 import MLIcon from "/img/icons/🧠.svg";
 import RustIcon from "/img/icons/🦀.svg";
+import checkCamelCaseNaming from "lib/utils/check-camelcase-naming";
 
 interface LanguagePillProps {
   topic: "react" | "javascript" | "python" | "ML" | "AI" | "rust" | string;
@@ -26,6 +27,7 @@ const LanguagePill = ({ topic, classNames, onClick }: LanguagePillProps) => {
 
     return iconMap[name] || "";
   };
+
   return (
     <div
       onClick={onClick}
@@ -34,7 +36,7 @@ const LanguagePill = ({ topic, classNames, onClick }: LanguagePillProps) => {
       }`}
     >
       <Image src={renderTopicIcon(topic)} alt={topic} />
-      <span className="font-normal capitalize">{topic}</span>
+      <span className="font-normal capitalize">{checkCamelCaseNaming(topic)}</span>
     </div>
   );
 };
diff --git a/components/atoms/TextInput/text-input.tsx b/components/atoms/TextInput/text-input.tsx
index 9dd695da..672b1d4f 100644
--- a/components/atoms/TextInput/text-input.tsx
+++ b/components/atoms/TextInput/text-input.tsx
@@ -14,6 +14,10 @@ interface TextInputProps {
   classNames?: string;
   errorMsg?: string;
   value?: string;
+  defaultValue?: string;
+  required?: boolean;
+  fieldRef?: React.RefObject<HTMLInputElement>;
+  pattern?: string;
   onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
 }
 
@@ -29,12 +33,18 @@ const TextInput = ({
   autoFocus,
   borderless = false,
   value,
+  defaultValue,
   onChange,
+  required,
+  fieldRef,
+  pattern,
   errorMsg = ""
 }: TextInputProps) => {
-  const inputRef = useRef<any>();
+  const inputRef = useRef<HTMLInputElement>(null);
   const handleResetInput = () => {
-    inputRef.current.value = "";
+    fieldRef ? fieldRef.current!.value = ""
+      :
+      inputRef.current!.value = "";
   };
   return (
     <>
@@ -50,18 +60,21 @@ const TextInput = ({
           )}
         >
           <input
-            ref={inputRef}
+            ref={fieldRef || inputRef}
             type="text"
             name={name}
             id={id || name || ""}
             placeholder={placeholder || ""}
             onChange={onChange}
             value={value}
+            defaultValue={defaultValue}
             className={`flex-1 focus:outline-none  ${classNames} ${
               disabled && "bg-light-slate-3 cursor-not-allowed  text-light-slate-9"
             }`}
             autoFocus={autoFocus}
             disabled={disabled}
+            required={required}
+            pattern={pattern}
           />
           {!disabled && (
             <>
diff --git a/components/molecules/CardLineChart/card-line-chart.tsx b/components/molecules/CardLineChart/card-line-chart.tsx
index 8943d4de..83275fb1 100644
--- a/components/molecules/CardLineChart/card-line-chart.tsx
+++ b/components/molecules/CardLineChart/card-line-chart.tsx
@@ -3,14 +3,15 @@ import EChartWrapper from "components/atoms/EChartWrapper/echart-wrapper";
 
 interface CardLineChartProps {
   lineChartOption: Object;
+  className?: string;
 }
 
-const CardLineChart: React.FC<CardLineChartProps> = ({ lineChartOption }) => {
+const CardLineChart: React.FC<CardLineChartProps> = ({ lineChartOption, className }) => {
   return (
     <>
-      <EChartWrapper option={lineChartOption} />
+      <EChartWrapper option={lineChartOption} className={className} />
     </>
   );
 };
 
-export default CardLineChart;
\ No newline at end of file
+export default CardLineChart;
diff --git a/components/molecules/FilterHeader/filter-header.tsx b/components/molecules/FilterHeader/filter-header.tsx
index b96e467f..55a658be 100644
--- a/components/molecules/FilterHeader/filter-header.tsx
+++ b/components/molecules/FilterHeader/filter-header.tsx
@@ -12,6 +12,7 @@ import useFilterOptions from "lib/hooks/useFilterOptions";
 import { captureAnayltics } from "lib/utils/analytics";
 import useFilterPrefetch from "lib/hooks/useFilterPrefetch";
 import uppercaseFirst from "lib/utils/uppercase-first";
+import checkCamelCaseNaming from "lib/utils/check-camelcase-naming";
 
 const HeaderFilter = () => {
   const router = useRouter();
@@ -33,15 +34,15 @@ const HeaderFilter = () => {
       <div className="header-image mr-2 p-2 min-w-[130px]">
         <ContextThumbnail size={120} ContextThumbnailURL={isHacktoberfest ? Thumbnail.src : ""}></ContextThumbnail>
       </div>
-      <div className="header-info flex flex-col grow justify-center p-2">
+      <div className="header-info md:truncate flex flex-col grow justify-center p-2">
         <Title level={1} className="!text-3xl font-semibold tracking-tight text-slate-900">
-          {isHacktoberfest ? "Hacktoberfest 2022" : uppercaseFirst(filterName as string)}
+          {isHacktoberfest ? "Hacktoberfest 2022" : checkCamelCaseNaming(filterName as string)}
         </Title>
         <Text className="mt-1 !text-base   text-slate-500">
           Insights on GitHub repositories{" "}
           {isHacktoberfest ? "opted into the largest open source hackathon." : `using the ${filterName} topic.`}
         </Text>
-        <div className="flex mt-4 items-center relative  gap-2">
+        <div className="flex mt-4 items-center gap-2">
           <FilterCard filterName={filterName as string} isRemovable={false} icon="topic" />
           <SuperativeSelector
             filterOptions={filterOptions}
diff --git a/components/molecules/HighlightsFeedCard/highlights-filter-card.tsx b/components/molecules/HighlightsFeedCard/highlights-filter-card.tsx
new file mode 100644
index 00000000..a342cc1b
--- /dev/null
+++ b/components/molecules/HighlightsFeedCard/highlights-filter-card.tsx
@@ -0,0 +1,52 @@
+import Title from "components/atoms/Typography/title";
+import TestRepoAvatar from "img/icons/test-repo-avatar.svg";
+import Icon from "components/atoms/Icon/icon";
+
+const HighlightsFilterCard = (): JSX.Element => {
+  const testRepoList = [
+    {
+      repoName: "opensauced",
+      repoIcon: TestRepoAvatar
+    },
+    {
+      repoName: "Contentful",
+      repoIcon: TestRepoAvatar
+    },
+    {
+      repoName: "StrapiJs",
+      repoIcon: TestRepoAvatar
+    },
+    {
+      repoName: "javascript",
+      repoIcon: TestRepoAvatar
+    },
+    {
+      repoName: "reactjs",
+      repoIcon: TestRepoAvatar
+    },
+    {
+      repoName: "facebook",
+      repoIcon: TestRepoAvatar
+    }
+  ];
+  return (
+    <div className="py-4 px-6 rounded-lg  border w-[18.625rem]">
+      <Title className="!font-normal" level={3}>
+        Repositories
+      </Title>
+      <p className="text-sm text-light-slate-11 mb-2 font-normal">Click to filter the highlights</p>
+      <div className="flex flex-col border-t pt-4 gap-2">
+        {testRepoList.map(({ repoName, repoIcon }) => (
+          <div
+            key={""}
+            className="flex hover:border-orange-600 hover:bg-orange-200 cursor-pointer gap-1 w-max  p-1 pr-2 border-[1px] border-light-slate-6 rounded-lg text-light-slate-12"
+          >
+            <Icon IconImage={repoIcon} className="rounded-[4px] overflow-hidden" />
+            <span className="max-w-[45px] md:max-w-[100px] truncate text-xs ">{repoName}</span>
+          </div>
+        ))}
+      </div>
+    </div>
+  );
+};
+export default HighlightsFilterCard;
diff --git a/components/molecules/SuperlativeSelector/superlative-selector.tsx b/components/molecules/SuperlativeSelector/superlative-selector.tsx
index 2ebd5545..bc27ac0e 100644
--- a/components/molecules/SuperlativeSelector/superlative-selector.tsx
+++ b/components/molecules/SuperlativeSelector/superlative-selector.tsx
@@ -36,7 +36,6 @@ const SuperativeSelector: React.FC<SuperlativeSelectorProps> = ({
       if (isOpen && ref.current && !ref.current.contains(e.target)) {
         setIsOpen(false);
       }
-
     };
     document.addEventListener("mousedown", checkIfClickedOutside);
 
@@ -50,24 +49,22 @@ const SuperativeSelector: React.FC<SuperlativeSelectorProps> = ({
   const filterDescription = filterOption ? filterOption : selected;
 
   return (
-    <div className="max-w-max relative" ref={ref}>
+    <div className="truncate" ref={ref}>
       <ContextFilterButton onClick={toggleFilter} isSelected={!!selected}>
         {selected ? (
-          <div className="flex">
-            <div className="flex" onClick={toggleFilter}>
-              <span className="text-dark-slate-10">Filtered by:</span>
-              <div className="flex items-center ml-1 text-light-slate-12">
-                {filterDescription}
+          <div className="flex" onClick={toggleFilter}>
+            <span className="inline-block text-dark-slate-10">Filtered by:</span>
+            <div className="block truncate pr-4 ml-1 text-light-slate-12">
+              {filterDescription}
 
-                <Icon
-                  className="ml-2"
-                  IconImage={cancelIcon}
-                  onClick={() => {
-                    handleCancelClick();
-                    setIsOpen(false);
-                  }}
-                />
-              </div>
+              <Icon
+                className="absolute right-1 top-1/2 transform -translate-y-1/2"
+                IconImage={cancelIcon}
+                onClick={() => {
+                  handleCancelClick();
+                  setIsOpen(false);
+                }}
+              />
             </div>
           </div>
         ) : (
@@ -75,7 +72,7 @@ const SuperativeSelector: React.FC<SuperlativeSelectorProps> = ({
         )}
       </ContextFilterButton>
       {isOpen && (
-        <div className="absolute -left-full md:left-0 space-y-1 mt-1 shadow-superlative w-72 z-10 bg-white rounded-lg px-1.5 py-2">
+        <div className="absolute transform -translate-x-1/3 md:translate-x-0 space-y-1 mt-1 shadow-superlative z-10 w-72 bg-white rounded-lg px-1.5 py-2">
           {filterOptions.length > 0 &&
             filterOptions.map((option, index) => {
               const filterKey = getFilterKey(option);
@@ -94,8 +91,8 @@ const SuperativeSelector: React.FC<SuperlativeSelectorProps> = ({
                 >
                   {option}
                 </Radio>
-              );}
-            )}
+              );
+            })}
         </div>
       )}
     </div>
diff --git a/components/molecules/TableRepositoryName/table-repository-name.tsx b/components/molecules/TableRepositoryName/table-repository-name.tsx
index c790bad1..590028e0 100644
--- a/components/molecules/TableRepositoryName/table-repository-name.tsx
+++ b/components/molecules/TableRepositoryName/table-repository-name.tsx
@@ -28,7 +28,7 @@ const TableRepositoryName = ({ avatarURL, name, handle, topic, user }: TableRepo
       {/* Avatar */}
       <a href={`https://www.github.com/${handle}/${name}`} target="_blank" rel="noreferrer">
         <Avatar
-          isCached={true}
+          isCached={false}
           className="shrink-0 min-w-10 min-h-10"
           size={40}
           avatarURL={avatarURL}
diff --git a/components/organisms/ContributorProfilePage/contributor-profile-page.tsx b/components/organisms/ContributorProfilePage/contributor-profile-page.tsx
index d8e5cb0d..9188a6ff 100644
--- a/components/organisms/ContributorProfilePage/contributor-profile-page.tsx
+++ b/components/organisms/ContributorProfilePage/contributor-profile-page.tsx
@@ -82,9 +82,9 @@ const ContributorProfilePage = ({
     <div className=" w-full">
       {loading ? <SkeletonWrapper height={200} /> : <ContributorProfileHeader  isConnected={!!user} githubName={githubName}avatarUrl={githubAvatar} />}
       <div className="pt-24 px-4 md:px-10 lg:px-16 flex flex-col lg:flex-row lg:gap-40 w-full overflow-hidden justify-between">
-        <div className="flex flex-col min-w-[270px] gap-4 ">
+        <div className="flex flex-col w-80 gap-4 ">
           {
-            loading ? <SkeletonWrapper height={210} radius={12} classNames="pb-16 lg:pb-0" /> : <> 
+            loading ? <SkeletonWrapper height={210} radius={12} classNames="pb-16 lg:pb-0" /> : <>
               <ContributorProfileInfo
                 interests={interests}
                 // eslint-disable-next-line camelcase
@@ -151,7 +151,7 @@ const ContributorProfilePage = ({
                 </div>
               </div>
               <div className="mt-10 h-32">
-                <CardLineChart lineChartOption={chart} />
+                <CardLineChart lineChartOption={chart} className="!h-32" />
               </div>
               <div>
                 <CardRepoList limit={7} repoList={repoList} />
diff --git a/components/organisms/UserSettingsPage/user-settings-page.tsx b/components/organisms/UserSettingsPage/user-settings-page.tsx
index f5bb7087..4a904983 100644
--- a/components/organisms/UserSettingsPage/user-settings-page.tsx
+++ b/components/organisms/UserSettingsPage/user-settings-page.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from "react";
+import { useState, useEffect, useRef } from "react";
 
 import { User } from "@supabase/supabase-js";
 
@@ -10,7 +10,7 @@ import Select from "components/atoms/Select/select";
 import SelectOption from "components/atoms/Select/select-option";
 import LanguagePill from "components/atoms/LanguagePill/LanguagePill";
 
-import { updateUser } from "lib/hooks/update-user";
+import { updateUser, updateUserPayload } from "lib/hooks/update-user";
 import { ToastTrigger } from "lib/utils/toast-trigger";
 import { authSession } from "lib/hooks/authSession";
 import { validateEmail } from "lib/utils/validate-email";
@@ -33,7 +33,7 @@ const UserSettingsPage = ({ user }: userSettingsPageProps) => {
   const [displayLocalTime, setDisplayLocalTime] = useState(false);
   const [timezone, setTimezone] = useState("");
   const [userInfo, setUserInfo] = useState<DbUser>();
-  const [email, setEmail] = useState<string | undefined>(userInfo?.email || user?.email);
+  const [email, setEmail] = useState<string | undefined>("");
   const [emailPreference, setEmailPreference] = useState<EmailPreferenceType>({
     // eslint-disable-next-line camelcase
     display_email: false,
@@ -41,15 +41,27 @@ const UserSettingsPage = ({ user }: userSettingsPageProps) => {
     receive_collaboration: false
   });
   const [selectedInterest, setSelectedInterest] = useState<string[]>([]);
+  const formRef = useRef<HTMLFormElement>(null);
   const interestArray = ["javascript", "python", "rust", "ML", "AI", "react"];
 
   useEffect(() => {
     async function fetchAuthSession() {
       const response = await authSession();
-      if (response !== false) setUserInfo(response);
+      if (response !== false) {
+        setUserInfo(response);
+        formRef.current!.nameInput.value = response.name;
+        setEmail(response.email);
+        formRef.current!.bio.value = response.bio;
+        formRef.current!.url.value = response.url;
+        formRef.current!.twitter_username.value = response.twitter_username;
+        formRef.current!.company.value = response.company;
+        formRef.current!.location.value = response.location;
+      }
     }
+    fetchAuthSession();
+  }, [user]);
 
-    if (user) setEmail(user.email);
+  useEffect(() => {
     if (insightsUser) {
       setEmailPreference({
         // eslint-disable-next-line camelcase
@@ -59,10 +71,9 @@ const UserSettingsPage = ({ user }: userSettingsPageProps) => {
       });
       setSelectedInterest(insightsUser?.interests?.split(","));
       setDisplayLocalTime(insightsUser?.display_local_time);
-      setTimezone(insightsUser?.timezone);
+      setTimezone(insightsUser?.timezone || "");
     }
-    fetchAuthSession();
-  }, [user, insightsUser]);
+  }, [insightsUser]);
 
   const handleSelectInterest = (interest: string) => {
     if (selectedInterest.length > 0 && selectedInterest.includes(interest)) {
@@ -93,14 +104,26 @@ const UserSettingsPage = ({ user }: userSettingsPageProps) => {
     }
   };
 
-  const handleUpdateProfile = async () => {
+  const handleUpdateProfile = async (e: React.FormEvent<HTMLFormElement>) => {
+    e.preventDefault();
+    const payload: updateUserPayload = {
+      name: formRef.current!.nameInput.value,
+      email,
+      bio: formRef.current!.bio.value,
+      // eslint-disable-next-line camelcase
+      twitter_username: formRef.current!.twitter_username.value,
+      company: formRef.current!.company.value,
+      location: formRef.current!.location.value,
+      // eslint-disable-next-line camelcase
+      display_local_time: displayLocalTime,
+      timezone
+    };
+    if(formRef.current?.url.value) {
+      payload.url = formRef.current!.url.value;
+    }
+
     const data = await updateUser({
-      data: {
-        email,
-        // eslint-disable-next-line camelcase
-        display_local_time: displayLocalTime,
-        timezone
-      }
+      data: payload
     });
 
     if (data) {
@@ -117,13 +140,13 @@ const UserSettingsPage = ({ user }: userSettingsPageProps) => {
           <Title className="!text-2xl !text-light-slate-11" level={2}>
             Public profile
           </Title>
-          <form onSubmit={(e) => e.preventDefault()} className="flex flex-col gap-6 mt-6">
+          <form onSubmit={handleUpdateProfile} className="flex flex-col gap-6 mt-6" ref={formRef}>
             <TextInput
               classNames="bg-light-slate-4 text-light-slate-11 font-medium"
               label="Name*"
               placeholder="April O'Neil"
-              value={user?.user_metadata.full_name}
-              disabled
+              required
+              name="nameInput"
             />
             <TextInput
               classNames="bg-light-slate-4 text-light-slate-11 font-medium"
@@ -138,6 +161,7 @@ const UserSettingsPage = ({ user }: userSettingsPageProps) => {
               }}
               label="Email*"
               value={email}
+              required
             />
 
             {/* Bio section */}
@@ -147,39 +171,33 @@ const UserSettingsPage = ({ user }: userSettingsPageProps) => {
                 rows={4}
                 placeholder="Tell us about yourself."
                 className="bg-light-slate-4 rounded-lg px-3 py-2 disabled:cursor-not-allowed "
-                readOnly
-                value={
-                  userInfo?.bio ||
-                  "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."
-                }
+                name="bio"
               ></textarea>
             </div>
             <TextInput
               classNames="bg-light-slate-4 text-light-slate-11 font-medium"
               placeholder="https://opensauced.pizza"
               label="URL"
-              disabled
+              pattern="http[s]?://.*\..{2,}"
+              name="url"
             />
             <TextInput
               classNames="bg-light-slate-4 text-light-slate-11"
               placeholder="@saucedopen"
               label="Twitter Username"
-              disabled
-              value={`@${(userInfo && userInfo.twitter_username) || "saucedopen"}`}
+              name="twitter_username"
             />
             <TextInput
               classNames="bg-light-slate-4 text-light-slate-11 font-medium"
               placeholder="OpenSauced"
               label="Company"
-              disabled
-              value={userInfo?.company || "OpenSauced"}
+              name="company"
             />
             <TextInput
               classNames="bg-light-slate-4 text-light-slate-11 font-medium"
               placeholder="USA"
               label="Location"
-              disabled
-              value={userInfo?.location || "California"}
+              name="location"
             />
             <div>
               <Checkbox
@@ -194,8 +212,8 @@ const UserSettingsPage = ({ user }: userSettingsPageProps) => {
             </div>
             <div className="flex flex-col gap-2">
               <label>Time zone*</label>
-              <Select value={timezone} onChange={e => setTimezone(e.target.value)}>
-                <SelectOption value="select timezone">Select time zone</SelectOption>
+              <Select value={timezone} onChange={e => setTimezone(e.target.value)} required>
+                <SelectOption value="">Select time zone</SelectOption>
                 {timezones.map((timezone, index) => (
                   <SelectOption key={index} value={timezone.value}>
                     {timezone.text}
@@ -203,7 +221,7 @@ const UserSettingsPage = ({ user }: userSettingsPageProps) => {
                 ))}
               </Select>
             </div>
-            <Button disabled={!isValidEmail} onClick={handleUpdateProfile} type="primary">
+            <Button disabled={!isValidEmail} type="primary">
               Update profile
             </Button>
           </form>
diff --git a/interfaces/with-page-layout.ts b/interfaces/with-page-layout.ts
index 2e9d88a6..7b5fb3e3 100644
--- a/interfaces/with-page-layout.ts
+++ b/interfaces/with-page-layout.ts
@@ -2,4 +2,6 @@ import type { NextPage } from "next";
 
 export type WithPageLayout = NextPage & {
     PageLayout?: React.ComponentType<any>;
-};
\ No newline at end of file
+    SEO?: SEOobject;
+    updateSEO?: (SEO: SEOobject) => void;
+};
diff --git a/jest.config.js b/jest.config.js
index 3a46e96b..797ac2ed 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -6,7 +6,7 @@ const createJestConfig = nextJest({
 
 const customJestConfig = {
   moduleDirectories: ["node_modules", "<rootDir>/"],
-  modulePathIgnorePatterns: ['e2e-tests']
+  modulePathIgnorePatterns: ["e2e-tests"]
   /* testEnvironment: "jest-environment-jsdom" */
 };
 
diff --git a/layouts/SEO/SEO.tsx b/layouts/SEO/SEO.tsx
new file mode 100644
index 00000000..0d9a921d
--- /dev/null
+++ b/layouts/SEO/SEO.tsx
@@ -0,0 +1,25 @@
+import Head from "next/head";
+
+export default function SEO({description, image, title, twitterCard, noindex}: SEOobject) {
+
+  return (
+    <Head>
+      <title key="title">{title || "OpenSauced Insights"}</title>
+      <meta name="title" content={title || "OpenSauced Insights"} key="metatitle" />
+      <meta name="description" content={description || "The open-source intelligence platform for developers and maintainers. Unlock the power of open source with project insights by the slice."} key="description" />
+
+      <meta property="og:title" content={title || "OpenSauced Insights"} key="og:title" />
+      <meta property="og:description" content={description || "The open-source intelligence platform for developers and maintainers. Unlock the power of open source with project insights by the slice."} key="og:description" />
+      {image && <meta property="og:image" content={image} key="og:image" />}
+      <meta property="og:site_name" content="OpenSauced Insights" key="og:site_name" />
+      <meta property="og:type" content="website" key="og:type" />
+
+      <meta name="twitter:title" content={title || "OpenSauced Insights"} key="twitter:title" />
+      <meta name="twitter:description" content={description || "The open-source intelligence platform for developers and maintainers. Unlock the power of open source with project insights by the slice."} key="twitter:description" />
+      {image && <meta name="twitter:image" content={image} key="twitter:image" />}
+      <meta name="twitter:card" content={twitterCard || "summary"} key="twitter:card" />
+
+      {noindex && <meta name="robots" content="noindex" key="noindex" />}
+    </Head>
+  );
+}
diff --git a/lib/hooks/update-user.ts b/lib/hooks/update-user.ts
index 678715ab..7cb43a3d 100644
--- a/lib/hooks/update-user.ts
+++ b/lib/hooks/update-user.ts
@@ -1,7 +1,18 @@
 import { supabase } from "lib/utils/supabase";
 
+export interface updateUserPayload {
+  name?: string;
+  email?: string;
+  bio?: string;
+  url?: string;
+  twitter_username?: string;
+  company?: string;
+  location?: string;
+  interests?: string[]; display_local_time?: boolean; timezone?: string;
+}
+
 interface useUpdateUserProps {
-  data: { email?: string; interests?: string[]; display_local_time?: boolean; timezone?: string };
+  data: updateUserPayload;
   params?: string;
 }
 
diff --git a/lib/utils/check-camelcase-naming.ts b/lib/utils/check-camelcase-naming.ts
new file mode 100644
index 00000000..a98db444
--- /dev/null
+++ b/lib/utils/check-camelcase-naming.ts
@@ -0,0 +1,11 @@
+import uppercaseFirst from "./uppercase-first";
+
+/** Checks the input and if it should be camelcased, it will camelcase it. Uppercases the output. */
+const checkCamelCaseNaming = (name: string) => {
+  if (!name) return "";
+
+  if(name.toLowerCase() == "javascript") return "JavaScript";
+  else return uppercaseFirst(name);
+};
+
+export default checkCamelCaseNaming;
\ No newline at end of file
diff --git a/lib/utils/date-utils.ts b/lib/utils/date-utils.ts
index dfa6bc17..09e81abe 100644
--- a/lib/utils/date-utils.ts
+++ b/lib/utils/date-utils.ts
@@ -36,11 +36,11 @@ export const getRelativeDays = (days: number) => {
   }
 
   if (days >= 365) {
-    return `${days}y`;
+    return `${Math.floor(days / 365)}y`;
   }
 
   if (days > 30 && days < 365) {
-    return `${Math.floor(days / 12)}mo`;
+    return `${Math.floor(days / 30)}mo`;
   }
 
   return `${days}d`;
@@ -50,11 +50,11 @@ export const calcDistanceFromToday = (endDate: Date) => {
   const daysFromNow = calcDaysFromToday(endDate);
 
   if (daysFromNow >= 365) {
-    return `${getRelativeDays(calcYearsFromToday(endDate))}`;
+    return `${getRelativeDays(calcDaysFromToday(endDate))}`;
   }
 
   if (daysFromNow > 30 && daysFromNow < 365) {
-    return `${getRelativeDays(calcMonthsFromToday(endDate))}`;
+    return `${getRelativeDays(calcDaysFromToday(endDate))}`;
   }
 
   return `${getRelativeDays(daysFromNow)}`;
diff --git a/next-types.d.ts b/next-types.d.ts
index e573a14f..694e7616 100644
--- a/next-types.d.ts
+++ b/next-types.d.ts
@@ -132,3 +132,11 @@ interface DbUser {
   readonly display_email: boolean;
   readonly timezone: string;
 }
+
+interface SEOobject {
+  title?: string;
+  description?: string;
+  image?: string;
+  twitterCard?: string;
+  noindex?: boolean;
+}
diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json
index 958efabb..64c8f4cc 100644
--- a/npm-shrinkwrap.json
+++ b/npm-shrinkwrap.json
@@ -1,12 +1,12 @@
 {
   "name": "@open-sauced/insights",
-  "version": "1.28.0",
+  "version": "1.29.0-beta.7",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "@open-sauced/insights",
-      "version": "1.28.0",
+      "version": "1.29.0-beta.7",
       "hasInstallScript": true,
       "license": "MIT",
       "dependencies": {
diff --git a/package.json b/package.json
index beccc4c4..0b4f2fc0 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.28.0",
+  "version": "1.29.0-beta.7",
   "author": "TED Vortex <ted.vortex@gmail.com>",
   "private": true,
   "license": "MIT",
diff --git a/pages/[filterName]/[toolName].tsx b/pages/[filterName]/[toolName].tsx
index 8e4f8aef..18d73ad8 100644
--- a/pages/[filterName]/[toolName].tsx
+++ b/pages/[filterName]/[toolName].tsx
@@ -3,11 +3,22 @@ import { WithPageLayout } from "../../interfaces/with-page-layout";
 import { useRouter } from "next/router";
 import Tool from "components/organisms/ToolsDisplay/tools-display";
 import changeCapitalization from "lib/utils/change-capitalization";
+import { useEffect } from "react";
 
 const Filter: WithPageLayout = () => {
   const router = useRouter();
-  
-  const { toolName } = router.query;
+
+  const { filterName, toolName } = router.query;
+
+  const title = `Open Sauced Insights ${filterName ? ` - ${changeCapitalization(filterName!.toString(), true)}` : ""} ${
+    toolName ? ` / ${changeCapitalization(toolName.toString(), true)}` : ""
+  }`;
+
+  useEffect( () => {
+    Filter.updateSEO!({
+      title: title
+    });
+  }, [title]);
 
   return (
     <Tool tool={toolName ? changeCapitalization(toolName.toString(), true) : undefined} />
@@ -16,4 +27,4 @@ const Filter: WithPageLayout = () => {
 
 Filter.PageLayout = FilterLayout;
 
-export default Filter;
\ No newline at end of file
+export default Filter;
diff --git a/pages/[filterName]/[toolName]/[filter]/[...selectedFilter].tsx b/pages/[filterName]/[toolName]/[filter]/[...selectedFilter].tsx
index 7165c34d..229c076d 100644
--- a/pages/[filterName]/[toolName]/[filter]/[...selectedFilter].tsx
+++ b/pages/[filterName]/[toolName]/[filter]/[...selectedFilter].tsx
@@ -4,11 +4,22 @@ import FilterLayout from "layouts/filter";
 import { WithPageLayout } from "interfaces/with-page-layout";
 import Tool from "components/organisms/ToolsDisplay/tools-display";
 import changeCapitalization from "lib/utils/change-capitalization";
+import { useEffect } from "react";
 
 const SelectedFilter: WithPageLayout = () => {
   const router = useRouter();
 
-  const { toolName } = router.query;
+  const { toolName, filterName } = router.query;
+
+  const title = `Open Sauced Insights ${filterName ? ` - ${changeCapitalization(filterName!.toString(), true)}` : ""} ${
+    toolName ? ` / ${changeCapitalization(toolName.toString(), true)}` : ""
+  }`;
+
+  useEffect( () => {
+    SelectedFilter.updateSEO!({
+      title: title
+    });
+  }, [title]);
 
   return <Tool tool={toolName ? changeCapitalization(toolName.toString(), true) : undefined} />;
 };
diff --git a/pages/[filterName]/insights/[insightId]/edit.tsx b/pages/[filterName]/insights/[insightId]/edit.tsx
index 452df682..2133159e 100644
--- a/pages/[filterName]/insights/[insightId]/edit.tsx
+++ b/pages/[filterName]/insights/[insightId]/edit.tsx
@@ -39,5 +39,9 @@ const EditInsightPage: WithPageLayout = () => {
 };
 
 EditInsightPage.PageLayout = HubLayout;
+EditInsightPage.SEO = {
+  title: "Edit Insight Page | Open Sauced Insights",
+  noindex: true
+};
 
 export default EditInsightPage;
diff --git a/pages/[filterName]/insights/index.tsx b/pages/[filterName]/insights/index.tsx
index 27ba8cbf..b08959c3 100644
--- a/pages/[filterName]/insights/index.tsx
+++ b/pages/[filterName]/insights/index.tsx
@@ -32,7 +32,7 @@ const InsightsHub: WithPageLayout = () => {
         router.push("/");
       }
     }
-    
+
     getUser();
   }, [router, onboarded]);
 
@@ -70,14 +70,17 @@ const InsightsHub: WithPageLayout = () => {
         passHref
         href={"/hub/insights/new"}
         className="w-full bg-light-slate-4 text-lg text-light-slate-11 py-5 md:py-8 lg:py-10 rounded-lg text-center border border-light-slate-7">
-        
+
           Create a new Insight Page
-        
+
       </Link>
     </div>
   ) : <></>;
 };
 
 InsightsHub.PageLayout = HubLayout;
+InsightsHub.SEO = {
+  title: "Insights Hub | Open Sauced Insights"
+};
 
 export default InsightsHub;
diff --git a/pages/[filterName]/insights/new.tsx b/pages/[filterName]/insights/new.tsx
index 60d43476..b2e1c319 100644
--- a/pages/[filterName]/insights/new.tsx
+++ b/pages/[filterName]/insights/new.tsx
@@ -8,5 +8,9 @@ const NewInsightPage: WithPageLayout = () => {
 };
 
 NewInsightPage.PageLayout = HubLayout;
+NewInsightPage.SEO = {
+  title: "Create Insight Page | Open Sauced Insights",
+  description: "An insight page is a dashboard containing selected repositories that you and your team can get insights from."
+};
 
 export default NewInsightPage;
diff --git a/pages/_app.tsx b/pages/_app.tsx
index 993be986..5d08529b 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -3,7 +3,7 @@
 import "../styles/globals.css";
 import "react-loading-skeleton/dist/skeleton.css";
 
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
 import Head from "next/head";
 import type { AppProps } from "next/app";
 import { useRouter } from "next/router";
@@ -18,15 +18,24 @@ import apiFetcher from "lib/hooks/useSWR";
 import { initiateAnalytics } from "lib/utils/analytics";
 import { supabase } from "lib/utils/supabase";
 import { Toaster } from "react-hot-toast";
+import SEO from "layouts/SEO/SEO";
 
 type ComponentWithPageLayout = AppProps & {
   Component: AppProps["Component"] & {
     PageLayout?: React.ComponentType<any>;
+    SEO?: SEOobject;
+    updateSEO?: (SEO: SEOobject) => void;
   };
 };
 
 function MyApp({ Component, pageProps }: ComponentWithPageLayout) {
   const router = useRouter();
+  const [seo, updateSEO] = useState<SEOobject>(Component.SEO || {});
+  Component.updateSEO = updateSEO;
+
+  useEffect(() => {
+    updateSEO(Component.SEO || {});
+  }, [Component]);
 
   // From documentation on using Posthog with Next.js: https://posthog.com/docs/integrate/third-party/next-js
   useEffect(() => {
@@ -72,26 +81,13 @@ function MyApp({ Component, pageProps }: ComponentWithPageLayout) {
     }
   }
 
-  const title = `Open Sauced Insights ${filterName ? ` - ${changeCapitalization(filterName!.toString(), true)}` : ""} ${
-    toolName ? ` / ${changeCapitalization(toolName.toString(), true)}` : ""
-  }`;
-
   return (
     <>
       <Head>
-        <title>{title}</title>
         <link rel="icon" href="/favicon.ico" />
-        <meta property="og:url" content="https://insights.opensauced.pizza" />
-        <meta property="og:type" content="website" />
-        <meta property="og:title" content="OpenSauced Insights" />
-        <meta name="twitter:card" content="summary" />
-        <meta
-          property="og:description"
-          content="The open-source intelligence platform for developers and maintainers. Unlock the power of open source with project insights by the slice."
-        />
-        <meta property="og:image" content="/social-card.png" />
-        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
+        <meta name="viewport" content="width=device-width, initial-scale=1" />
       </Head>
+      <SEO {...seo} />
 
       <SWRConfig
         value={{
@@ -103,6 +99,7 @@ function MyApp({ Component, pageProps }: ComponentWithPageLayout) {
         <Toaster position="top-center" />
         <SessionContextProvider supabaseClient={supabase} initialSession={pageProps.initialSession}>
           <TipProvider>
+
             {Component.PageLayout ? (
               <Component.PageLayout>
                 <Component {...pageProps} />
diff --git a/pages/index.tsx b/pages/index.tsx
index 5553d8af..87567899 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -5,6 +5,7 @@ import { useRouter } from "next/router";
 import Loader from "components/templates/Loader/loader";
 import { supabase } from "lib/utils/supabase";
 import useSession from "lib/hooks/useSession";
+import Head from "next/head";
 
 const Home: WithPageLayout = () => {
   const router = useRouter();
diff --git a/pages/pages/[userOrg]/[filterName]/[toolName].tsx b/pages/pages/[userOrg]/[filterName]/[toolName].tsx
index 17056c1d..63ea75d4 100644
--- a/pages/pages/[userOrg]/[filterName]/[toolName].tsx
+++ b/pages/pages/[userOrg]/[filterName]/[toolName].tsx
@@ -8,6 +8,8 @@ import HubPageLayout from "layouts/hub-page";
 import { WithPageLayout } from "interfaces/with-page-layout";
 import changeCapitalization from "lib/utils/change-capitalization";
 import useInsight from "lib/hooks/useInsight";
+import Head from "next/head";
+import { useEffect } from "react";
 
 const HubPage: WithPageLayout = () => {
   const router = useRouter();
@@ -16,6 +18,14 @@ const HubPage: WithPageLayout = () => {
   const { data: insight, isLoading, isError } = useInsight(insightId);
   const repositories = insight ? insight.repos.map(repo => repo.repo_id) : [];
 
+  const title = `${insight && insight.name} | Open Sauced Insights Hub`;
+
+  useEffect( () => {
+    HubPage.updateSEO!({
+      title: title
+    });
+  }, [title]);
+
   return (
     <>
       { isLoading ? <div>Loading...</div>: "" }
@@ -67,4 +77,4 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
 
 HubPage.PageLayout = HubPageLayout;
 
-export default HubPage;
\ No newline at end of file
+export default HubPage;
diff --git a/pages/pages/[userOrg]/[filterName]/[toolName]/filter/[...selectedFilter].tsx b/pages/pages/[userOrg]/[filterName]/[toolName]/filter/[...selectedFilter].tsx
index 17056c1d..63ea75d4 100644
--- a/pages/pages/[userOrg]/[filterName]/[toolName]/filter/[...selectedFilter].tsx
+++ b/pages/pages/[userOrg]/[filterName]/[toolName]/filter/[...selectedFilter].tsx
@@ -8,6 +8,8 @@ import HubPageLayout from "layouts/hub-page";
 import { WithPageLayout } from "interfaces/with-page-layout";
 import changeCapitalization from "lib/utils/change-capitalization";
 import useInsight from "lib/hooks/useInsight";
+import Head from "next/head";
+import { useEffect } from "react";
 
 const HubPage: WithPageLayout = () => {
   const router = useRouter();
@@ -16,6 +18,14 @@ const HubPage: WithPageLayout = () => {
   const { data: insight, isLoading, isError } = useInsight(insightId);
   const repositories = insight ? insight.repos.map(repo => repo.repo_id) : [];
 
+  const title = `${insight && insight.name} | Open Sauced Insights Hub`;
+
+  useEffect( () => {
+    HubPage.updateSEO!({
+      title: title
+    });
+  }, [title]);
+
   return (
     <>
       { isLoading ? <div>Loading...</div>: "" }
@@ -67,4 +77,4 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
 
 HubPage.PageLayout = HubPageLayout;
 
-export default HubPage;
\ No newline at end of file
+export default HubPage;
diff --git a/pages/user/[username].tsx b/pages/user/[username].tsx
index 5e1c5f12..ad4fc75a 100644
--- a/pages/user/[username].tsx
+++ b/pages/user/[username].tsx
@@ -7,8 +7,12 @@ import { ContributorsProfileType } from "components/molecules/ContributorHoverCa
 
 import ProfileLayout from "layouts/profile";
 import { useFetchUser } from "lib/hooks/useFetchUser";
+import Head from "next/head";
+import SEO from "layouts/SEO/SEO";
+import { WithPageLayout } from "interfaces/with-page-layout";
+import { useEffect } from "react";
 
-const Contributor = (): JSX.Element => {
+const Contributor: WithPageLayout = () => {
   const router = useRouter();
   const { username } = router.query;
   const contributorLogin = username as string;
@@ -25,6 +29,15 @@ const Contributor = (): JSX.Element => {
     totalPR: contributor[0]?.recent_pr_total
   };
 
+  useEffect( () => {
+    Contributor.updateSEO!({
+      title: `${contributorLogin} | OpenSauced`,
+      description: `${user?.bio || "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."}`,
+      image: profile.githubAvatar,
+      twitterCard: "summary_large_image"
+    });
+  }, [contributorLogin, user?.bio, profile.githubAvatar]);
+
   return (
     <div className="w-full">
       <ContributorProfilePage
diff --git a/pages/user/settings.tsx b/pages/user/settings.tsx
index 2f11a98b..ba422d28 100644
--- a/pages/user/settings.tsx
+++ b/pages/user/settings.tsx
@@ -2,8 +2,10 @@ import useSupabaseAuth from "lib/hooks/useSupabaseAuth";
 
 import UserSettingsPage from "components/organisms/UserSettingsPage/user-settings-page";
 import ProfileLayout from "layouts/profile";
+import Head from "next/head";
+import { WithPageLayout } from "interfaces/with-page-layout";
 
-const ProfileSettings = (): JSX.Element => {
+const ProfileSettings: WithPageLayout = () => {
   const { user } = useSupabaseAuth();
 
   return (
@@ -20,4 +22,8 @@ const ProfileSettings = (): JSX.Element => {
 };
 
 ProfileSettings.PageLayout = ProfileLayout;
+ProfileSettings.SEO = {
+  title: "Account Settings | OpenSauced Insights",
+  noindex: true
+};
 export default ProfileSettings;
diff --git a/playwright.config.ts b/playwright.config.ts
index afa20f3f..0892c9f9 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -1,5 +1,5 @@
-import type { PlaywrightTestConfig } from '@playwright/test';
-import { devices } from '@playwright/test';
+import type { PlaywrightTestConfig } from "@playwright/test";
+import { devices } from "@playwright/test";
 
 /**
  * Read environment variables from file.
@@ -11,7 +11,7 @@ import { devices } from '@playwright/test';
  * See https://playwright.dev/docs/test-configuration.
  */
 const config: PlaywrightTestConfig = {
-  testDir: './e2e-tests',
+  testDir: "./e2e-tests",
   /* Maximum time one test can run for. */
   timeout: 30 * 1000,
   expect: {
@@ -30,40 +30,40 @@ const config: PlaywrightTestConfig = {
   /* Opt out of parallel tests on CI. */
   workers: process.env.CI ? 1 : undefined,
   /* Reporter to use. See https://playwright.dev/docs/test-reporters */
-  reporter: 'html',
+  reporter: "html",
   /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
   use: {
     /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
     actionTimeout: 0,
     /* Base URL to use in actions like `await page.goto('/')`. */
-    baseURL: 'http://localhost:3000',
+    baseURL: "http://localhost:3000",
 
     /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
-    trace: 'on-first-retry',
+    trace: "on-first-retry"
   },
 
   /* Configure projects for major browsers */
   projects: [
     {
-      name: 'chromium',
+      name: "chromium",
       use: {
-        ...devices['Desktop Chrome'],
-      },
+        ...devices["Desktop Chrome"]
+      }
     },
 
     {
-      name: 'firefox',
+      name: "firefox",
       use: {
-        ...devices['Desktop Firefox'],
-      },
+        ...devices["Desktop Firefox"]
+      }
     },
 
     {
-      name: 'webkit',
+      name: "webkit",
       use: {
-        ...devices['Desktop Safari'],
-      },
-    },
+        ...devices["Desktop Safari"]
+      }
+    }
 
     /* Test against mobile viewports. */
     // {
@@ -99,9 +99,9 @@ const config: PlaywrightTestConfig = {
 
   /* Run your local dev server before starting the tests */
   webServer: {
-    command: 'npm run dev',
-    port: 3000,
-  },
+    command: "npm run dev",
+    port: 3000
+  }
 };
 
 export default config;
diff --git a/stories/molecules/highlights-filter-card.stories.tsx b/stories/molecules/highlights-filter-card.stories.tsx
new file mode 100644
index 00000000..68ca6241
--- /dev/null
+++ b/stories/molecules/highlights-filter-card.stories.tsx
@@ -0,0 +1,16 @@
+import { TooltipProvider } from "@radix-ui/react-tooltip";
+import { ComponentStory } from "@storybook/react";
+import HighlightsFilterCard from "components/molecules/HighlightsFeedCard/highlights-filter-card";
+
+const StoryConfig = {
+  title: "Design System/Molecules/HighlightsFilterCard"
+};
+export default StoryConfig;
+
+const HighlightsFilterCardTemplate: ComponentStory<typeof HighlightsFilterCard> = () => (
+  <TooltipProvider>
+    <HighlightsFilterCard />
+  </TooltipProvider>
+);
+
+export const Default = HighlightsFilterCardTemplate.bind({});


Test patch:
diff --git a/components/molecules/LatestPrTableRow/latest-pr-table-row.tsx b/components/molecules/LatestPrTableRow/latest-pr-table-row.tsx
index 0faca368..ed83e5cd 100644
--- a/components/molecules/LatestPrTableRow/latest-pr-table-row.tsx
+++ b/components/molecules/LatestPrTableRow/latest-pr-table-row.tsx
@@ -38,7 +38,7 @@ const LatestPrTableRow = ({
 }: LatestPrTableRowProps) => {
   return (
     <div className="flex gap-2 items-center px-2 py-1">
-      <div className={`flex cursor-default item-center gap-2 w-3/5 ${isHoverCard && "w-5/6"}`}>
+      <div className={`flex item-center gap-2 w-3/5 ${isHoverCard && "w-5/6"}`}>
         {prStatus === "open" ? (
           <IconContext.Provider value={{ color: "green", style: { width: 14, height: 14, marginTop: 2 } }}>
             <VscGitPullRequest title="Open Pull Request" />
@@ -59,7 +59,7 @@ const LatestPrTableRow = ({
           </IconContext.Provider>
         )}
         <Text title="updated date">{calcDistanceFromToday(new Date(parseInt(prUpdatedTime, 10)))}</Text>
-        <Text title={prName} className="!text-light-slate-12 !w-32 md:!w-72  !truncate ">
+        <Text title={prName} className="!text-light-slate-12 !w-32 md:!w-72 !truncate">
           <a href={`https://github.com/${repoOwner}/${repoName}/pull/${prNumber}`} target="_blank" rel="noreferrer">
             {prName}
           </a>
diff --git a/e2e-tests/UnsignedUsersRedirects.spec.ts b/e2e-tests/UnsignedUsersRedirects.spec.ts
index c40c22fc..e30aab85 100644
--- a/e2e-tests/UnsignedUsersRedirects.spec.ts
+++ b/e2e-tests/UnsignedUsersRedirects.spec.ts
@@ -1,12 +1,12 @@
-import { test, expect } from '@playwright/test';
+import { test, expect } from "@playwright/test";
 
-test('Homepage redirects to /javascript for unsigned users', async ({ page }) => {
-  await page.goto('/');
-  await expect(page).toHaveURL(/.*\/javascript.*/, { timeout: 10000 })
+test("Homepage redirects to /javascript for unsigned users", async ({ page }) => {
+  await page.goto("/");
+  await expect(page).toHaveURL(/.*\/javascript.*/, { timeout: 10000 });
 });
 
-test('Insights Hub redirects to /javascript for unsigned users', async ({ page }) => {
-  await page.goto('/hub/insights');
-  await expect(page).toHaveURL('/')
-  await expect(page).toHaveURL(/.*\/javascript.*/, { timeout: 10000 })
+test("Insights Hub redirects to /javascript for unsigned users", async ({ page }) => {
+  await page.goto("/hub/insights");
+  await expect(page).toHaveURL("/");
+  await expect(page).toHaveURL(/.*\/javascript.*/, { timeout: 10000 });
 });
diff --git a/tests/lib/utils/check-camelcase-naming.test.ts b/tests/lib/utils/check-camelcase-naming.test.ts
new file mode 100644
index 00000000..03cbc073
--- /dev/null
+++ b/tests/lib/utils/check-camelcase-naming.test.ts
@@ -0,0 +1,15 @@
+import checkCamelCaseNaming from "lib/utils/check-camelcase-naming";
+
+describe("[lib] checkCamelCaseNaming()", () => {
+  it("should make javascript camelCase and uppercase first letter", () => {
+    const testString = "javascript";
+    const result = checkCamelCaseNaming(testString);
+    expect(result).toBe("JavaScript");
+  });
+
+  it("should uppercase python without camelCase", () => {
+    const testString = "python";
+    const result = checkCamelCaseNaming(testString);
+    expect(result).toBe("Python");
+  });
+});
diff --git a/tests/lib/utils/date-utils.test.ts b/tests/lib/utils/date-utils.test.ts
new file mode 100644
index 00000000..8417571a
--- /dev/null
+++ b/tests/lib/utils/date-utils.test.ts
@@ -0,0 +1,84 @@
+import { calcDaysFromToday, calcDistanceFromToday, calcMonthsFromToday, calcYearsFromToday, getRelativeDays } from "lib/utils/date-utils";
+
+
+describe("[lib] date-utils methods", () => {
+
+  // from lib\utils\date-utils.ts
+
+  // calcDaysfromToday
+  it("calcDaysFromToday() Should return the number of days from today", () => {
+    let dayBefore = new Date();
+    dayBefore.setDate(new Date().getDate() - 7);
+    const result = calcDaysFromToday(dayBefore);
+
+    expect(result).toEqual(7);
+  });
+
+  // calcMonthsFromToday
+  it("calcMonthsFromToday() Should return the number of months from today", () => {
+    let monthBefore = new Date();
+    monthBefore.setMonth(new Date().getMonth() - 3);
+    const result = calcMonthsFromToday(monthBefore);
+
+    expect(result).toEqual(3);
+  });
+
+  // calcYearsFromToday
+  it("calcYearsFromToday() Should return the number of years from today", () => {
+    let yearBefore = new Date();
+    yearBefore.setFullYear(new Date().getFullYear() - 4);
+    const result = calcYearsFromToday(yearBefore);
+
+    expect(result).toEqual(4);
+  });
+
+  // getRelativeDays
+  it("getRelativeDays() Should properly produce formats depending on the number of days", () => {
+
+    let days = 0;
+    let result = getRelativeDays(days);
+    expect(result).toEqual("-");
+
+    days = 29;
+    result = getRelativeDays(days);
+    expect(result).toEqual("29d");
+
+    days = 60;
+    result = getRelativeDays(days);
+    expect(result).toEqual("2mo");
+
+    days = 367;
+    result = getRelativeDays(days);
+    expect(result).toEqual("1y");
+  });
+
+  // calcDistanceFromToday
+  it("calcDistanceFromToday() Should return distance between today and a date formatted to best time unit", () => {
+    let date1 = new Date();
+    date1.setDate(new Date().getDate() - 5);
+    const result = calcDistanceFromToday(date1);
+    expect(result).toEqual("5d");
+
+    let date2 = new Date();
+    date2.setMonth(new Date().getMonth() - 8);
+    const result2 = calcDistanceFromToday(date2);
+    expect(result2).toEqual("8mo");
+
+    let date3 = new Date();
+    date3.setFullYear(new Date().getFullYear() - 4);
+    const result3 = calcDistanceFromToday(date3);
+    expect(result3).toEqual("4y");
+
+    let date4 = new Date();
+    date4.setMonth(new Date().getMonth() - 24);
+    const result4 = calcDistanceFromToday(date4);
+    expect(result4).toEqual("2y");
+
+    let date5 = new Date();
+    date5.setDate(new Date().getDate() - 65);
+    const result5 = calcDistanceFromToday(date5);
+    expect(result5).toEqual("2mo");
+
+  });
+
+});
diff --git a/tests/lib/utils/get-filter-key.test.ts b/tests/lib/utils/get-filter-key.test.ts
new file mode 100644
index 00000000..6b5acb3b
--- /dev/null
+++ b/tests/lib/utils/get-filter-key.test.ts
@@ -0,0 +1,10 @@
+import getFilterKey from "lib/utils/get-filter-key";
+
+describe("[lib] getFilterKey()", () => {
+// read the function and give me a description of what it does COPILOT
+  it("Should format the string for filter key", () => {
+    const testString = "FILTER BY DATE";
+    const result = getFilterKey(testString);
+    expect(result).toEqual("filter-by-date");
+  });
+});
diff --git a/tests/lib/utils/get-filter-query.test.ts b/tests/lib/utils/get-filter-query.test.ts
new file mode 100644
index 00000000..8743b7f1
--- /dev/null
+++ b/tests/lib/utils/get-filter-query.test.ts
@@ -0,0 +1,27 @@
+import getFilterQuery from "lib/utils/get-filter-query";
+
+describe("[lib] getFilterQuery()", () => {
+
+  it("Should return a filter query if single string or array of 1 string", () => {
+    let testString = "recent";
+    let result = getFilterQuery(testString);
+    expect(result).toEqual("&filter=recent");
+
+    let testString2 = ["recent"];
+    result = getFilterQuery(testString);
+    expect(result).toEqual("&filter=recent");
+  });
+
+  it("Should return an empty string if no filter is provided", () => {
+    const testString = "";
+    const result = getFilterQuery(testString);
+    expect(result).toEqual("");
+  });
+
+  // in cae the input is string array of more than one item
+  it("Should return repo query if an array of 2 items", () => {
+    const testString = ["recent", "popular"];
+    const result = getFilterQuery(testString);
+    expect(result).toEqual("&repo=recent%2Fpopular");
+  });
+});
diff --git a/tests/lib/utils/get-percent.test.ts b/tests/lib/utils/get-percent.test.ts
new file mode 100644
index 00000000..6c939614
--- /dev/null
+++ b/tests/lib/utils/get-percent.test.ts
@@ -0,0 +1,12 @@
+import getPercent from "lib/utils/get-percent";
+
+describe("[lib] getPercent()", () => {
+
+  it("Should return correct percentages", () => {
+    expect(getPercent(100, 50)).toEqual(50);
+    expect(getPercent(100, 25)).toEqual(25);
+    expect(getPercent(100, 75)).toEqual(75);
+    expect(getPercent(100, 0)).toEqual(0);
+    expect(getPercent(100, 100)).toEqual(100);
+  });
+});
diff --git a/tests/lib/utils/github.test.ts b/tests/lib/utils/github.test.ts
new file mode 100644
index 00000000..88e35677
--- /dev/null
+++ b/tests/lib/utils/github.test.ts
@@ -0,0 +1,25 @@
+import { getAvatarById, getAvatarByUsername, getProfileLink, getRepoIssuesLink } 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");
+  });
+
+  it("Should return a valid avatar link", () => {
+    const result = getAvatarByUsername("Deadreyo", 460);
+    expect(result).toEqual("https://www.github.com/Deadreyo.png?size=460");
+  });
+
+  it("Should return a valid profile link", () => {
+    const result = getProfileLink("Deadreyo");
+    expect(result).toEqual("https://github.com/Deadreyo");
+  });
+
+  it("Should return a valid repo issues link", () => {
+    const result = getRepoIssuesLink("Deadreyo/insights");
+    expect(result).toEqual("https://github.com/Deadreyo/insights/issues");
+  });
+
+});
diff --git a/tests/lib/utils/timezones.test.ts b/tests/lib/utils/timezones.test.ts
new file mode 100644
index 00000000..e937ee1e
--- /dev/null
+++ b/tests/lib/utils/timezones.test.ts
@@ -0,0 +1,20 @@
+import { getTimezone } from "lib/utils/timezones";
+
+describe("[lib] getTimezone()", () => {
+
+  it("Should return the offset of a timezone", () => {
+    const result = getTimezone("Tonga Standard Time");
+
+    expect(result).toEqual(13);
+
+    const result2 = getTimezone("Pacific Standard Time");
+
+    expect(result2).toEqual(-8);
+  });
+
+  it("Should return +1 if timezone is not found", () => {
+    const result = getTimezone("Not a timezone");
+
+    expect(result).toEqual(1);
+  });
+});
diff --git a/tests/lib/utils/uppercase-first.test.ts b/tests/lib/utils/uppercase-first.test.ts
new file mode 100644
index 00000000..c3f35225
--- /dev/null
+++ b/tests/lib/utils/uppercase-first.test.ts
@@ -0,0 +1,11 @@
+import uppercaseFirst from "lib/utils/uppercase-first";
+
+describe("[lib] uppercaseFirst()", () => {
+
+  it("Should uppercase the first letter of a string", () => {
+    const testString = "chad";
+    const result = uppercaseFirst(testString);
+
+    expect(result).toEqual("Chad");
+  });
+});
diff --git a/tests/lib/utils/validate-email.test.ts b/tests/lib/utils/validate-email.test.ts
new file mode 100644
index 00000000..8ef0d0b7
--- /dev/null
+++ b/tests/lib/utils/validate-email.test.ts
@@ -0,0 +1,23 @@
+import { validateEmail } from "lib/utils/validate-email";
+
+describe("[lib] validateEmail()", () => {
+
+  it("Should return true if string is a valid email", () => {
+    const testString = "ahmedatwa@yahoo.com";
+    const result = validateEmail(testString);
+
+    expect(result).toBeTruthy();
+  });
+
+  it("Should return false if string is not a valid email", () => {
+    let testString = "ahmedatwayahoo.com";
+    let result = validateEmail(testString);
+
+    expect(result).toBeFalsy();
+
+    testString = "ahmedatwa@yahoo";
+
+    result = validateEmail(testString);
+    expect(result).toBeFalsy();
+  });
+});