feat(discovery): lead with the existing search controls

This commit is contained in:
cesnimda
2026-11-09 22:57:07 +01:00
parent bd34a3504f
commit 0540e84ff6
3 changed files with 28 additions and 17 deletions
+8 -6
View File
@@ -564,7 +564,7 @@ export const translations = {
languageEnglish: "English",
languageNorwegianBokmal: "Norsk bokmål",
discoverJobs: "Discover jobs",
discoverJobsSubtitle: "Search official job-board feeds and save opportunities to your tracker.",
discoverJobsSubtitle: "Search vacancies and save opportunities to your tracker.",
landingSignIn: "Sign in",
landingSignInArrow: "Sign in →",
landingEyebrow: "AI-ASSISTED JOB SEARCH WORKSPACE",
@@ -636,11 +636,12 @@ export const translations = {
jobDiscoveryRoleCompany: "Role or company",
jobDiscoveryFilters: "Search filters",
jobDiscoveryJumpResults: "Jump to results",
jobDiscoveryReturnFilters: "Return to search filters",
jobDiscoveryReturnFilters: "Return to search",
jobDiscoveryMunicipality: "Municipality",
jobDiscoverySearchNav: "Search NAV",
jobDiscoveryResults: "Search results",
jobDiscoverySourceHelp: "Official Norwegian vacancies from NAV. FINN, Indeed and LinkedIn jobs can still be captured by URL.",
jobDiscoverySourceCoverage: "Sources and coverage",
jobDiscoverySourceHelp: "Search uses NAV for Norwegian vacancies. To track a job from another site, paste its URL and enter any details Jobjakt cannot retrieve.",
jobDiscoveryUnavailable: "NAV job discovery is temporarily unavailable.",
jobDiscoveryGuidance: "Search recent active vacancies by role, company, or municipality.",
jobDiscoveryNoResults: "No active vacancies matched this search. Try a broader role, company, or municipality.",
@@ -3787,7 +3788,7 @@ export const translations = {
languageEnglish: "Engelsk",
languageNorwegianBokmal: "Norsk bokmål",
discoverJobs: "Finn jobber",
discoverJobsSubtitle: "Søk i offisielle jobbportaler og lagre muligheter i oversikten din.",
discoverJobsSubtitle: "Søk etter stillinger og lagre dem i jobboversikten din.",
landingSignIn: "Logg inn",
landingSignInArrow: "Logg inn →",
landingEyebrow: "AI-STØTTET ARBEIDSOMRÅDE FOR JOBBSØK",
@@ -3859,11 +3860,12 @@ export const translations = {
jobDiscoveryRoleCompany: "Rolle eller bedrift",
jobDiscoveryFilters: "Søkefiltre",
jobDiscoveryJumpResults: "Gå til resultater",
jobDiscoveryReturnFilters: "Tilbake til søkefiltre",
jobDiscoveryReturnFilters: "Tilbake til søk",
jobDiscoveryMunicipality: "Kommune",
jobDiscoverySearchNav: "Søk i NAV",
jobDiscoveryResults: "Søkeresultater",
jobDiscoverySourceHelp: "Offisielle norske stillinger fra NAV. Stillinger fra FINN, Indeed og LinkedIn kan fortsatt hentes inn via nettadresse.",
jobDiscoverySourceCoverage: "Kilder og dekning",
jobDiscoverySourceHelp: "Søket bruker NAV for norske stillinger. For å følge en stilling fra et annet nettsted kan du lime inn nettadressen og fylle ut opplysningene Jobjakt ikke kan hente.",
jobDiscoveryUnavailable: "NAVs jobbsøk er midlertidig utilgjengelig.",
jobDiscoveryGuidance: "Søk i nylige aktive stillinger etter rolle, bedrift eller kommune.",
jobDiscoveryNoResults: "Ingen aktive stillinger samsvarte med søket. Prøv en bredere rolle, bedrift eller kommune.",
+10 -5
View File
@@ -24,20 +24,25 @@ beforeEach(() => { jest.resetAllMocks(); window.localStorage.clear(); searchResp
afterEach(() => jest.clearAllMocks());
test.each([
['en', 'Search filters', 'Jump to results', 'Return to search filters'],
['nb', 'Søkefiltre', 'Gå til resultater', 'Tilbake til søkefiltre'],
] as const)('keeps %s filters before results with keyboard jump/return and no automatic provider search', async (language, filtersName, jumpName, returnName) => {
['en', 'Search filters', 'Jump to results', 'Return to search', 'Sources and coverage'],
['nb', 'Søkefiltre', 'Gå til resultater', 'Tilbake til søk', 'Kilder og dekning'],
] as const)('keeps %s search before filters and results with keyboard jump/return and no automatic provider search', async (language, filtersName, jumpName, returnName, sourceName) => {
localStorage.setItem('uiLanguage', language);
renderPage();
await screen.findByText(translations[language].jobDiscoveryNoSavedSearches);
const filters = screen.getByRole('complementary', { name: filtersName });
const form = screen.getByRole('form', { name: translations[language].jobDiscoverySearchNav });
const query = within(form).getByRole('textbox', { name: translations[language].jobDiscoveryRoleCompany });
const municipality = within(filters).getByRole('textbox', { name: translations[language].jobDiscoveryMunicipality }) as HTMLInputElement;
const results = screen.getByRole('region', { name: translations[language].jobDiscoveryResults });
expect(form.compareDocumentPosition(filters) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(filters.compareDocumentPosition(results) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(within(filters).getByRole('form', { name: filtersName })).toBeInTheDocument();
expect(municipality.form).toBe(form);
expect(within(filters).getByText(sourceName).closest('details')).not.toHaveAttribute('open');
fireEvent.click(within(filters).getByRole('button', { name: jumpName }));
expect(results).toHaveFocus();
fireEvent.click(within(results).getByRole('button', { name: returnName }));
expect(screen.getByRole('textbox', { name: translations[language].jobDiscoveryRoleCompany })).toHaveFocus();
expect(query).toHaveFocus();
expect(mockedApi.get.mock.calls.map(([url]) => url)).toEqual(['/job-discovery/saved-searches']);
expect(mockedApi.post).not.toHaveBeenCalled();
});
+10 -6
View File
@@ -1,4 +1,4 @@
import { FormEvent, useEffect, useMemo, useRef, useState } from "react";
import { FormEvent, useEffect, useId, useMemo, useRef, useState } from "react";
import { Alert, Box, Button, Card, CardActions, CardContent, Chip, CircularProgress, FormControlLabel, MenuItem, Stack, Switch, TextField, Typography } from "@mui/material";
import SearchIcon from "@mui/icons-material/Search";
import AddIcon from "@mui/icons-material/Add";
@@ -36,6 +36,7 @@ type SortOrder = "updated" | "deadline" | "title";
export default function JobDiscoveryPage() {
const navigate = useNavigate();
const searchFormId = useId();
const { language, t } = useI18n();
const { confirmAction } = useDialogActions();
const [query, setQuery] = useState("");
@@ -183,14 +184,17 @@ export default function JobDiscoveryPage() {
return (
<Stack spacing={3} sx={{ minWidth: 0 }}>
<Alert severity="info" variant="outlined" sx={{ color: "text.primary", "& .MuiAlert-icon": { color: "info.main" } }}>{t("jobDiscoverySourceHelp")}</Alert>
<Box component="form" id={searchFormId} aria-label={t("jobDiscoverySearchNav")} onSubmit={search} sx={{ display: "grid", gridTemplateColumns: { xs: "minmax(0, 1fr)", sm: "minmax(0, 1fr) auto" }, gap: 1.5 }}>
<TextField inputRef={queryRef} label={t("jobDiscoveryRoleCompany")} value={query} disabled={busy} onChange={(event) => setQuery(event.target.value)} />
<Button type="submit" variant="contained" startIcon={loading ? <CircularProgress aria-hidden="true" size={18} color="inherit" /> : <SearchIcon />} disabled={busy}>{t("jobDiscoverySearchNav")}</Button>
</Box>
<Box data-discovery-layout sx={{ display: "grid", gap: 3, minWidth: 0, alignItems: "start", "@media (min-width:1001px)": { gridTemplateColumns: "minmax(16rem, 20rem) minmax(0, 1fr)" } }}>
<Box component="aside" aria-label={t("jobDiscoveryFilters")} sx={{ display: "grid", gap: 2, minWidth: 0 }}>
<PageHeader level="h2" title={t("jobDiscoveryFilters")} actions={<Button onClick={() => resultsRef.current?.focus()}>{t("jobDiscoveryJumpResults")}</Button>} />
<Box component="form" aria-label={t("jobDiscoveryFilters")} onSubmit={search} sx={{ display: "grid", gridTemplateColumns: "minmax(0, 1fr)", gap: 1.5 }}>
<TextField inputRef={queryRef} label={t("jobDiscoveryRoleCompany")} value={query} disabled={busy} onChange={(event) => setQuery(event.target.value)} />
<TextField label={t("jobDiscoveryMunicipality")} value={location} disabled={busy} onChange={(event) => setLocation(event.target.value)} />
<Button type="submit" variant="contained" startIcon={loading ? <CircularProgress aria-hidden="true" size={18} color="inherit" /> : <SearchIcon />} disabled={busy}>{t("jobDiscoverySearchNav")}</Button>
<TextField label={t("jobDiscoveryMunicipality")} value={location} disabled={busy} onChange={(event) => setLocation(event.target.value)} slotProps={{ htmlInput: { form: searchFormId } }} />
<Box component="details">
<Typography component="summary" variant="body2" sx={{ cursor: "pointer", py: 0.5, "@media (pointer: coarse)": { minHeight: 44 } }}>{t("jobDiscoverySourceCoverage")}</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>{t("jobDiscoverySourceHelp")}</Typography>
</Box>
<Box aria-busy={savedSearchState === "loading"}>
<Box sx={{ display: "grid", gap: 1.5 }}>