Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/mauroperez055/infoJobs/llms.txt

Use this file to discover all available pages before exploring further.

InfoJobs DevBoard separates all non-trivial logic from its page and component files into custom hooks located in frontend/src/hooks/. This pattern keeps components declarative and easy to read, makes the logic independently testable, and allows the same behaviour to be reused across multiple components without prop drilling. Each hook follows React’s naming convention of prefixing with use.

useAISummary

useAISummary fetches an AI-generated summary for a given job listing from the backend’s streaming endpoint. It handles the full lifecycle: initialising the stream, reading each text chunk as it arrives, and accumulating the chunks into a single summary string that components can render progressively.

Signature

useAISummary(id: string) => {
  summary: string | null,
  loading: boolean,
  error: string | null,
  generateSummary: () => Promise<void>
}

Return values

NameTypeDescription
summarystring | nullAccumulated AI text. Grows chunk-by-chunk as the stream arrives. null before generation starts.
loadingbooleantrue while the stream is active
errorstring | nullError message if the fetch fails; null otherwise
generateSummaryfunctionAsync function that initiates the fetch and begins reading the stream

Full source

import { useState } from "react";

const API_URL = import.meta.env.VITE_API_URL;

export function useAISummary(id) {
  const [summary, setSummary]   = useState(null);
  const [loading, setLoading]   = useState(false);
  const [error,   setError]     = useState(null);

  const generateSummary = async () => {
    setLoading(true);
    setError(null);
    setSummary('');

    try {
      const response = await fetch(`${API_URL}/ai/summary/${id}`);

      if (!response.ok) {
        throw new Error('Error fetching summary');
      }

      // Stream handling via the Streams API
      const reader  = response.body.getReader();
      const decoder = new TextDecoder();

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        const chunkText = decoder.decode(value, { stream: true });
        setSummary(prev => prev + chunkText);
      }
    } catch {
      setError('Error al generar el resumen');
    } finally {
      setLoading(false);
    }
  };

  return { summary, loading, error, generateSummary };
}

Usage example

The AISummary component in Details.jsx shows a “Generar resumen con IA” button before the summary exists, then streams the text into the page once generation begins:
import { useAISummary } from "../hooks/useAISummary";

function AISummary({ id }) {
  const { summary, loading, generateSummary } = useAISummary(id);

  if (summary) {
    return (
      <section>
        <h2>✨ Resumen generado por IA</h2>
        <p>{summary}</p>
      </section>
    );
  }

  return (
    <button onClick={generateSummary} disabled={loading}>
      {loading ? 'Generando resumen...' : '✨ Generar resumen con IA'}
    </button>
  );
}

useFilters

useFilters is the central state-management hook for the /search page. It owns filter state (text query, technology, location, experience level), pagination state, and the fetched job results. On every state change it re-fetches the jobs from the API and keeps the browser URL synchronised with the current filter values via useSearchParams.

Signature

useFilters() => {
  jobs: object[],
  total: number,
  loading: boolean,
  totalPages: number,
  currentPage: number,
  filters: { technology: string, location: string, experienceLevel: string },
  textToFilter: string,
  handlePageChange: (page: number) => void,
  handleSearch: (filters: object) => void,
  handleTextFilter: (text: string) => void,
  handleClearFilters: (event: Event) => void,
  hasActiveFilters: () => boolean
}

Return values

NameTypeDescription
jobsobject[]Array of job objects returned by the current query
totalnumberTotal number of matching jobs (used to calculate pages)
loadingbooleantrue while a fetch is in-flight
totalPagesnumberMath.ceil(total / 5) — total page count at 5 results per page
currentPagenumberCurrently active page number
filtersobjectCurrent values of the three dropdown filters
textToFilterstringCurrent free-text search string
handlePageChangefunctionUpdates currentPage; triggers a new fetch
handleSearchfunctionUpdates dropdown filters and resets to page 1
handleTextFilterfunctionUpdates textToFilter and resets to page 1
handleClearFiltersfunctionResets all filters, text, and page to their empty/initial state
hasActiveFiltersfunctionReturns true if any filter or text value is non-empty

Key implementation details

Filters are initialised from URL search params, making the current search fully restorable from a URL:
import { useState, useEffect } from "react";
import { useSearchParams } from "react-router-dom";

const RESULT_PER_PAGE = 5;

export const useFilters = () => {
  const [searchParams, setSearchParams] = useSearchParams();

  const [filters, setFilters] = useState(() => ({
    technology:      searchParams.get('technology') || '',
    location:        searchParams.get('type')       || '',
    experienceLevel: searchParams.get('level')      || '',
  }));

  const [textToFilter, setTextToFilter] = useState(
    () => searchParams.get('text') || ''
  );

  const [currentPage, setCurrentPage] = useState(() => {
    const page = Number(searchParams.get('page'));
    return Number.isNaN(page) ? page : 1;
  });

  // ... fetch useEffect, URL-sync useEffect, handlers
};
The URL is kept in sync by a dedicated useEffect that calls setSearchParams whenever filters, textToFilter, or currentPage change.

