Careers across agencies
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.
