DEV Community

Cover image for Introducing LinkedIn Job Scanner Chrome Extension and Sharing Problems I Encountered While Developing
SeongKuk Han
SeongKuk Han

Posted on

Introducing LinkedIn Job Scanner Chrome Extension and Sharing Problems I Encountered While Developing

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
Youtube How to use Video Link


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.



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


Enter fullscreen mode Exit fullscreen mode

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




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,
      },
    ]
  }
];


Enter fullscreen mode Exit fullscreen mode

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

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.



{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>
        ))}


Enter fullscreen mode Exit fullscreen mode

Referring to the official document, 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

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.



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;
  };



Enter fullscreen mode Exit fullscreen mode

3. Job Post Loading Check

Linkedin loading page

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.



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

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


Enter fullscreen mode Exit fullscreen mode

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



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);
    });
  });
});


Enter fullscreen mode Exit fullscreen mode

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, 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]



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`,
      },
    },
  },
});


Enter fullscreen mode Exit fullscreen mode

[vite-script.config.ts]



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',
      },
    },
  },
});


Enter fullscreen mode Exit fullscreen mode

[package.json]



{
  "scripts": {
    "build": "tsc && vite build && vite build -c vite-script.config.ts && browserify ./dist/assets/content.js -o ./dist/assets/content.js",
  }
}


Enter fullscreen mode Exit fullscreen mode

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.



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


Enter fullscreen mode Exit fullscreen mode

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. 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.

Top comments (2)

Collapse
 
oliviapandora profile image
Olivia Pandora Stokes

I think this is a smart idea, especially with the current LinkedIn algorithms. Good luck with your job search!

Collapse
 
lico profile image
SeongKuk Han

Thank you very much 👍