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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user