Skip to main content
The Detail Page displays comprehensive information about a specific job posting, including description, requirements, responsibilities, and company information.

Route

  • Path: /jobs/:jobId
  • Component: JobDetailPage
  • File: source/src/pages/Detail.jsx
  • Styles: source/src/pages/Detail.module.css

Purpose

The detail page provides in-depth job information, allowing users to:
  • Read full job descriptions and requirements
  • View markdown-formatted content sections
  • Click tags to filter related jobs
  • Apply for the position
  • Navigate back to search results

URL Parameters

Route Parameters

ParameterDescriptionExample
jobIdUnique job identifier/jobs/123
Extracted via:
const { jobId } = useParams()

Key Features

1. Job Header

Displays primary job information:
  • Job Title: Main heading
  • Company Name: Organization offering the position
  • Location: Work location or remote status
  • Breadcrumb Navigation: Back to search results

2. Interactive Tags

Clickable tags for:
  • Technologies: e.g., React, JavaScript, Python
  • Work Modality: e.g., Remoto, CDMX, Barcelona
  • Experience Level: e.g., Junior, Mid, Senior, Lead
Behavior: Clicking a tag navigates to search page with appropriate filter

3. Markdown Content Sections

Four main content areas rendered from markdown:
  1. Job Description: Overview of the position
  2. Responsibilities: Key duties and tasks
  3. Requirements: Skills and qualifications needed
  4. About the Company: Company background and culture

4. Apply Button

Call-to-action for job application (currently placeholder)

Components Used

import { Link } from '@/components'

<Link href="/search" className={styles.breadcrumbButton}>
  Empleos
</Link>
Purpose: Navigation link with consistent styling

JobDetailSkeleton Component

import { JobDetailSkeleton } from '@/components'

if (loading) {
  return <JobDetailSkeleton />
}
Purpose: Loading placeholder matching detail page layout

JobSection Component (Internal)

Custom internal component for rendering markdown sections:
function JobSection({ title, content }) {
  const html = snarkdown(content)

  return (
    <section className={styles.section}>
      <h2 className={styles.sectionTitle}>{title}</h2>
      <div
        className={`${styles.sectionContent} prose`}
        dangerouslySetInnerHTML={{ __html: html }}
      />
    </section>
  )
}
Props:
  • title: Section heading (e.g., β€œDescripciΓ³n del puesto”)
  • content: Markdown string to be converted to HTML

Hooks and State Management

React Router Hooks

import { useNavigate, useParams } from "react-router-dom"

const { jobId } = useParams()  // Extract job ID from URL
const navigate = useNavigate() // Programmatic navigation

Local State

const [job, setJob] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
State Variables:
  • job: Job data object or null
  • loading: Boolean indicating fetch status
  • error: Error message string or null

Data Fetching

useEffect Hook

Fetches job details on component mount and when jobId changes:
useEffect(() => {
  // Reset scroll position
  window.scrollTo(0, 0)

  fetch(`https://jscamp-api.vercel.app/api/jobs/${jobId}`)
    .then(res => {
      if (!res.ok) {
        throw new Error('Job not found')
      }
      return res.json()
    })
    .then(data => {
      setJob(data)
      setError(null)
    })
    .catch(err => {
      setError(err.message)
      setJob(null)
    })
    .finally(() => {
      setLoading(false)
    })
}, [jobId])
Key Logic:
  1. Scrolls to top (important for navigation from search)
  2. Fetches job data from API
  3. Handles 404 errors for missing jobs
  4. Updates state with data or error
  5. Sets loading to false when complete

API Response Format

{
  "id": "123",
  "titulo": "Senior React Developer",
  "empresa": "TechCorp",
  "ubicacion": "Remoto",
  "data": {
    "technology": ["react", "javascript", "typescript"],
    "modalidad": "remoto",
    "nivel": "senior"
  },
  "content": {
    "description": "## About the Role\n\nWe're looking for...",
    "responsibilities": "- Lead development team\n- Code reviews",
    "requirements": "- 5+ years React\n- Strong TypeScript",
    "about": "## About Us\n\nTechCorp is a leading..."
  }
}

