feat(ux): first-time onboarding, empty states, and copy fixes

Implements the six "propose first" items from the product/UX review:

- "/" now redirects to /dashboard instead of the empty /jobs table --
  a new user's first screen is now an overview with orientation, not
  a data table with zero rows and four filter dropdowns.
- Jobs table gets a real first-time empty state (distinct from "no
  results match your filters") pointing at Add Job and the bookmarklet,
  instead of a bare "No jobs found."
- Match Score card and Candidate Fit tab now each get a one-line
  caption explaining what they are and how they differ (deterministic
  keyword coverage vs. AI opinion) -- they previously sat side by side
  with no explanation of why there are two.
- Google sign-in hint now reflects self-serve signup when
  Auth:AllowRegistration is on, instead of always implying you need an
  existing linked account.
- Quick Search button now shows its keyboard shortcut (Ctrl+K / ⌘K)
  inline instead of being undiscoverable.
This commit is contained in:
cesnimda
2026-07-12 04:05:32 +02:00
parent 7dadf8dde4
commit 58868fc2b6
5 changed files with 54 additions and 8 deletions
+25 -2
View File
@@ -110,6 +110,19 @@ function parseTags(raw?: string | null): string[] {
}
function EmptyJobsState({ firstTime, onOpenSettings, t }: { firstTime: boolean; onOpenSettings: () => void; t: (key: any) => string }) {
if (!firstTime) {
return <Typography sx={{ py: 2, textAlign: "center", color: "text.secondary" }}>{t("jobTableNoJobsFound")}</Typography>;
}
return (
<Box sx={{ py: 4, textAlign: "center" }}>
<Typography sx={{ fontWeight: 800, mb: 0.5 }}>{t("jobTableEmptyFirstTimeTitle")}</Typography>
<Typography sx={{ color: "text.secondary", mb: 1.5, maxWidth: 440, mx: "auto" }}>{t("jobTableEmptyFirstTimeBody")}</Typography>
<Button variant="text" onClick={onOpenSettings}>{t("jobTableEmptyFirstTimeBookmarklet")}</Button>
</Box>
);
}
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
</Paper>
);
})}
{filteredJobs.length === 0 && !jobsResource.loading && !jobsResource.error ? <Typography sx={{ py: 2, textAlign: "center" }}>{t("jobTableNoJobsFound")}</Typography> : null}
{filteredJobs.length === 0 && !jobsResource.loading && !jobsResource.error ? (
<EmptyJobsState firstTime={isFirstTimeEmpty} onOpenSettings={() => navigate("/settings")} t={t} />
) : null}
</Stack>
) : (
<Box sx={{ overflowX: "auto" }}>
@@ -721,7 +742,9 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
</React.Fragment>
);
})}
{filteredJobs.length === 0 && !jobsResource.loading && !jobsResource.error ? <TableRow><TableCell colSpan={visibleDesktopColumns}><Typography sx={{ py: 2, textAlign: "center" }}>{t("jobTableNoJobsFound")}</Typography></TableCell></TableRow> : null}
{filteredJobs.length === 0 && !jobsResource.loading && !jobsResource.error ? (
<TableRow><TableCell colSpan={visibleDesktopColumns}><EmptyJobsState firstTime={isFirstTimeEmpty} onOpenSettings={() => navigate("/settings")} t={t} /></TableCell></TableRow>
) : null}
</TableBody>
</Table>
</Box>