Introducing LinkedIn Job Scanner Chrome Extension and Sharing Problems I Encountered While Developing
Tags: react, job, webdev, productivity
Published: 2023-10-06 22:35:46

## Linkedin Job Scanner

Introducing the LinkedIn Job Scanner Chrome Extension!

Tired of sifting through countless job posts on LinkedIn? Let our extension do the heavy lifting for you! The LinkedIn Job Scanner Extension is here to help you find the job opportunities that match your keywords effortlessly.

[Chrome Webstore Download Link]
(https://chrome.google.com/webstore/detail/linkedin-job-scanner/jpfldhdekpcpihghnbhigmjmdcemgkhi?hl=en)
[Youtube How to use Video Link](https://www.youtube.com/watch?v=SbgCQaQ6fzI)

---

## Why I Created This Extension

As someone in search of frontend positions in Germany without proficiency in German, I faced the challenge of finding English-language job opportunities on LinkedIn that matched my skills. I quickly realized that sifting through countless job posts was both time-consuming and demotivating. Additionally, there were relatively fewer junior-level English positions available. During my job search, I found myself repeatedly reading job articles and scanning for specific keywords in job titles and descriptions. This gave me the idea to create an application that could automatically find job posts containing these keywords, making the job search process more manageable and less overwhelming.

---

I developed the extension using React in the Vite environment. In the following content, I'm going to share problems I encountered during development and how I dealt with them.

---

## 7 Problems I Encountered While Developing

### 1. Handling Updates with `useFieldArray` in `react-hook-form`

There is a condition type that has several sub-conditions.

```typescript
export interface JobCondition {
  id: string;
  subConditions: {
    id: string;
    not: boolean;
    caseInsensitive: boolean;
    target: ConditionTarget;
    operator: ConditionOperator;
    text: string;
    frequency: number;
  }[];
}
```

For example, if you want to look for job posts that contain `frontend` or `react` in the job title and `react` in the job description.

It will look like

```typescript

const conditions:JobCondition[] = [
  {
    id: 'jca',
    subConditions: [
      {
        id: 'sca',
        not: false,
        caseInsensitive: true,
        target: "title",
        operator: '>=',
        text: 'react',
        frequency: 1,
      },
      {
        id: 'scb',
        not: false,
        caseInsensitive: true,
        target: "title",
        operator: '>=',
        text: 'frontend',
        frequency: 1,
      }
    ]
  },
  {
    id: 'jcb',
    subConditions: [
      {
        id: 'sca',
        not: false,
        caseInsensitive: true,
        target: "description",
        operator: '>=',
        text: 'react',
        frequency: 1,
      },
    ]
  }
];
```

Each job post should satisfy all conditions, and meeting a condition implies that a job post fulfills at least one of the sub-conditions.

![extension UI to add conditions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8wdp71o0u73suvqkuz13.png)

If you click the button highlighted in red within the image, the sub-condition will be included in the list depicted in blue lines.

To manage form data, I opted for `react-hook-form` and used the `useFieldArray` for handling arrays. However, I faced an issue that updating the sub-conditions updates to the entire condition, causing a visible rendering flicker.

```typescript
{fields.map((item, index) => (
          <li key={item.id}>
            <input {...register(`test.${index}.firstName`)} />
            <Controller
              render={({ field }) => <input {...field} />}
              name={`test.${index}.lastName`}
              control={control}
            />
            <button type="button" onClick={() => remove(index)}>Delete</button>
          </li>
        ))}
``` 

Referring to [the official document](https://react-hook-form.com/docs/usefieldarray), it was suggested to utilize `item.id`. But I found that the `id` changes when the `update` function is called. I initially expected that changing the sub-conditions field within the item would only update that field.

There might have been other solutions, but I ended up replacing it with `useState`.

---

### 2. Data loss when the UI is closed

![Extension task create form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fqa7jq9377u4rnjto9w7.png)

When a user clicks outside of the UI, the extension is closed and the data is gone.

I encountered this issue multiple times during testing, so I addressed it by saving the user's work-in-progress data to `chrome.storage`.

```typescript
export interface TaskFormDraft {
  taskId: string | null;
  isEdit: boolean;
  value: {
    taskName: string;
    delay: number;
    jobConditions: JobCondition[];
  };
}

export interface StorageData {
  tasks?: JobTask[];
  activeTask?: ActiveJobTask;
  draft?: TaskFormDraft;
}

//...

export const draftTaskFormData = async (draftData: TaskFormDraft) => {
  if (!window.chrome?.storage?.local) return;
  const versionData = await chrome.storage.local.get(STORAGE_VERSION);
  const data = (versionData[STORAGE_VERSION] ?? {}) as StorageData;

  await chrome.storage.local.set({
    [STORAGE_VERSION]: {
      ...data,
      draft: draftData,
    },
  });
};

export const loadDraftTaskFormData =
  async (): Promise<TaskFormDraft | null> => {
    if (!window.chrome?.storage?.local) return null;
    const versionData = await chrome.storage.local.get(STORAGE_VERSION);
    const data = (versionData[STORAGE_VERSION] ?? {}) as StorageData;

    return data.draft ?? null;
  };

```

---

### 3. Job Post Loading Check

![Linkedin loading page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8alan43g7ncfsvnk5zvy.png)

This is the loading UI of a job post on Linkedin, but, the disappearance of the loading UI doesn't indicate that the job has finished loading.

I noticed that certain HTML elements have classes like "--loading" during loading, and skeleton elements have the "jobs-ghost-fadein-placeholder" class. 

I wrote a function like below to check whether a job post is done loading or not. However, this approach was more of a guess than an analysis.

```typescript
export const isLoading = () => {
  const loading =
    document.querySelector('#main *[class*=--loading]') ??
    document.querySelector('.jobs-ghost-fadein-placeholder');

  return loading !== null ? true : false;
};
```

I considered an alternative approach, which involved comparing the current job post's contents with the previous one. Fortunately, it seemed to work well.

Now that I think about it, I could have tried to capture all HTML variations to get more details.

---

### 4. DOM Test Code

I implemented functions that grab content from job posts. Initially, I thought about fetching the LinkedIn job listing page before testing, but since it doesn't allow us to see job posts without signing in, that approach wasn't feasible.

I downloaded HTML files by myself and loaded the HTML files in the test code.

![directory structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yymx9pk7xj21akbe1jrs.png)

```typescript
const jobListHtml = readFileSync(path.join(__dirname, 'html/joblist.html'), {
  encoding: 'utf-8',
  flag: 'r',
}).toString();

const jobListLoadingHtml = readFileSync(
  path.join(__dirname, 'html/joblist-detail-contents-loading.html'),
  {
    encoding: 'utf-8',
    flag: 'r',
  }
).toString();

const jobListLastPageHtml = readFileSync(
  path.join(__dirname, 'html/joblist-last-page.html'),
  {
    encoding: 'utf-8',
    flag: 'r',
  }
).toString();

describe('JobList Loading Page', () => {
  beforeAll(() => {
    document.documentElement.innerHTML = jobListLoadingHtml;
  });

  describe('isLoading', () => {
    test('should return true.', () => {
      expect(isLoading()).toBe(true);
    });
  });
});
```

It would be better in the future to have a program that downloads HTML files to update with the latest pages. In the process, signing up logic might be necessary.

---

### 5. Chrome Storage Test Code

Actually, I didn't consider testing `chrome.storage` as I aimed to complete the project quickly. I am unsure if there are existing methods for testing it. If not, we might be able to test it by creating mock functions for `chrome.storage`.

---

### 6. Module Error in Chrome Extention Content Script

While I don't have a deep understanding of the `Vite` build system, it seemed that sharing code between the React project and the content script, which runs in the background of the web browser, created a new file to optimize code sharing. Just to clarify, there are two entry points to generate two outputs.

It was okay, but the problem was that `import` didn't work in the content script. At first, I transpiled the code to a lower version of ES and used `commonjs` modules, but it still didn't work.

I found [Browserify](https://browserify.org/), which lets you `require` in the browser by bundling up all of my dependencies. I solved the content script problem after applying it, but the React project encountered some other issues.

I ended up using a separate configuration file for each entry point to avoid generating the shared code file and browserify the content script.

**[vite.config.ts]**
```typescript
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import tsconfigPaths from 'vite-tsconfig-paths';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), tsconfigPaths()],
  build: {
    rollupOptions: {
      input: {
        main: './index.html',
      },
      output: {
        entryFileNames: `assets/[name].js`,
      },
    },
  },
});
```

**[vite-script.config.ts]**
```typescript
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import tsconfigPaths from 'vite-tsconfig-paths';
import commonjs from '@rollup/plugin-commonjs';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), tsconfigPaths()],
  build: {
    emptyOutDir: false,
    target: 'ES2015',
    rollupOptions: {
      plugins: [commonjs()],
      input: {
        content: './src/content/content.ts',
      },
      output: {
        entryFileNames: `assets/[name].js`,
        format: 'cjs',
      },
    },
  },
});
```

**[package.json]**
```json
{
  "scripts": {
    "build": "tsc && vite build && vite build -c vite-script.config.ts && browserify ./dist/assets/content.js -o ./dist/assets/content.js",
  }
}
```

---

### 7. Ensuring the stability of the code in the repository 

The code in the repository must pass tests and build successfully. To ensure the stability of the code, I added a Github action that runs tests and builds the project when changes are pushed to the main branch.

```yml
name: Test

on:
  push:
    branches: ['main']

  workflow_dispatch:

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - uses: pnpm/action-setup@v2
        with:
          version: 8
      - name: Set up Node
        uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: 'pnpm'
      - name: Get pnpm store directory
        id: pnpm-cache
        shell: bash
        run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
      - name: Setup pnpm cache
        uses: actions/cache@v3
        with:
          path: ${{ env.STORE_PATH }}
          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
          restore-keys: |
            ${{ runner.os }}-pnpm-store-
      - name: Install dependencies
        run: pnpm install
        shell: bash
      - name: Run test files
        run: pnpm run test
      - name: Build
        run: pnpm run build
```

---

## Wrap Up

I found something interesting. Even though there were thousands of job posts, the extension couldn't reach all of them. The last page was 40(1000 job posts).

As I was eager to complete the project quickly while searching for jobs, There is still plenty of room for improvement and potential bugs.

If you have any suggestions or find bugs, please let me know through [the github issues](https://github.com/hsk-kr/linkedin-job-scanner). Your contributions to improving and refactoring the code would be appreciated as well.

Getting a developer position is absolutely brutal, much more than I expected. I have heard people saying they have failed hundreds, and even thousands, of applications. I also haven't received any positive responses from over 60 companies I applied to. I haven't even had one interview. I expected to fail some interviews due to my English skills, but I couldn't have expected not even getting a chance to fail. I felt frustrated by the results and thought about going back to my country. However, the people around me kept supporting and encouraging me. I truly appreciate the support from my friends. I am going to start job searching again.

I hope some people find the extension helpful. For me, the happiest moment as a developer is when I find my work is helpful to someone else.

Some of you may be in the same situation as I am. Don't give up, and let's keep trying together. Your efforts will pay.

There is a quote that I saw a few days ago in one of the feeds on LinkedIn. 

"You Never Fail Until You Stop Trying"

Thanks for reading this.