Interactive Tag System

handleTagClick Function

Navigates to search page with appropriate filters:
const handleTagClick = (type, value) => {
  const params = new URLSearchParams()

  // Valid filter values
  const validTechnologies = [
    'javascript', 'python', 'react', 'nodejs', 
    'java', 'csharp', 'c', 'c++', 'ruby', 'php'
  ]
  const validLocations = ['remoto', 'cdmx', 'guadalajara', 'monterrey', 'barcelona']
  const validLevels = ['junior', 'mid', 'senior', 'lead']

  const lowerValue = value.toLowerCase()

  if (type === 'technology') {
    if (validTechnologies.includes(lowerValue)) {
      params.set('technology', lowerValue)
    } else {
      params.set('text', value)
    }
  } else if (type === 'modalidad') {
    // Similar logic for location
  } else if (type === 'nivel') {
    // Similar logic for level
  }

  navigate(`/search?${params.toString()}`)
}
Logic:
  1. Creates new URLSearchParams object
  2. Validates if tag value matches a filter option
  3. If valid: Sets specific filter parameter (e.g., ?technology=react)
  4. If invalid: Falls back to text search (e.g., ?text=React Native)
  5. Navigates to search page with constructed query string

Tag Rendering

{/* Technology tags */}
{job.data.technology.map((tech) => (
  <span
    key={tech}
    className={styles.tag}
    onClick={() => handleTagClick('technology', tech)}
  >
    {tech}
  </span>
))}

{/* Modality tag */}
<span
  className={`${styles.tag} ${styles.tagModalidad}`}
  onClick={() => handleTagClick('modalidad', job.data.modalidad)}
>
  {job.data.modalidad}
</span>

{/* Level tag */}
<span
  className={`${styles.tag} ${styles.tagNivel}`}
  onClick={() => handleTagClick('nivel', job.data.nivel)}
>
  {job.data.nivel}
</span>

Markdown Rendering

snarkdown Library

Converts markdown to HTML:
import snarkdown from "snarkdown"

function JobSection({ title, content }) {
  const html = snarkdown(content)

  return (
    <section className={styles.section}>
      <h2 className={styles.sectionTitle}>{title}</h2>
      <div
        className={`${styles.sectionContent} prose`}
        dangerouslySetInnerHTML={{ __html: html }}
      />
    </section>
  )
}
Classes:
  • prose: Typography utility class for readable content
  • styles.sectionContent: Custom styling from CSS module

Section Usage

<JobSection 
  title="DescripciΓ³n del puesto" 
  content={job.content.description} 
/>
<JobSection 
  title="Responsabilidades" 
  content={job.content.responsibilities} 
/>
<JobSection 
  title="Requisitios" 
  content={job.content.requirements} 
/>
<JobSection 
  title="Acerca de la empresa" 
  content={job.content.about} 
/>

Error Handling

Error State UI

if (error || !job) {
  return (
    <div style={{ maxWidth: '1280px', margin: '0 auto', padding: '0 1rem' }}>
      <div className={styles.error}>
        <h2 className={styles.errorTitle}>
          Oferta no encontrada
        </h2>
        <button
          onClick={() => navigate('/')}
          className={styles.errorButton}
        >
          Volver al inicio
        </button>
      </div>
    </div>
  )
}
Error Scenarios:
  • Job ID doesn’t exist (404)
  • Network error during fetch
  • Invalid job data format

User Flows

View Job Detail Flow

  1. User clicks job card on search page
  2. Browser navigates to /jobs/{jobId}
  3. Component mounts, extracts jobId from URL
  4. Page scrolls to top automatically
  5. Skeleton loader displays immediately
  6. API fetch retrieves job data
  7. Content renders with all sections
  8. User can interact with tags and apply button

Tag Click Flow

  1. User clicks technology tag (e.g., β€œReact”)
  2. handleTagClick validates tag value
  3. URLSearchParams constructed with filter
  4. Navigate to /search?technology=react
  5. Search page displays filtered results
  6. User can return via browser back button

