Careers across agencies
Your Neighborhood
Based on your location: In your neighborhood:
Not quite right?
.node-unpublished { background-color: transparent; }
#zone-content,
#zone-content .grid-24{ width: 100%!important;
}
.not-front .region-content-inner{
padding:0;
}
#section-header,
h1.portal-title,
h1,
.rs_addtools, .rsbtn,
.pane-add-this,
#readspeaker_button1,
#page-title,
#page-title, .title,
.section-footer,
#zone-postscript-wrapper,
.tabs.primary.clearfix{ display: none!important;
}
#root h1{
Display:block!important;
color:#fff!important;
Background:0!important;
border:0!important;
}
#root h1:after, #root h2:after, #root h3:after, #root h4:after, #root h5:after, #root h6:after{
content:''!important;
border:0!important;
background:0!important;
}
h2{ background:0!important; border:0!important;
}
.sort-by-button{ border:0!important;
}
input[type="text"]{ padding: 0 105px 0 40px; height: 40px; border-radius: 25px; border: 1px solid #9ca3af;
} /* Import Neue Haas Grotesk Display Pro */ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap'); @import url("https://use.typekit.net/xrm0bpn.css"); /* Apply font family globally */ h1, h2, h3, h4, h5, h6, button, .tagline { font-family: "neue-haas-grotesk-display", sans-serif !important; font-weight: 700; } button{ font-weight: 500; } label{ font-weight: 500; } .tagline{ font-weight: 400; } body, div, span, button, label { font-family: 'Open Sans', sans-serif!important; } .search-icon { color: #D22517 !important; }
.region-content-inner a.text-white{
Color:#fff!important;
}
button{ text-shadow: none;
}
.close-button{border:none;}
.region-content-inner .job-detail a.text-white:hover { Color: #0038b1 !important;
}
--> const { useState, useEffect, useMemo } = React; const Search = (props) => React.createElement('svg', { ...props, width: props.size || 24, height: props.size || 24, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "3", strokeLinecap: "round", strokeLinejoin: "round" }, React.createElement('circle', { cx: "11", cy: "11", r: "8" }), React.createElement('path', { d: "m21 21-4.35-4.35" })); const ChevronLeft = (props) => React.createElement('svg', { ...props, width: props.size || 24, height: props.size || 24, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }, React.createElement('polyline', { points: "15 18 9 12 15 6" })); const ChevronRight = (props) => React.createElement('svg', { ...props, width: props.size || 24, height: props.size || 24, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }, React.createElement('polyline', { points: "9 18 15 12 9 6" })); const ChevronsLeft = (props) => React.createElement('svg', { ...props, width: props.size || 24, height: props.size || 24, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }, React.createElement('polyline', { points: "11 17 6 12 11 7" }), React.createElement('polyline', { points: "18 17 13 12 18 7" })); const ChevronsRight = (props) => React.createElement('svg', { ...props, width: props.size || 24, height: props.size || 24, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }, React.createElement('polyline', { points: "13 17 18 12 13 7" }), React.createElement('polyline', { points: "6 17 11 12 6 7" })); const ChevronUp = (props) => React.createElement('svg', { ...props, width: props.size || 24, height: props.size || 24, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }, React.createElement('polyline', { points: "18 15 12 9 6 15" })); const ChevronDown = (props) => React.createElement('svg', { ...props, width: props.size || 24, height: props.size || 24, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }, React.createElement('polyline', { points: "6 9 12 15 18 9" })); const X = (props) => React.createElement('svg', { ...props, width: props.size || 24, height: props.size || 24, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }, React.createElement('line', { x1: "18", y1: "6", x2: "6", y2: "18" }), React.createElement('line', { x1: "6", y1: "6", x2: "18", y2: "18" })); // Helper function to sanitize HTML for React const sanitizeHtmlForReact = (html) => { if (!html) return ''; return html .replace(/class=/g, 'className=') .replace(/class\s*=/g, 'className=') .replace(/class\s*=\s*/g, 'className=') .replace(/class\s*=\s*"/g, 'className="') .replace(/class\s*=\s*'/g, "className='"); };
const staticCategories = [ { name: "Administrative & Executive Support", description: "Clerical, executive assistance, and administrative services", count: 0, }, { name: "Arts, Culture & Recreation", description: "Program development, community engagement, and heritage and cultural preservation", count: 0, }, { name: "Business, Finance & Budget Management", description: "Accounting, budgeting, auditing, and purchasing", count: 0, }, { name: "Communications, Policy & Public Affairs", description: "Media relations, strategic communications, legislative affairs, and public policy", count: 0, }, { name: "Community & Social Services", description: "Social work, housing assistance, family support, and community outreach", count: 0, }, { name: "Education & Instruction", description: "Teaching, curriculum development, and child/youth programs", count: 0, }, { name: "Engineering, Architecture & Planning", description: "Infrastructure design, urban planning, and project management", count: 0, }, { name: "Environmental, Public Health & Safety", description: "Environmental protection, health services, safety inspection, and emergency management", count: 0, }, { name: "Facilities, Maintenance & Operations", description: "Building operations, trades (electrical, HVAC, plumbing), and grounds keeping", count: 0, }, { name: "Human Resources & Labor Relations", description: "Recruitment, employee services, and labor policy", count: 0, }, { name: "Information Technology & Systems", description: "Tech support, cybersecurity, software development, and data analytics", count: 0, }, { name: "Legal, Regulatory & Compliance", description: "Legal counsel, enforcement, and regulatory review", count: 0, }, { name: "Public Safety, Law Enforcement & Emergency Services", description: "Police, fire, corrections, and emergency response", count: 0, }, { name: "Research, Data & Evaluation", description: "Policy and program research, data collection and analysis, monitoring and evaluation", count: 0, }, { name: "Transportation & Public Works", description: "Roads, transit systems, fleet services, and Infrastructure maintenance", count: 0, },
];
const staticCopyOnJobDetails = `Need Guidance with the Application Process?
{isHomepage ? ( // Explore Careers Across the District Your growing guide for DC government jobs—with new postings added regularly {/* Search Bar */} {} {searchTerm && ( )} { setIsHomepage(false); setSelectedCategory(""); }} > Search searchTerm.length >= 2 && setShowAutocomplete(true) } onBlur={() => setTimeout(() => setShowAutocomplete(false), 200) } className="w-full text-gray-800 pl-10 pr-4 py-2 border border-gray-400 rounded-full focus:ring-2 focus:ring-blue-500 focus:border-transparent" /> {/* Autocomplete Dropdown */} {showAutocomplete && autocompleteSuggestions && autocompleteSuggestions.length > 0 && ( {autocompleteSuggestions.map((suggestion, index) => ( handleAutocompleteSelect(suggestion)} className={`px-4 py-2 cursor-pointer hover:bg-gray-100 text-gray-800 text-left ${ index === autocompleteIndex ? "bg-blue-50 border-l-4 border-blue-500" : "" }`} > {suggestion .split(new RegExp(`(${searchTerm})`, "gi")) .map((part, i) => ( {part} ))} ))} )} Browse jobs by selecting a category { { setIsHomepage(false); clearFilters(); // Set URL param to show all jobs updateURL('all'); } }} > View all jobs {globalCategories && globalCategories.length > 0 ? ( globalCategories.map((cat, index) => { return ( { // setCurrentJobFunction(job.JobPostingID); if (cat.count === 0) { return } setSelectedCategory(cat.name); setSearchTerm(""); window.scrollTo({ top: 0, left: 0, behavior: "smooth", }); setIsHomepage(false); //setCurrentJob(currentItems[0]); console.log("currentItems", currentItems); console.log("currentJob", currentJob); }} onKeyDown={(e) => { if (e.key === "Enter" && cat.count !== 0) { e.preventDefault(); setSelectedCategory(cat.name); setSearchTerm(""); window.scrollTo({ top: 0, left: 0, behavior: "smooth", }); setIsHomepage(false); } }} className={`category-card bg--50 rounded-lg p-8 relative ${cat.count === 0 ? 'bg-gray-300 ' : 'hover:border-blue-800 hover:bg-gray-100 hover:cursor-pointer transition-colors border border-solid border-blue-900'}`} >
) : ( <> {/* */} {/* Search and Filters container*/} { { setIsHomepage(true); setSearchTerm(""); setCurrentPage(1); // Reset to first page to clear currentItems setCurrentJob(null); clearJobFromURL(); // Remove job parameter from URL } }} className="home-link cursor text-[16px] md:text-md text-blue-900 font-semibold hover:cursor-pointer hover:underline hover:bg-blue-100" > Home {" "} / Job search Your growing guide for DC government jobs {/* Search Bar */} {/* Search Bar */} {searchTerm && ( )} searchTerm.length >= 2 && setShowAutocomplete(true) } onBlur={() => setTimeout(() => setShowAutocomplete(false), 200) } className="w-full pl-10 pr-4 py-2 border border-gray-400 rounded-full focus:ring-2 focus:ring-blue-500 focus:border-transparent" /> {/* Autocomplete Dropdown */} {showAutocomplete && autocompleteSuggestions && autocompleteSuggestions.length > 0 && ( {autocompleteSuggestions.map((suggestion, index) => ( handleAutocompleteSelect(suggestion)} className={`px-4 py-2 cursor-pointer hover:bg-gray-100 ${ index === autocompleteIndex ? "bg-blue-50 border-l-4 border-blue-500" : "" }`} > {suggestion .split(new RegExp(`(${searchTerm})`, "gi")) .map((part, i) => ( {part} ))} ))} )} {/* Filters */} {/* Category Filter */} Filters Category
{/* Custom Category Dropdown
*/} setCategoryDropdownOpen((open) => !open)} aria-haspopup="listbox" aria-expanded={categoryDropdownOpen} > {selectedCategory ? selectedCategory : "Category"} {categoryDropdownOpen && ( {categories && categories.map((category) => ( { setSelectedCategory(category); setCategoryDropdownOpen(false); }} > {category} ))} { setSelectedCategory(""); setCategoryDropdownOpen(false); }} > Clear category )} {/* Agency Filter (dependent on category) */} Agency {/* Custom Agency Dropdown */} { if (agencies && agencies.length > 0) setCategoryDropdownOpen(false); // close category dropdown if open setAgencyDropdownOpen && setAgencyDropdownOpen((open) => !open); }} onMouseDown={e => e.preventDefault()} // Prevents focus loss tabIndex={0} > {selectedAgency || "Agencies"} {/* Dropdown options */} {typeof agencyDropdownOpen !== "undefined" && agencyDropdownOpen && ( { setSelectedAgency(""); setAgencyDropdownOpen && setAgencyDropdownOpen(false); }} > All agencies {agencies && agencies.map((agency) => ( { setSelectedAgency(agency); setAgencyDropdownOpen && setAgencyDropdownOpen(false); }} > {agency} ))} )} {/* Salary Range Filter (dependent on category and agency) */} Salary Range {/* Custom Salary Range Dropdown */} setSalaryDropdownOpen && setSalaryDropdownOpen((open) => !open)} aria-haspopup="listbox" aria-expanded={typeof salaryDropdownOpen !== "undefined" ? salaryDropdownOpen : false} > {selectedSalaryRange ? selectedSalaryRange : "Salary"} {salaryDropdownOpen && ( { setSelectedSalaryRange(""); setSalaryDropdownOpen && setSalaryDropdownOpen(false); }} > All salaries {availableSalaryRanges && availableSalaryRanges.map((range) => ( { setSelectedSalaryRange(range.label); setSalaryDropdownOpen && setSalaryDropdownOpen(false); }} > {range.label} ))} )} {/* Active Filters Display */} {hasActiveFilters && ( Clear all )} {/* Results Count and Items Per Page */} Showing {filteredItems?.length === 0 ? 0 : startIndex + 1}- {Math.min(endIndex, filteredItems?.length || 0)} of{" "} {filteredItems?.length || 0} jobs {/* Sort Dropdown */} Sort by: setSortDropdownOpen((open) => !open)} aria-haspopup="listbox" aria-expanded={sortDropdownOpen} > {selectedOption ? selectedOption.label : "Sort by"} {sortDropdownOpen && ( {sortOptions.map((opt) => ( { setSortField(opt.value); setSortDropdownOpen(false); }} > {opt.label} ))} { setSortField("PostingTitle"); setSortDropdownOpen(false); }} > Clear sort )} Jobs per page: { setItemsPerPage(Number(e.target.value)); setCurrentPage(1); }} className="min-w-[45px] px-2 py-1 border border-gray-400 rounded text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent" > 10 20 50 Job count may include overlaps across categories {/* Results Count */} {/* Showing {filteredItems.length} of {items.length} Jobs */} {/* Details section */} {/* Items List */} {/* */} {!currentItems || currentItems.length === 0 ? (
]*>/gi, ""); return ( ); })()} {/* if the job is from people soft, show the static copy on job details */} {currentItems[0]?.ATS?.includes("PS") ? ( ) : ( )} )} ) : (
]*>/gi, ""); return ( ); })()} {/* if the job is from people soft, show the static copy on job details */} {currentJob?.ATS?.includes("PS") ? ( ) : ( )} { if (currentJob?.JobPostingID) { setCurrentJobFunction(currentJob.JobPostingID); } }} id={currentJob?.JobPostingID || ''} > {currentJob?.ATS?.includes("PS") ? "Apply on careers.dc.gov" : "Apply on DCPS site"} )} {/* Modal */} {isModalOpen && ( {/* Modal Header */}
We're Here to Support You! Applying for a position can feel overwhelming, and we’re here to help make it a little easier. If you need guidance during the application process, please don’t hesitate to contact the DC Department of Human Resources at 202-442-9700 or dchr.erecruit@dc.govIf you have questions about the status of a specific application, we encourage you to connect directly with the hiring agency; their HR staff is best positioned to provide timely updates and answers regarding individual scenarios.
`; const staticCopyNoPeopleSoft = `Have Questions About An Application?If you have questions about the status of a specific application, we encourage you to connect directly with the hiring agency; their HR staff is best positioned to provide timely updates and answers regarding individual scenarios.
` // Main App Component const App = () => { const [items, setItems] = useState([]); const [searchTerm, setSearchTerm] = useState(""); const [selectedCategory, setSelectedCategory] = useState(""); const [selectedAgency, setSelectedAgency] = useState(""); // const [selectedStatus, setSelectedStatus] = useState(""); const [showAutocomplete, setShowAutocomplete] = useState(false); const [autocompleteIndex, setAutocompleteIndex] = useState(-1); const [currentJob, setCurrentJob] = useState(null); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(10); const [selectedSalaryRange, setSelectedSalaryRange] = useState(""); // Salary range filter const [isModalOpen, setIsModalOpen] = useState(false); const [isHomepage, setIsHomepage] = useState(true); const [showBanner, setShowBanner] = useState(true); const [sortOrder, setSortOrder] = useState("asc"); // Sort order: "asc" or "desc" const [globalCategories, setGlobalCategories] = useState(staticCategories); // Global categories state const [sortDropdownOpen, setSortDropdownOpen] = useState(false); const [categoryDropdownOpen, setCategoryDropdownOpen] = useState(false); const [agencyDropdownOpen, setAgencyDropdownOpen] = useState(false); const [salaryDropdownOpen, setSalaryDropdownOpen] = useState(false); const sortOptions = [ { value: "PostingTitle", label: "Job title" }, { value: "JobPostingDate", label: "Posting date" }, { value: "DaystoClose", label: "Closing date" }, ]; // Local state for dropdown open/close // Close dropdown on outside click useEffect(() => { if (!sortDropdownOpen) return; const handleClick = (e) => { // Only close if click is outside the dropdown if (!e.target.closest(".custom-sort-dropdown")) { setSortDropdownOpen(false); } }; document.addEventListener("mousedown", handleClick); return () => document.removeEventListener("mousedown", handleClick); }, [sortDropdownOpen]); // useEffect to run countCategoryOccurrences when items are loaded useEffect(() => { if (items.length > 0) { const result = countCategoryOccurrences(globalCategories, items, setGlobalCategories); console.log('Category counts from useEffect:', result); } }, [items]); // Run when items change // Check local storage for banner visibility on component mount useEffect(() => { const bannerHidden = localStorage.getItem('dcCareersBannerHidden'); if (bannerHidden === 'true') { setShowBanner(false); } }, []); // Override browser back button behavior useEffect(() => { const handlePopState = (event) => { // When user hits back button and we're not on homepage, go to homepage if (!isHomepage) { setIsHomepage(true); setSearchTerm(""); setSelectedCategory(""); setSelectedAgency(""); setSelectedSalaryRange(""); setCurrentPage(1); setCurrentJob(null); // Prevent the default back navigation event.preventDefault(); // Push a new state to maintain the current URL window.history.pushState(null, '', window.location.href); } }; // Add event listener for popstate (back/forward button) window.addEventListener('popstate', handlePopState); // Push initial state when not on homepage if (!isHomepage) { window.history.pushState(null, '', window.location.href); } // Cleanup return () => { window.removeEventListener('popstate', handlePopState); }; }, [isHomepage]); // Function to close banner and save to local storage const closeBanner = () => { setShowBanner(false); localStorage.setItem('dcCareersBannerHidden', 'true'); }; const handleItemClick = (item) => { setIsModalOpen(true); }; const closeModal = () => { setIsModalOpen(false); setCurrentJob(null); // Clear job ID from URL when modal is closed clearJobFromURL(); }; const handleModalBackdropClick = (e) => { if (e.target === e.currentTarget) { closeModal(); } }; // Find label for current sortField const selectedOption = sortOptions.find(opt => opt.value === sortField); /** - Handle escape key press to close modal */ // Function to count category occurrences in jobData and update globalCategories using setGlobalCategories const countCategoryOccurrences = (globalCategories, jobData, setGlobalCategories) => { // Count occurrences of each category name in jobData const counts = {}; jobData.forEach(job => { if (Array.isArray(job.Category)) { job.Category.forEach(catName => { counts[catName] = (counts[catName] || 0) + 1; }); } }); // Create a new categories array with updated counts const updatedCategories = globalCategories.map(cat => ({ ...cat, count: counts[cat.name] || 0 })); setGlobalCategories(updatedCategories); console.log("updatedCategories", updatedCategories); }; useEffect(() => { const handleEscapeKey = (e) => { if (e.key === "Escape" && isModalOpen) { closeModal(); } }; document.addEventListener("keydown", handleEscapeKey); return () => { document.removeEventListener("keydown", handleEscapeKey); }; }, [isModalOpen]); // API call to fetch jobs from DC Government useEffect(() => { const fetchJobs = async () => { try { const response = await fetch('https://datagate.dc.gov/dc/jobs/CareerAggregate'); //https://datagate.dc.gov/dc/jobs/CareerAggregate if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); // Extract the processResponse array from the API response if (data.processResponse && Array.isArray(data.processResponse)) { setItems(data.processResponse); console.log('Jobs fetched successfully:', data.processResponse.length, 'jobs'); } else { console.error('Invalid data structure received from API'); } } catch (error) { console.error('Error fetching jobs:', error); // Keep the existing jobData as fallback setItems(jobData); } }; fetchJobs(); }, []); // Empty dependency array - runs only once on mount // Handle URL parameters when items are loaded useEffect(() => { if (items && items.length > 0) { const jobIdFromURL = getJobIdFromURL(); if (jobIdFromURL) { // Special case: show all jobs if (jobIdFromURL === 'all') { setIsHomepage(false); setCurrentJob(null); if (typeof clearFilters === 'function') { clearFilters(); } return; } // Find the job by ID and open it const job = items.find(item => item.JobPostingID == jobIdFromURL); if (job) { setCurrentJob(job); setIsModalOpen(true); setIsHomepage(false); // Set to job search page when loading job from URL } } } }, [items]); // Run when items are loaded // Handle browser back/forward navigation useEffect(() => { const handlePopState = (event) => { const jobIdFromURL = getJobIdFromURL(); if (jobIdFromURL && items && items.length > 0) { if (jobIdFromURL === 'all') { setIsHomepage(false); setIsModalOpen(false); setCurrentJob(null); if (typeof clearFilters === 'function') { clearFilters(); } return; } // Find the job by ID and open it const job = items.find(item => item.JobPostingID == jobIdFromURL); if (job) { setCurrentJob(job); setIsModalOpen(true); setIsHomepage(false); // Set to job search page when navigating to job URL } } else { // No job ID in URL, close modal if open and go to homepage if (isModalOpen) { setIsModalOpen(false); setCurrentJob(null); } setIsHomepage(true); // Go back to homepage when no job in URL } }; window.addEventListener('popstate', handlePopState); return () => { window.removeEventListener('popstate', handlePopState); }; }, [items, isModalOpen]); // URL utility functions const updateURL = (jobId) => { const url = new URL(window.location); if (jobId) { url.searchParams.set('job', jobId); } else { url.searchParams.delete('job'); } window.history.pushState({ jobId }, '', url); }; const getJobIdFromURL = () => { const urlParams = new URLSearchParams(window.location.search); return urlParams.get('job'); }; const clearJobFromURL = () => { const url = new URL(window.location); url.searchParams.delete('job'); window.history.pushState({}, '', url); }; function setCurrentJobFunction(jobId) { // console.log("jobs", jobs) //console.log(e); if (!items || !Array.isArray(items)) return; const currentJob = items.filter((job) => job.JobPostingID == jobId); console.log("currentJob2", currentJob); if (currentJob && currentJob.length > 0) { setCurrentJob(currentJob[0]); setIsModalOpen(true); // Update URL with job ID updateURL(jobId); } } // Get unique categories const categories = useMemo(() => { if (!items || !Array.isArray(items)) return []; const allCategories = [ ...new Set(items.map((item) => item.Category)), ].sort(); // let uniquecats = result.flat(Infinity); const result2 = allCategories.flat(); console.log("result2", result2); // console.log("result", result); const uniquecats = result2.filter((value, index, self) => { return self.indexOf(value) === index; }); console.log("uniquecats", uniquecats.sort()); console.log( "test", [...new Set(items.flat().map((item) => item.Category))].sort() ); return uniquecats.sort(); // return [...new Set(items.flat().map((item) => item.Category))].sort(); }, [items]); // Get agencies based on selected category const agencies = useMemo(() => { if (!items || !Array.isArray(items)) return []; if (!selectedCategory) { return [...new Set(items.map((item) => item.Agency))].sort(); } return [ ...new Set( items .filter((item) => item.Category && item.Category.includes(selectedCategory)) .map((item) => item.Agency) ), ].sort(); }, [items, selectedCategory]); // Get unique statuses const statuses = useMemo(() => { if (!items || !Array.isArray(items)) return []; return [...new Set(items.map((item) => item.status))].sort(); }, [items]); // Helper function to parse salary string and return min/max values const parseSalaryString = (salaryString) => { if (!salaryString) return { min: 0, max: 0 }; const salary = salaryString.trim(); console.log('Parsing salary string:', salary); // Handle hourly rates (e.g., "$25/hr") // if (salary.includes('/hr')) { // const hourlyRate = parseFloat(salary.replace(/[^0-9.]/g, '')); // const annualRate = Math.round(hourlyRate * 8 * 52); // Convert to annual and round // console.log('Hourly rate parsed:', { hourlyRate, annualRate }); // return { min: annualRate, max: annualRate }; // } // Handle hourly range format (e.g., "$20 - $25/hr" or "$20–$25/hr") if ((salary.includes('-') || salary.includes('–')) && salary.includes('/hr')) { // Split on both regular dash and en dash const parts = salary.split(/[-–]/).map(part => part.trim()); const minStr = parts[0].replace(/[^0-9.]/g, ''); // Try to get max part by removing '/hr' in case it's part of the string const maxPartRaw = parts[1] ? parts[1].replace(/\/hr/i, '').trim() : ''; const maxStr = maxPartRaw.replace(/[^0-9.]/g, ''); const min = Math.round(parseFloat(minStr) || 0); const max = Math.round(parseFloat(maxStr) || 0); console.log('Hourly range parsed:', { original: salary, minStr, maxStr, min, max }); // For parsing, let's follow the display expectation and return as hourly rates return { min, max, isHourly: true }; } // Handle range format (e.g., "$50,000 - $75,000" or "$25,662 – $32,824") if (salary.includes('-') || salary.includes('–')) { // Split on both regular dash and en dash const parts = salary.split(/[-–]/).map(part => part.trim()); const minStr = parts[0].replace(/[^0-9.]/g, ''); const maxStr = parts[1].replace(/[^0-9.]/g, ''); const min = Math.round(parseFloat(minStr) || 0); const max = Math.round(parseFloat(maxStr) || 0); console.log('Range parsed:', { original: salary, minStr, maxStr, min, max }); return { min, max }; } // Handle single value (e.g., "$50,000") const value = Math.round(parseFloat(salary.replace(/[^0-9.]/g, '')) || 0); console.log('Single value parsed:', { original: salary, value }); return { min: value, max: value }; }; // Helper function to format salary string with rounded values for display const formatSalaryForDisplay = (salaryString) => { if (!salaryString) return 'Salary not specified'; const salary = salaryString.trim(); // Handle hourly range (e.g., "$20 - $25/hr" or "$20–$25/hr") BEFORE single hourly value if ((salary.includes('-') || salary.includes('–')) && salary.includes('/hr')) { const parts = salary.split(/[-–]/).map(part => part.trim()); const minStr = parts[0].replace(/[^0-9.]/g, ''); const maxPartRaw = parts[1] ? parts[1].replace(/\/hr/i, '').trim() : ''; const maxStr = maxPartRaw.replace(/[^0-9.]/g, ''); const roundedMin = Math.round(parseFloat(minStr) || 0); const roundedMax = Math.round(parseFloat(maxStr) || 0); return `$${roundedMin.toLocaleString()} - $${roundedMax.toLocaleString()}/hr`; } // Handle single hourly rate (e.g., "$25/hr") if (salary.includes('/hr')) { const hourlyRate = parseFloat(salary.replace(/[^0-9.]/g, '')); const roundedRate = Math.round(hourlyRate); return `$${roundedRate.toLocaleString()}/hr`; } // Handle range format (e.g., "$50,000 - $75,000" or "$25,662 – $32,824") if (salary.includes('-') || salary.includes('–')) { // Split on both regular dash and en dash const parts = salary.split(/[-–]/).map(part => part.trim()); const minStr = parts[0].replace(/[^0-9.]/g, ''); const maxStr = parts[1].replace(/[^0-9.]/g, ''); const roundedMin = Math.round(parseFloat(minStr) || 0); const roundedMax = Math.round(parseFloat(maxStr) || 0); return `$${roundedMin.toLocaleString()} – $${roundedMax.toLocaleString()}`; } // Handle single value (e.g., "$50,000") const value = Math.round(parseFloat(salary.replace(/[^0-9.]/g, '')) || 0); return `$${value.toLocaleString()}`; }; // Generate salary range options based on the data // Creates predefined ranges that encompass the salary data const salaryRanges = useMemo(() => { const ranges = [ { label: "$40,000+", min: 40000, max: Infinity }, { label: "$60,000+", min: 60000, max: Infinity }, { label: "$80,000+", min: 80000, max: Infinity }, { label: "$100,000+", min: 100000, max: Infinity }, { label: "$120,000+", min: 120000, max: Infinity }, { label: "$140,000+", min: 140000, max: Infinity }, ]; return ranges; }, []); // Get salary ranges that are available based on selected category and agency // This creates dependent filtering where salary ranges are filtered by other selections const availableSalaryRanges = useMemo(() => { // Start with all items, then filter by category and agency if selected let filteredItems = items; if (selectedCategory) { filteredItems = filteredItems.filter((item) => item.Category && item.Category.includes(selectedCategory) ); } if (selectedAgency) { filteredItems = filteredItems.filter( (item) => item.Agency === selectedAgency ); } // Find which salary ranges have matching items return salaryRanges.filter((range) => { return filteredItems.some((item) => { // Parse the salary string to get min/max values const salaryRange = parseSalaryString(item.Salary); // Check if item's minimum salary is greater than or equal to the range minimum // This works for the new "X+" format where we want jobs that pay at least X const matches = salaryRange.min >= range.min; // Debug logging for available salary ranges if (item.Salary && item.Salary.includes('25,662')) { console.log('Debug available salary ranges:', { jobTitle: item.PostingTitle, salaryString: item.Salary, parsedRange: salaryRange, rangeLabel: range.label, rangeMin: range.min, matches: matches }); } return matches; }); }); }, [items, selectedCategory, selectedAgency, salaryRanges]); // Get autocomplete suggestions based on search term const autocompleteSuggestions = useMemo(() => { if (!searchTerm || searchTerm.length < 2 || typeof searchTerm !== 'string' || !items) return []; const suggestions = items .filter( (item) => item && item.PostingTitle && item.PostingTitle.toLowerCase().includes(searchTerm.toLowerCase()) && item.PostingTitle.toLowerCase() !== searchTerm.toLowerCase() ) .map((item) => item.PostingTitle) .filter((name, index, array) => array.indexOf(name) === index) // Remove duplicates .sort() .slice(0, 5); // Limit to 5 suggestions return suggestions; }, [items, searchTerm]); // Reset agency filter when category changes useEffect(() => { if (selectedCategory && !agencies.includes(selectedAgency)) { setSelectedAgency(""); } }, [selectedCategory, selectedAgency, agencies]); // Reset salary range filter when category or agency changes and current range is no longer valid useEffect(() => { if ( selectedSalaryRange && !availableSalaryRanges || !availableSalaryRanges.some( (range) => range.label === selectedSalaryRange ) ) { setSelectedSalaryRange(""); } }, [ selectedCategory, selectedAgency, selectedSalaryRange, availableSalaryRanges, ]); useEffect(() => { const handleClickOutside = (event) => { if (categoryDropdownOpen && !event.target.closest('.filter-category')) { setCategoryDropdownOpen(false); } if (agencyDropdownOpen && !event.target.closest('.filter-agency')) { setAgencyDropdownOpen(false); } if (salaryDropdownOpen && !event.target.closest('.filter-salary')) { setSalaryDropdownOpen(false); } }; document.addEventListener('click', handleClickOutside); return () => document.removeEventListener('click', handleClickOutside); }, [categoryDropdownOpen, agencyDropdownOpen, salaryDropdownOpen]); // Filter items based on search and filters const filteredItems = useMemo(() => { if (!items || !Array.isArray(items)) return []; return items.filter((item) => { const searchTermLower = searchTerm ? searchTerm.toLowerCase() : ''; const matchesSearch = (item.PostingTitle && item.PostingTitle.toLowerCase().includes(searchTermLower)) || (item.Agency && item.Agency.toLowerCase().includes(searchTermLower)) || (item.Description && item.Description.toLowerCase().includes(searchTermLower)); const matchesCategory = !selectedCategory || (item.Category && item.Category.includes(selectedCategory)); const matchesAgency = !selectedAgency || item.Agency === selectedAgency; // const matchesStatus = !selectedStatus || item.status === selectedStatus; // Salary range filter: match if no salary range selected or item's minimum salary meets the range requirement const matchesSalaryRange = !selectedSalaryRange || (() => { const selectedRange = salaryRanges.find( (range) => range.label === selectedSalaryRange ); if (!selectedRange) return true; // Parse the salary string to get min/max values const salaryRange = parseSalaryString(item.Salary); // Check if item's minimum salary is greater than or equal to the selected range minimum // This works for the new "X+" format where we want jobs that pay at least X const matches = salaryRange.min >= selectedRange.min; // Debug logging for salary filtering if (item.Salary && item.Salary.includes('25,662')) { console.log('Debug salary filtering:', { jobTitle: item.PostingTitle, salaryString: item.Salary, parsedRange: salaryRange, selectedRange: selectedRange, matches: matches }); } return matches; })(); return ( matchesSearch && matchesCategory && matchesAgency && // matchesStatus && matchesSalaryRange ); }); }, [ items, searchTerm, selectedCategory, selectedAgency, // selectedStatus, selectedSalaryRange, salaryRanges, ]); // Sort filtered items by selected field and order // sortField can be "PostingTitle", "JobPostingDate", or "DaystoClose" const [sortField, setSortField] = useState("PostingTitle"); // default sort field const sortedItems = useMemo(() => { return [...filteredItems].sort((a, b) => { let aValue, bValue; if (sortField === "PostingTitle") { aValue = a.PostingTitle ? a.PostingTitle.toLowerCase() : ""; bValue = b.PostingTitle ? b.PostingTitle.toLowerCase() : ""; if (sortOrder === "asc") { return aValue.localeCompare(bValue); } else { return bValue.localeCompare(aValue); } } else if (sortField === "JobPostingDate") { // Newest first if asc, oldest first if desc (reversed from original) aValue = a.JobPostingDate ? new Date(a.JobPostingDate) : new Date(0); bValue = b.JobPostingDate ? new Date(b.JobPostingDate) : new Date(0); if (sortOrder === "asc") { return bValue - aValue; } else { return aValue - bValue; } } else if (sortField === "DaystoClose") { // Handle null values - they should come after actual numbers const aHasDays = a.DaystoClose !== null && a.DaystoClose !== undefined; const bHasDays = b.DaystoClose !== null && b.DaystoClose !== undefined; // If one has days and the other doesn't, prioritize the one with days if (aHasDays && !bHasDays) { return sortOrder === "asc" ? -1 : 1; // Days comes first in asc, last in desc } if (!aHasDays && bHasDays) { return sortOrder === "asc" ? 1 : -1; // No days comes last in asc, first in desc } if (!aHasDays && !bHasDays) { return 0; // Both have no days, maintain original order } // Both have days, sort normally (numeric comparison) aValue = Number(a.DaystoClose); bValue = Number(b.DaystoClose); if (sortOrder === "asc") { return aValue - bValue; // Ascending: lowest days first } else { return bValue - aValue; // Descending: highest days first } } // Default fallback return 0; }); }, [filteredItems, sortOrder, sortField]); // Sort filtered items by PostingTitle // const sortedItems = useMemo(() => { // return [...filteredItems].sort((a, b) => { // const titleA = a.PostingTitle.toLowerCase(); // const titleB = b.PostingTitle.toLowerCase(); // if (sortOrder === "asc") { // return titleA.localeCompare(titleB); // } else { // return titleB.localeCompare(titleA); // } // }); // }, [filteredItems, sortOrder]); // Pagination calculations const totalPages = Math.ceil((sortedItems?.length || 0) / itemsPerPage); const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = startIndex + itemsPerPage; const currentItems = sortedItems?.slice(startIndex, endIndex) || []; // Reset to first page when filters change useEffect(() => { setCurrentPage(1); }, [ searchTerm, selectedCategory, selectedAgency, // selectedStatus, selectedSalaryRange, ]); // Clear all filters const clearFilters = () => { setSearchTerm(""); setSelectedCategory(""); setSelectedAgency(""); // setSelectedStatus(""); setShowAutocomplete(false); setAutocompleteIndex(-1); setSelectedSalaryRange(""); }; // Handle search input changes const handleSearchChange = (e) => { const value = e.target.value; setSearchTerm(value); setShowAutocomplete(value.length >= 2); setAutocompleteIndex(-1); }; // Clear search function const clearSearch = () => { setSearchTerm(""); setShowAutocomplete(false); setAutocompleteIndex(-1); }; // Handle autocomplete selection const handleAutocompleteSelect = (suggestion) => { setSearchTerm(suggestion); setShowAutocomplete(false); setAutocompleteIndex(-1); }; // Handle keyboard navigation for autocomplete // If an autocomplete option is highlighted, select it. // If no autocomplete options are available, treat as a search and go to results. const handleKeyDown = (e) => { if (e.key === "Enter") { e.preventDefault(); // If autocomplete is shown and an option is highlighted, select it if (showAutocomplete && autocompleteSuggestions && autocompleteSuggestions.length > 0 && autocompleteIndex >= 0) { handleAutocompleteSelect(autocompleteSuggestions[autocompleteIndex]); setShowAutocomplete(false); setAutocompleteIndex(-1); } // If autocomplete is shown but no option is highlighted, or if no autocomplete options available else { setShowAutocomplete(false); setAutocompleteIndex(-1); // Treat as a search and go to results setIsHomepage(false); setSelectedCategory(""); } return; } // Only handle arrow keys and escape if autocomplete is shown and has options if (!showAutocomplete || !autocompleteSuggestions || autocompleteSuggestions.length === 0) return; switch (e.key) { case "ArrowDown": e.preventDefault(); setAutocompleteIndex((prev) => prev < autocompleteSuggestions.length - 1 ? prev + 1 : 0 ); break; case "ArrowUp": e.preventDefault(); setAutocompleteIndex((prev) => prev > 0 ? prev - 1 : autocompleteSuggestions.length - 1 ); break; case "Escape": setShowAutocomplete(false); setAutocompleteIndex(-1); break; } }; // Check if any filters are active const hasActiveFilters = searchTerm || selectedCategory || selectedAgency || // selectedStatus || selectedSalaryRange; const formatter = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", // These options can be used to round to whole numbers. trailingZeroDisplay: "stripIfInteger", // This is probably what most people // want. It will only stop printing // the fraction when the input // amount is a round number (int) // already. If that's not what you // need, have a look at the options // below. //minimumFractionDigits: 0, // This suffices for whole numbers, but will // print 2500.10 as $2,500.1 //maximumFractionDigits: 0, // Causes 2500.99 to be printed as $2,501 }); return ( {/* */} {isHomepage && showBanner && ( Welcome to the new DC government careers site. Explore jobs by category with improved search and navigation. Apply through agency or careers.dc.gov portals A new DC government careers site—browse jobs by category and apply through agency or careers.dc.gov portals. )}{cat.name} {cat.count !== 0 ? cat.count : ""}
{cat.description} {cat.count === 0 ? ( Coming soon) :''} ); }) ) : ( No data )} Make a difference in the heart of the nation’s capitalNo jobs found matching your criteria
) : ( currentItems.map((job) => ( { if (job?.JobPostingID) { setCurrentJobFunction(job.JobPostingID); } }} onKeyDown={(e) => { if (e.key === "Enter" && job?.JobPostingID) { setCurrentJobFunction(job.JobPostingID); } }} >{job?.PostingTitle || 'Job Title Not Available'}
Agency: {job?.Agency || 'Agency Not Available'} Location:{" "} {job?.WorkLocationAddress || 'Location Not Available'} Salary:{" "} {formatSalaryForDisplay(job?.Salary) || 'Salary not specified'} {(() => { // Calculate days since posting // if (job?.JobPostingDate && job?.ATS !== "TSHO") { if (job?.JobPostingDate) { console.log("job.JobPostingDate", job.JobPostingDate); const postingDate = new Date(job.JobPostingDate); const now = new Date(); const diffMs = now - postingDate; const daysAgo = Math.max(0, Math.ceil(diffMs / (1000 * 60 * 60 * 24))); return ( Posted {daysAgo} day{daysAgo !== 1 ? "s" : ""} ago ); } })()} {job?.Reg_Temp && ( {job.Reg_Temp} )} {job?.Full_Part && ( {job.Full_Part} )} {job?.DaystoClose === null ? "Sch Yr 25-26" : ( Closing in{" "} {job?.DaystoClose || 0} days )} )) )} {/* Pagination */} {filteredItems && filteredItems.length > 0 && totalPages > 1 && ( {/* First Page Button */} setCurrentPage(1)} disabled={currentPage === 1} className="hidden p-2 rounded-lg border border-gray-300 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" title="First page" > {/* Previous Page Button */} setCurrentPage((prev) => Math.max(prev - 1, 1)) } disabled={currentPage === 1} className="p-2 rounded-lg border border-gray-300 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" title="Previous page" > {/* Page Numbers */} {Array.from({ length: totalPages }, (_, i) => i + 1) .filter((page) => { // Show first page, last page, current page, and pages around current if ( page === 1 || page === totalPages || Math.abs(page - currentPage) <= 1 ) { return true; } return false; }) .map((page, index, array) => ( {/* Add ellipsis if there's a gap */} {index > 0 && page - array[index - 1] > 1 && ( ... )} setCurrentPage(page)} className={`px-2 py-2 rounded-lg border transition-colors ${ currentPage === page ? "bg-blue-900 text-white border-blue-500" : "bg-white border-gray-300 hover:bg-gray-50" }`} > {page} ))} {/* Next Page Button */} setCurrentPage((prev) => Math.min(prev + 1, totalPages) ) } disabled={currentPage === totalPages} className="p-2 rounded-lg border border-gray-300 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" title="Next page" > {/* Last Page Button */} setCurrentPage(totalPages)} disabled={currentPage === totalPages} className="hidden p-2 rounded-lg border border-gray-300 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" title="Last page" > )} {/* Pagination Info */} {filteredItems && filteredItems.length > 0 && totalPages > 1 && ( Page {currentPage} of {totalPages} )} {!currentJob ? ( {currentItems.length > 0 && ({currentItems[0]?.PostingTitle || 'Job Title Not Available'}
Agency:{" "} {currentItems[0]?.Agency} Location:{" "} {currentItems[0]?.WorkLocationAddress} {currentItems[0]?.Reg_Temp} {currentItems[0]?.Full_Part} Salary:{" "} {formatSalaryForDisplay(currentItems[0]?.Salary) || 'Salary not specified'} { if (currentItems[0]?.JobPostingID) { setCurrentJobFunction(currentItems[0]?.JobPostingID); } }} id={currentItems[0]?.JobPostingID || ''} > {currentItems[0]?.ATS?.includes("PS") ? "Apply on careers.dc.gov" : "Apply on DCPS site"} {/* Description */} {/* Strip style attributes from currentJob.Description before rendering */} {(() => { // Remove all style="..." and style='...' attributes from the HTML string const desc = currentItems[0]?.Description || ""; const descNoStyle = desc.replace(/style\s*=\s*(['"])[\s\S]*?\1/gi, ""); const descNoImg = descNoStyle.replace(/{currentJob?.PostingTitle || 'Job Title Not Available'}
Agency:{" "} {currentJob.Agency} Location:{" "} {currentJob.WorkLocationAddress} {currentJob.Reg_Temp} {currentJob.Full_Part} Salary:{" "} {formatSalaryForDisplay(currentJob.Salary) || 'Salary not specified'} { if (currentJob?.JobPostingID) { setCurrentJobFunction(currentJob.JobPostingID); } }} id={currentJob?.JobPostingID || ''} > {currentJob?.ATS?.includes("PS") ? "Apply on careers.dc.gov" : "Apply on DCPS site"} {/* Description */} {/* Strip style attributes from currentJob.Description before rendering */} {(() => { // Remove all style="..." and style='...' attributes from the HTML string const desc = currentJob?.Description || ""; const descNoStyle = desc.replace(/style\s*=\s*(['"])[\s\S]*?\1/gi, ""); const descNoImg = descNoStyle.replace(/{currentJob?.PostingTitle || 'Job Title Not Available'}
{ setCurrentJobFunction(currentJob?.JobPostingID); }} id={currentJob?.JobPostingID} > {currentJob?.ATS?.includes("PS") ? "Apply on careers.dc.gov" : "Apply on DCPS site"} {/* Modal Content */} Agency:{" "} {currentJob?.Agency} Location:{" "} {currentJob?.WorkLocationAddress} {currentJob?.Reg_Temp} {currentJob?.Full_Part} Salary:{" "} {formatSalaryForDisplay(currentJob?.Salary) || 'Salary not specified'} Description {/* if the job is from people soft, show the static copy on job details */} {currentJob?.ATS?.includes("PS") ? ( ) : ( )} {/* Modal Footer */} Close { setCurrentJobFunction(currentJob.JobPostingID); window.open(currentJob.JobPostingURL, "_blank"); }} className="px-4 py-2 bg-blue-900 text-white rounded-lg hover:bg-blue-600 transition-colors" > {currentJob?.ATS?.includes("PS") ? "Apply on careers.dc.gov" : "Apply on DCPS Site"} )} )}{" "} {/* End of Page 2 */} ); }; // Render the app const root = ReactDOM.createRoot(document.getElementById('root')); root.render();
Presented by
Government of the District of Columbia

Muriel Bowser
Mayor of the District of Columbia
We want to hear from you.
Your feedback is important to us. We use it to improve this website and District services, and it's always anonymous.
Was this page helpful?
Tell us what you think in our 5 minute survey.
Send your feedback
Last updated: 12:28 PM EST December 2, 2025
Maintained by DC.gov
