diff --git a/job-tracker-ui/src/App.tsx b/job-tracker-ui/src/App.tsx index 855155d..0210bd3 100644 --- a/job-tracker-ui/src/App.tsx +++ b/job-tracker-ui/src/App.tsx @@ -123,6 +123,9 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo const path = location.pathname; const isJobs = path.startsWith("/jobs"); + const shortcutHint = useMemo(() => ( + typeof navigator !== "undefined" && /Mac|iPhone|iPod|iPad/.test(navigator.platform) ? "⌘K" : "Ctrl+K" + ), []); useEffect(() => { api.get("/auth/config").then((r) => setRequireAuth(Boolean(r.data?.requireAuth))).catch(() => setRequireAuth(false)); @@ -246,14 +249,19 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo setQuickOpen(true)} sx={{ border: "1px solid", borderColor: "divider", borderRadius: 2.5, width: 42, height: 42, flex: "0 0 auto" }} > ) : ( - + )} {isJobs ? ( + + ); +} + function generateOverview(job: JobApplication): string { if (job.fullSummary) return job.fullSummary; if (job.shortSummary) return job.shortSummary; @@ -220,6 +233,12 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col return jobs.filter((job) => needsWorkflowWork(job)); }, [jobs, readinessFilter]); + // Distinguishes "you have zero jobs, period" from "no results match your filters" so the + // empty state can actually help a first-time user instead of just saying "nothing here". + const noFiltersActive = !debouncedSearch.trim() && statusFilter === "All" && companyFilterId === "All" + && !debouncedLocation.trim() && !needsFollowUpOnly && readinessFilter === "all"; + const isFirstTimeEmpty = mode === "jobs" && total === 0 && noFiltersActive; + const selectedIdSet = useMemo(() => new Set(selectedIds), [selectedIds]); const selectedAllOnPage = filteredJobs.length > 0 && filteredJobs.every((job) => selectedIdSet.has(job.id)); @@ -629,7 +648,9 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col ); })} - {filteredJobs.length === 0 && !jobsResource.loading && !jobsResource.error ? {t("jobTableNoJobsFound")} : null} + {filteredJobs.length === 0 && !jobsResource.loading && !jobsResource.error ? ( + navigate("/settings")} t={t} /> + ) : null} ) : ( @@ -721,7 +742,9 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col ); })} - {filteredJobs.length === 0 && !jobsResource.loading && !jobsResource.error ? {t("jobTableNoJobsFound")} : null} + {filteredJobs.length === 0 && !jobsResource.loading && !jobsResource.error ? ( + navigate("/settings")} t={t} /> + ) : null} diff --git a/job-tracker-ui/src/i18n/translations.ts b/job-tracker-ui/src/i18n/translations.ts index 84c091f..76432ce 100644 --- a/job-tracker-ui/src/i18n/translations.ts +++ b/job-tracker-ui/src/i18n/translations.ts @@ -606,6 +606,7 @@ export const translations = { googleAvailableToLink: "Available to link", googleLinkedDate: "Linked {date}", googleSignInHint: "Sign in with a Google account that has already been linked to your Jobbjakt user.", + googleSignInHintSelfServe: "Continue with Google. New here? We'll create your account automatically.", continueWithGoogle: "Continue with Google", signInWithGoogle: "Sign in with Google", linkWithGoogle: "Link with Google", @@ -750,6 +751,9 @@ export const translations = { jobTableOverview: "Overview", jobTableNoSummaryYet: "No summary yet.", jobTableNoJobsFound: "No jobs found.", + jobTableEmptyFirstTimeTitle: "No jobs yet — let's fix that.", + jobTableEmptyFirstTimeBody: "Click \"Add job\" above to add one manually, or paste a job posting URL. There's also a one-click bookmarklet that captures a posting straight from the page you're viewing.", + jobTableEmptyFirstTimeBookmarklet: "Set up the bookmarklet", jobTableSetStatus: "Set {status}", editJobTitle: "Edit job", editJobIntro: "Update job details, timeline status, documents, and notes from one editing workspace.", @@ -894,6 +898,7 @@ export const translations = { jobDetailsFollowUpSent: "Follow-up sent and logged.", jobDetailsFollowUpSendFailed: "Failed to send follow-up.", jobDetailsHowYouMatch: "How you match", + jobDetailsAiFitHint: "AI opinion — strengths, gaps, and a tailored pitch based on your CV and this posting.", matchScoreTitle: "Match score", matchScoreLoading: "Scoring your CV against this role…", matchScoreBand_Strong: "Strong match", @@ -902,7 +907,7 @@ export const translations = { matchScoreBand_Unknown: "Not enough signal", matchScoreKeywordsCovered: "{matched}/{total} keywords", matchScoreNoSignal: "Add more CV detail or a fuller job description to get a reliable score.", - matchScoreDeterministicHint: "Deterministic keyword coverage — no AI, so the score is stable and repeatable.", + matchScoreDeterministicHint: "Deterministic keyword coverage — no AI, so the score is stable and repeatable. For a written opinion on strengths and gaps, see the AI section below.", matchScoreMatched: "Matched keywords", matchScoreMissing: "Missing keywords", matchScoreNoneYet: "No matches found yet.", @@ -1564,6 +1569,7 @@ export const translations = { googleAvailableToLink: "Tilgjengelig for kobling", googleLinkedDate: "Koblet {date}", googleSignInHint: "Logg inn med en Google-konto som allerede er koblet til Jobbjakt-brukeren din.", + googleSignInHintSelfServe: "Fortsett med Google. Ny her? Vi oppretter kontoen din automatisk.", continueWithGoogle: "Fortsett med Google", signInWithGoogle: "Logg inn med Google", linkWithGoogle: "Koble til med Google", @@ -1708,6 +1714,9 @@ export const translations = { jobTableOverview: "Oversikt", jobTableNoSummaryYet: "Ingen oppsummering ennå.", jobTableNoJobsFound: "Ingen jobber funnet.", + jobTableEmptyFirstTimeTitle: "Ingen jobber ennå — la oss fikse det.", + jobTableEmptyFirstTimeBody: "Klikk \"Legg til jobb\" over for å legge til en manuelt, eller lim inn en lenke til en stillingsannonse. Det finnes også et bokmerke som fanger en annonse rett fra siden du ser på.", + jobTableEmptyFirstTimeBookmarklet: "Sett opp bokmerket", jobTableSetStatus: "Sett {status}", editJobTitle: "Rediger jobb", editJobIntro: "Oppdater jobbdetaljer, status i tidslinjen, dokumenter og notater fra ett redigeringsområde.", @@ -1852,6 +1861,7 @@ export const translations = { jobDetailsFollowUpSent: "Oppfølging sendt og loggført.", jobDetailsFollowUpSendFailed: "Kunne ikke sende oppfølging.", jobDetailsHowYouMatch: "Slik matcher du", + jobDetailsAiFitHint: "AI-vurdering — styrker, svakheter og et skreddersydd pitch basert på CV-en din og denne annonsen.", matchScoreTitle: "Match-score", matchScoreLoading: "Vurderer CV-en mot denne stillingen…", matchScoreBand_Strong: "Sterk match", @@ -1860,7 +1870,7 @@ export const translations = { matchScoreBand_Unknown: "For lite grunnlag", matchScoreKeywordsCovered: "{matched}/{total} nøkkelord", matchScoreNoSignal: "Legg til mer CV-innhold eller en fyldigere stillingstekst for en pålitelig score.", - matchScoreDeterministicHint: "Deterministisk nøkkelorddekning — ingen AI, så scoren er stabil og repeterbar.", + matchScoreDeterministicHint: "Deterministisk nøkkelorddekning — ingen AI, så scoren er stabil og repeterbar. For en skriftlig vurdering av styrker og svakheter, se AI-seksjonen under.", matchScoreMatched: "Treff på nøkkelord", matchScoreMissing: "Manglende nøkkelord", matchScoreNoneYet: "Ingen treff ennå.",