Usage example

import { useFilters } from "../hooks/useFilters";

function SearchPage() {
  const {
    jobs, total, loading, totalPages,
    currentPage, filters, textToFilter,
    handlePageChange, handleSearch,
    handleTextFilter, handleClearFilters, hasActiveFilters
  } = useFilters();

  return (
    <main>
      <SearchFormSection
        onSearch={handleSearch}
        onTextFilter={handleTextFilter}
        handleClearFilters={handleClearFilters}
        filters={filters}
        textToFilter={textToFilter}
        hasActiveFilters={hasActiveFilters()}
      />
      {loading ? <Spinner /> : <JobListings jobs={jobs} />}
      <Pagination
        currentPage={currentPage}
        totalPages={totalPages}
        onPageChange={handlePageChange}
      />
    </main>
  );
}

useSearchForm

useSearchForm manages the interactive behaviour of the SearchFormSection form. It controls the text input’s local state, debounces free-text input to avoid firing a search request on every keystroke, and builds a filters object from FormData when a dropdown value changes.

Signature

useSearchForm(options: object) => {
  searchText: string,
  handleSubmit: (event: Event) => void,
  handleTextChange: (event: Event) => void
}

Parameters

The hook accepts a configuration object with the following keys:
KeyDescription
idTechnologyUnique ID of the technology <select> (from useId())
idLocationUnique ID of the location <select>
idExperienceLevelUnique ID of the experience level <select>
idTextUnique ID of the text <input>
onSearchCallback to invoke with the assembled filters object
onTextFilterCallback to invoke with the current text after debounce
textToFilterExternal text value used to sync the input when filters are cleared

Key behaviour

  • Dropdown changeshandleSubmit fires on the form’s onChange event. If the changed field is the text input (event.target.name === idText), the function returns early; otherwise it reads FormData, assembles the filters object, and calls onSearch.
  • Text inputhandleTextChange updates local searchText immediately (for a responsive input), then sets a 500 ms debounce timeout before calling onTextFilter. Any previous pending timeout is cancelled first (clearTimeout).
  • External clear — a useEffect synchronises searchText with the textToFilter prop, so clearing all filters from the parent also clears the input visually.
import { useEffect, useState, useRef } from "react";

export const useSearchForm = ({
  idTechnology, idLocation, idExperienceLevel,
  idText, onSearch, onTextFilter, textToFilter
}) => {
  const timeoutId  = useRef(null);
  const [searchText, setSearchText] = useState('');

  // Keep input in sync when filters are cleared externally
  useEffect(() => {
    setSearchText(textToFilter);
  }, [textToFilter]);

  const handleSubmit = (event) => {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);

    if (event.target.name === idText) return; // handled by handleTextChange

    const filters = {
      technology:      formData.get(idTechnology),
      location:        formData.get(idLocation),
      experienceLevel: formData.get(idExperienceLevel),
    };
    onSearch(filters);
  };

  const handleTextChange = (event) => {
    const text = event.target.value;
    setSearchText(text);

    // Debounce: wait 500 ms after user stops typing
    if (timeoutId.current) clearTimeout(timeoutId.current);
    timeoutId.current = setTimeout(() => {
      onTextFilter(text);
    }, 500);
  };

  return { searchText, handleSubmit, handleTextChange };
};

useRouter

useRouter is a thin abstraction over React Router’s useNavigate and useLocation hooks. It centralises navigation logic and exposes a named navigateTo helper, making it easy to update navigation behaviour app-wide from a single location.

Signature

useRouter() => {
  currentPath: string,
  navigateTo: (path: string) => void
}

Return values

NameTypeDescription
currentPathstringThe current URL pathname (e.g. /search)
navigateTofunctionCalls React Router’s navigate(path) programmatically

Full source

import { useNavigate, useLocation } from "react-router";

export function useRouter() {
  const navigate = useNavigate();
  const location = useLocation();

  const currentPath = location.pathname;

  function navigateTo(path) {
    navigate(path);
  }

  return {
    currentPath,
    navigateTo,
  };
}

Usage example

useRouter is used in the HomePage to navigate to the search page after a hero form submission, building the target URL with the encodeURIComponent-encoded search term:
import { useRouter } from "../hooks/useRouter";

function HomePage() {
  const { navigateTo } = useRouter();

  const handleSearch = (event) => {
    event.preventDefault();
    const formData   = new FormData(event.target);
    const searchTerm = formData.get('search');

    const url = searchTerm
      ? `/search?text=${encodeURIComponent(searchTerm)}`
      : '/search';

    navigateTo(url);
  };

  return (
    <form role="search" onSubmit={handleSearch}>
      {/* ... */}
    </form>
  );
}

Build docs developers (and LLMs) love