Error Recovery Flow

  1. Job fetch fails (404 or network error)
  2. Error state is set
  3. Error UI displays β€œOferta no encontrada”
  4. User clicks β€œVolver al inicio” button
  5. Navigate to home page

Code Example

Complete Detail Page Implementation

import { useNavigate, useParams } from "react-router-dom"
import { useEffect, useState } from "react"
import snarkdown from "snarkdown"
import { Link, JobDetailSkeleton } from "@/components"
import styles from './Detail.module.css'

function JobSection({ title, content }) {
  const html = snarkdown(content)

  return (
    <section className={styles.section}>
      <h2 className={styles.sectionTitle}>{title}</h2>
      <div
        className={`${styles.sectionContent} prose`}
        dangerouslySetInnerHTML={{ __html: html }}
      />
    </section>
  )
}

export function JobDetailPage() {
  const { jobId } = useParams()
  const navigate = useNavigate()

  const [job, setJob] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  const handleTagClick = (type, value) => {
    const params = new URLSearchParams()
    const validTechnologies = [
      'javascript', 'python', 'react', 'nodejs', 
      'java', 'csharp', 'c', 'c++', 'ruby', 'php'
    ]
    const validLocations = [
      'remoto', 'cdmx', 'guadalajara', 'monterrey', 'barcelona'
    ]
    const validLevels = ['junior', 'mid', 'senior', 'lead']

    const lowerValue = value.toLowerCase()

    if (type === 'technology') {
      if (validTechnologies.includes(lowerValue)) {
        params.set('technology', lowerValue)
      } else {
        params.set('text', value)
      }
    } else if (type === 'modalidad') {
      if (validLocations.includes(lowerValue)) {
        params.set('type', lowerValue)
      } else {
        params.set('text', value)
      }
    } else if (type === 'nivel') {
      if (validLevels.includes(lowerValue)) {
        params.set('level', lowerValue)
      } else {
        params.set('text', value)
      }
    }

    navigate(`/search?${params.toString()}`)
  }

  useEffect(() => {
    window.scrollTo(0, 0)

    fetch(`https://jscamp-api.vercel.app/api/jobs/${jobId}`)
      .then(res => {
        if (!res.ok) throw new Error('Job not found')
        return res.json()
      })
      .then(data => {
        setJob(data)
        setError(null)
      })
      .catch(err => {
        setError(err.message)
        setJob(null)
      })
      .finally(() => {
        setLoading(false)
      })
  }, [jobId])

  if (loading) return <JobDetailSkeleton />

  if (error || !job) {
    return (
      <div style={{ maxWidth: '1280px', margin: '0 auto', padding: '0 1rem' }}>
        <div className={styles.error}>
          <h2 className={styles.errorTitle}>Oferta no encontrada</h2>
          <button
            onClick={() => navigate('/')}
            className={styles.errorButton}
          >
            Volver al inicio
          </button>
        </div>
      </div>
    )
  }

  return (
    <div style={{ maxWidth: '1280px', margin: '0 auto', padding: '0 1rem' }}>
      <div className={styles.container}>
        <nav className={styles.breadcrumb}>
          <Link href="/search" className={styles.breadcrumbButton}>
            Empleos
          </Link>
          <span className={styles.breadcrumbSeparator}>/</span>
          <span className={styles.breadcrumbCurrent}>{job.titulo}</span>
        </nav>
      </div>

      <header className={styles.header}>
        <h1 className={styles.title}>{job.titulo}</h1>
        <p className={styles.meta}>
          {job.empresa} Β· {job.ubicacion}
        </p>
      </header>

      <div className={styles.tags}>
        {job.data.technology.map((tech) => (
          <span
            key={tech}
            className={styles.tag}
            onClick={() => handleTagClick('technology', tech)}
          >
            {tech}
          </span>
        ))}
        <span
          className={`${styles.tag} ${styles.tagModalidad}`}
          onClick={() => handleTagClick('modalidad', job.data.modalidad)}
        >
          {job.data.modalidad}
        </span>
        <span
          className={`${styles.tag} ${styles.tagNivel}`}
          onClick={() => handleTagClick('nivel', job.data.nivel)}
        >
          {job.data.nivel}
        </span>
      </div>

      <button className={styles.applyButton}>
        πŸš€ Aplicar ahora
      </button>

      <JobSection 
        title="DescripciΓ³n del puesto" 
        content={job.content.description} 
      />
      <JobSection 
        title="Responsabilidades" 
        content={job.content.responsibilities} 
      />
      <JobSection 
        title="Requisitios" 
        content={job.content.requirements} 
      />
      <JobSection 
        title="Acerca de la empresa" 
        content={job.content.about} 
      />
    </div>
  )
}

