Careers across agencies

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

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. )} Image removed. {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'}`} >

{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 capital Image removed. Image removed. Image removed. ) : ( <> {/* */} {/* 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 ? (

No 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(/Image removed.]*>/gi, ""); return ( ); })()} {/* if the job is from people soft, show the static copy on job details */} {currentItems[0]?.ATS?.includes("PS") ? ( ) : ( )} )} ) : (

{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(/Image removed.]*>/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 */}

{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();
We Are DC

Powered by

DC.gov

DC logo

Visit DC.gov's website

Presented by

Government of the District of Columbia

Mayor-image.png

Muriel Bowser

Mayor of the District of Columbia

Visit the Mayor's site

Last updated: 12:28 PM EST December 2, 2025
Maintained by DC.gov