Repository: open-sauced/insights
Pull request #862 (2023-02-10 17:12:53)

feat: add unit testing for lib files
## What type of PR is this? (check all applicable)

- [ ] 🍕 Feature
- [ ] 🐛 Bug Fix
- [ ] 📝 Documentation Update
- [ ] 🎨 Style
- [ ] 🧑‍💻 Code Refactor
- [ ] 🔥 Performance Improvements
- [x] ✅ Test
- [ ] 🤖 Build
- [ ] 🔁 CI
- [ ] 📦 Chore (Release)
- [ ] ⏩ Revert

## Description
- Fixes the method `getRelativeDays` in `date-utils.ts`
- Adds unit tests to the simple lib functions.
<!-- 
Please do not leave this blank 
This PR [adds/removes/fixes/replaces] the [feature/bug/etc]. 
-->

## Related Tickets & Documents
<!-- 
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 
-->
closes #546 
## Mobile & Desktop Screenshots/Recordings

<!-- Visual changes require screenshots -->


## Added tests?

- [ ] 👍 yes
- [ ] 🙅 no, because they aren't needed
- [ ] 🙋 no, because I need help

## Added to documentation?

- [ ] 📜 README.md
- [ ] 📓 docs.opensauced.pizza
- [ ] 🍕 dev.to/opensauced
- [ ] 📕 storybook
- [ ] 🙅 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 -->



netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *design-insights* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 9edc78a353a65d58cf7cd86497a73d776386e72d |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/design-insights/deploys/63e67b17f4d88400088f7fd1 |
netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *oss-insights* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 9edc78a353a65d58cf7cd86497a73d776386e72d |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/oss-insights/deploys/63e67b1714b2530008ede02d |

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a3b7122e..0e2b6f2e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,20 @@
 
 > All notable changes to this project will be documented in this file
 
+## [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)
 
 
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/FilterHeader/filter-header.tsx b/components/molecules/FilterHeader/filter-header.tsx
index b96e467f..39ec782d 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();
@@ -35,7 +36,7 @@ const HeaderFilter = () => {
       </div>
       <div className="header-info 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{" "}
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/seo-type.ts b/interfaces/seo-type.ts
new file mode 100644
index 00000000..e62569c1
--- /dev/null
+++ b/interfaces/seo-type.ts
@@ -0,0 +1,7 @@
+export interface SEOobject {
+    title?: string;
+    description?: string;
+    image?: string;
+    twitterCard?: string;
+    noindex?: boolean;
+}
diff --git a/interfaces/with-page-layout.ts b/interfaces/with-page-layout.ts
index 2e9d88a6..ba490d71 100644
--- a/interfaces/with-page-layout.ts
+++ b/interfaces/with-page-layout.ts
@@ -1,5 +1,8 @@
 import type { NextPage } from "next";
+import { SEOobject } from "./seo-type";
 
 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..2e854754
--- /dev/null
+++ b/layouts/SEO/SEO.tsx
@@ -0,0 +1,26 @@
+import { SEOobject } from "interfaces/seo-type";
+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/npm-shrinkwrap.json b/npm-shrinkwrap.json
index f2022b75..afe135bf 100644
--- a/npm-shrinkwrap.json
+++ b/npm-shrinkwrap.json
@@ -1,12 +1,12 @@
 {
   "name": "@open-sauced/insights",
-  "version": "1.29.0-beta.3",
+  "version": "1.29.0-beta.5",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "@open-sauced/insights",
-      "version": "1.29.0-beta.3",
+      "version": "1.29.0-beta.5",
       "hasInstallScript": true,
       "license": "MIT",
       "dependencies": {
diff --git a/package.json b/package.json
index 71526c46..bdcb84dd 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.29.0-beta.3",
+  "version": "1.29.0-beta.5",
   "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..77c29969 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,25 @@ 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 { SEOobject } from "interfaces/seo-type";
+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 +82,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 +100,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;


Test patch:
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();
+  });
+});