export default JobDetailPage

Structure

JobDetailPage
β”œβ”€β”€ Container (max-width: 1280px)
β”‚   β”œβ”€β”€ Breadcrumb Navigation
β”‚   β”‚   β”œβ”€β”€ Link: "Empleos" β†’ /search
β”‚   β”‚   β”œβ”€β”€ Separator: "/"
β”‚   β”‚   └── Current: Job Title
β”‚   β”‚
β”‚   β”œβ”€β”€ Header
β”‚   β”‚   β”œβ”€β”€ Title (h1)
β”‚   β”‚   └── Meta (company Β· location)
β”‚   β”‚
β”‚   β”œβ”€β”€ Tags Section
β”‚   β”‚   β”œβ”€β”€ Technology Tags (clickable)
β”‚   β”‚   β”œβ”€β”€ Modality Tag (clickable)
β”‚   β”‚   └── Level Tag (clickable)
β”‚   β”‚
β”‚   β”œβ”€β”€ Apply Button
β”‚   β”‚
β”‚   └── Content Sections
β”‚       β”œβ”€β”€ JobSection: Description
β”‚       β”œβ”€β”€ JobSection: Responsibilities
β”‚       β”œβ”€β”€ JobSection: Requirements
β”‚       └── JobSection: About Company

Styling

The detail page uses CSS Modules for scoped styling: Import:
import styles from './Detail.module.css'
Key Classes:
  • styles.container: Main container wrapper
  • styles.breadcrumb: Breadcrumb navigation
  • styles.header: Job header section
  • styles.title: Job title (h1)
  • styles.meta: Company and location metadata
  • styles.tags: Tag container
  • styles.tag: Individual tag
  • styles.tagModalidad: Modality-specific tag styling
  • styles.tagNivel: Level-specific tag styling
  • styles.applyButton: Apply CTA button
  • styles.section: Content section wrapper
  • styles.sectionTitle: Section heading (h2)
  • styles.sectionContent: Markdown content area
  • styles.error: Error state container
  • styles.errorTitle: Error heading
  • styles.errorButton: Return home button

Integration Points

From Search Page

  • Receives jobId via route parameter /jobs/{id}
  • User arrives via job card click
  • Can return via breadcrumb or browser back button

To Search Page

  • Breadcrumb link navigates to /search
  • Tag clicks navigate with filters (e.g., /search?technology=react)
  • Error state β€œback home” button navigates to /

Performance Considerations

  • Scroll Reset: Automatically scrolls to top on navigation
  • Skeleton Loading: Instant visual feedback while fetching
  • Error Boundaries: Graceful error handling with recovery option
  • Dependency Array: useEffect only re-runs when jobId changes

Accessibility

  • Semantic HTML: Proper use of <header>, <nav>, <main>, <section>
  • Breadcrumb Navigation: Clear navigation hierarchy
  • Interactive Elements: All clickable elements use proper elements
  • Prose Class: Enhances readability of markdown content
  • Error Messaging: Clear feedback when job not found

Security Considerations

dangerouslySetInnerHTML

<div
  dangerouslySetInnerHTML={{ __html: html }}
/>
Risk: Potential XSS if content contains malicious scripts Mitigation:
  • Content is fetched from trusted API
  • Consider using a sanitization library like DOMPurify for production
  • Markdown conversion (snarkdown) provides some protection

External Dependencies

  • snarkdown: Lightweight markdown parser
  • react-router-dom: useParams, useNavigate for routing

Build docs developers (and LLMs) love