google dork links (#239)

* Add ready tab Google dork links

* ui changes

* docs

* ci fix
This commit is contained in:
Shaheer Sarfaraz
2026-02-28 22:23:50 +00:00
committed by GitHub
parent 432529b581
commit 6a19fff436
8 changed files with 361 additions and 46 deletions
@@ -147,4 +147,44 @@ describe("ReadyPanel", () => {
screen.queryByTestId("job-details-edit-drawer"),
).not.toBeInTheDocument();
});
it("renders descriptive google dork links in the ready summary", async () => {
render(
<MemoryRouter>
<ReadyPanel
job={createJob({
employer: "HP",
title: "Frontend Engineer",
skills: "Wolf Security, React, TypeScript",
})}
onJobUpdated={vi.fn()}
onJobMoved={vi.fn()}
/>
</MemoryRouter>,
);
await waitFor(() =>
expect(api.getResumeProjectsCatalog).toHaveBeenCalled(),
);
fireEvent.click(
screen.getByRole("button", {
name: /3 search links/i,
}),
);
const linkedInLink = screen.getByRole("link", {
name: "LinkedIn profiles with HP, Wolf Security, and React in them",
});
expect(linkedInLink).toHaveAttribute(
"href",
`https://www.google.com/search?q=${encodeURIComponent('site:linkedin.com/in "HP" "Wolf Security" "React"')}`,
);
expect(linkedInLink).toHaveAttribute("target", "_blank");
expect(linkedInLink).toHaveAttribute("rel", "noopener noreferrer");
expect(linkedInLink).toHaveAttribute(
"title",
'site:linkedin.com/in "HP" "Wolf Security" "React"',
);
});
});
@@ -25,13 +25,7 @@ import {
import type React from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
import { Button, buttonVariants } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
@@ -58,6 +52,8 @@ import { TailorMode } from "./discovered-panel/TailorMode";
import { GhostwriterDrawer } from "./ghostwriter/GhostwriterDrawer";
import { JobDetailsEditDrawer } from "./JobDetailsEditDrawer";
import { KbdHint } from "./KbdHint";
import { ReadySummaryAccordion } from "./ReadySummaryAccordion";
import { buildReadyPanelGoogleDorks } from "./ready-panel-google-dorks";
type PanelMode = "ready" | "tailor";
@@ -127,6 +123,10 @@ export const ReadyPanel: React.FC<ReadyPanelProps> = ({
const selectedProjectIds = useMemo(() => {
return job?.selectedProjectIds?.split(",").filter(Boolean) ?? [];
}, [job?.selectedProjectIds]);
const googleDorks = useMemo(
() => (job ? buildReadyPanelGoogleDorks(job) : []),
[job],
);
const handleUndoApplied = useCallback(
async (jobId: string) => {
@@ -427,47 +427,66 @@ export const ReadyPanel: React.FC<ReadyPanelProps> = ({
</div>
</div>
{/* ─────────────────────────────────────────────────────────────────────
APPLICATION KIT SUMMARY
Abstract representation of what the PDF contains - verify at a glance
───────────────────────────────────────────────────────────────────── */}
<div className="flex-1 py-4 space-y-4">
{/* Job identity - confirm this is the right role */}
<div className="space-y-3">
<FitAssessment job={job} />
<TailoredSummary job={job} />
{googleDorks.length > 0 ? (
<ReadySummaryAccordion
icon={ExternalLink}
summary={
<>
{googleDorks.length}{" "}
{googleDorks.length === 1 ? "search link" : "search links"}
</>
}
value="search-dorks"
>
<div className="text-muted-foreground flex flex-col items-start gap-2">
{googleDorks.map((dork) => (
<a
key={dork.query}
href={dork.href}
rel="noopener noreferrer"
target="_blank"
title={dork.query}
className={cn(
buttonVariants({ variant: "link", size: "sm" }),
"justify-start w-fit h-fit gap-1 px-0 wrap-break-word",
)}
>
{dork.label}
<ExternalLink className="ml-1" />
</a>
))}
</div>
</ReadySummaryAccordion>
) : null}
{/* Project selection - expandable accordion */}
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="projects" className="border-none">
<AccordionTrigger className="hover:no-underline py-0 data-[state=open]:pb-2">
<div className="flex items-center gap-3 w-full">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted/50 text-muted-foreground">
<FolderKanban className="h-4 w-4" />
</div>
<div className="min-w-0 flex-1 text-left">
<div className="text-sm font-medium text-foreground leading-tight">
{selectedProjectIds.length}{" "}
{selectedProjectIds.length === 1 ? "project" : "projects"}{" "}
selected
</div>
</div>
</div>
</AccordionTrigger>
<AccordionContent className="pt-1 pl-11">
<ul className="list-disc text-xs text-muted-foreground space-y-1">
{selectedProjectIds.map((id) => {
const name = catalog.find((p) => p.id === id)?.name;
if (!name) return null;
return <li key={id}>{name}</li>;
})}
{selectedProjectIds.length === 0 && (
<li className="list-none italic">No projects selected</li>
)}
</ul>
</AccordionContent>
</AccordionItem>
</Accordion>
<ReadySummaryAccordion
icon={FolderKanban}
summary={
<>
{selectedProjectIds.length}{" "}
{selectedProjectIds.length === 1 ? "project" : "projects"}{" "}
selected
</>
}
value="projects"
>
<ul className="list-disc text-xs text-muted-foreground space-y-1">
{selectedProjectIds.map((id) => {
const name = catalog.find((p) => p.id === id)?.name;
if (!name) return null;
return <li key={id}>{name}</li>;
})}
{selectedProjectIds.length === 0 && (
<li className="list-none italic">No projects selected</li>
)}
</ul>
</ReadySummaryAccordion>
</div>
</div>
@@ -0,0 +1,46 @@
import type { LucideIcon } from "lucide-react";
import type React from "react";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
interface ReadySummaryAccordionProps {
children: React.ReactNode;
icon: LucideIcon;
summary: React.ReactNode;
value: string;
}
export const ReadySummaryAccordion: React.FC<ReadySummaryAccordionProps> = ({
children,
icon: Icon,
summary,
value,
}) => {
return (
<Accordion type="single" collapsible className="w-full">
<AccordionItem value={value} className="border-none">
<AccordionTrigger className="cursor-pointer rounded-xl border border-border/40 px-2 py-1 hover:bg-muted/50 hover:no-underline data-[state=open]:rounded-b-none data-[state=open]:bg-muted/10 data-[state=open]:pb-2">
<div className="flex items-center gap-3 w-full">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted/50 text-muted-foreground">
<Icon className="h-4 w-4" />
</div>
<div className="min-w-0 flex-1 text-left">
<div className="text-sm font-medium text-foreground leading-tight">
{summary}
</div>
</div>
</div>
</AccordionTrigger>
<AccordionContent className="rounded-b-xl border border-border/40 bg-muted/10 pt-4 pl-13">
{children}
</AccordionContent>
</AccordionItem>
</Accordion>
);
};
@@ -0,0 +1,74 @@
import { createJob } from "@shared/testing/factories.js";
import { describe, expect, it } from "vitest";
import { buildReadyPanelGoogleDorks } from "./ready-panel-google-dorks";
describe("buildReadyPanelGoogleDorks", () => {
it("returns three links from employer, title, and skills", () => {
const links = buildReadyPanelGoogleDorks(
createJob({
employer: "HP",
title: "Frontend Engineer",
skills: "Wolf Security, React, TypeScript",
}),
);
expect(links).toHaveLength(3);
expect(links[0]).toMatchObject({
query: 'site:linkedin.com/in "HP" "Wolf Security" "React"',
label: "LinkedIn profiles with HP, Wolf Security, and React in them",
});
expect(links[1]).toMatchObject({
query: 'site:github.com "HP" "Wolf Security" "React"',
label: "GitHub pages with HP, Wolf Security, and React in them",
});
expect(links[2]).toMatchObject({
query: '"HP" "Frontend Engineer" "Wolf Security"',
label:
"Web results with HP, Frontend Engineer, and Wolf Security in them",
});
expect(links[0]?.href).toContain(
encodeURIComponent('site:linkedin.com/in "HP" "Wolf Security" "React"'),
);
});
it("falls back to tailored skills when raw skills are absent", () => {
const links = buildReadyPanelGoogleDorks(
createJob({
employer: "Acme",
title: "Backend Engineer",
skills: null,
tailoredSkills: JSON.stringify(["Node.js", "TypeScript"]),
}),
);
expect(links[0]?.query).toBe(
'site:linkedin.com/in "Acme" "Node.js" "TypeScript"',
);
});
it("deduplicates repeated keywords and excludes employer matches", () => {
const links = buildReadyPanelGoogleDorks(
createJob({
employer: "Acme",
skills: "Acme, React, react, TypeScript, TypeScript",
}),
);
expect(links[0]?.query).toBe(
'site:linkedin.com/in "Acme" "React" "TypeScript"',
);
expect(links[1]?.query).toBe('site:github.com "Acme" "React" "TypeScript"');
});
it("returns no links when employer and usable keywords are missing", () => {
const links = buildReadyPanelGoogleDorks(
createJob({
employer: "",
skills: null,
tailoredSkills: null,
}),
);
expect(links).toEqual([]);
});
});
@@ -0,0 +1,122 @@
import type { Job } from "@shared/types.js";
export interface ReadyPanelGoogleDorkLink {
href: string;
label: string;
query: string;
}
function splitRawSkills(skills: string): string[] {
return skills
.split(/[,\n|]+/g)
.map((value) => value.trim())
.filter(Boolean);
}
function parseTailoredSkills(raw: string): string[] {
try {
const parsed = JSON.parse(raw) as unknown;
if (!Array.isArray(parsed)) return [];
return parsed
.filter((value): value is string => typeof value === "string")
.map((value) => value.trim())
.filter(Boolean);
} catch {
return [];
}
}
function getKeywordTerms(job: Job): string[] {
const employer = job.employer.trim().toLowerCase();
const rawTerms = job.skills
? splitRawSkills(job.skills)
: job.tailoredSkills
? parseTailoredSkills(job.tailoredSkills)
: [];
const seen = new Set<string>();
const terms: string[] = [];
for (const term of rawTerms) {
const normalized = term.toLowerCase();
if (normalized === employer || seen.has(normalized)) continue;
seen.add(normalized);
terms.push(term);
if (terms.length === 2) break;
}
return terms;
}
function quoteTerms(terms: string[]): string {
return terms.map((term) => `"${term}"`).join(" ");
}
function formatTermList(terms: string[]): string {
if (terms.length === 0) return "";
if (terms.length === 1) return terms[0];
if (terms.length === 2) return `${terms[0]} and ${terms[1]}`;
return `${terms.slice(0, -1).join(", ")}, and ${terms.at(-1)}`;
}
function buildDork(
prefix: "LinkedIn profiles" | "GitHub pages" | "Web results",
queryTerms: string[],
): ReadyPanelGoogleDorkLink | null {
if (queryTerms.length === 0) return null;
const query = quoteTerms(queryTerms);
return {
query,
href: `https://www.google.com/search?q=${encodeURIComponent(query)}`,
label: `${prefix} with ${formatTermList(queryTerms)} in them`,
};
}
export function buildReadyPanelGoogleDorks(
job: Job,
): ReadyPanelGoogleDorkLink[] {
const employer = job.employer.trim();
const title = job.title.trim();
const keywords = getKeywordTerms(job);
if (!employer && keywords.length === 0) {
return [];
}
const linkedinTerms = [employer, ...keywords].filter(Boolean);
const githubTerms = [employer, ...keywords].filter(Boolean);
const webTerms = [employer, title, keywords[0]].filter(Boolean);
const links: ReadyPanelGoogleDorkLink[] = [];
const linkedinQuery =
linkedinTerms.length > 0
? `site:linkedin.com/in ${quoteTerms(linkedinTerms)}`
: "";
if (linkedinQuery) {
links.push({
query: linkedinQuery,
href: `https://www.google.com/search?q=${encodeURIComponent(linkedinQuery)}`,
label: `LinkedIn profiles with ${formatTermList(linkedinTerms)} in them`,
});
}
const githubQuery =
githubTerms.length > 0 ? `site:github.com ${quoteTerms(githubTerms)}` : "";
if (githubQuery) {
links.push({
query: githubQuery,
href: `https://www.google.com/search?q=${encodeURIComponent(githubQuery)}`,
label: `GitHub pages with ${formatTermList(githubTerms)} in them`,
});
}
const webLink = buildDork("Web results", webTerms);
if (webLink) {
links.push(webLink);
}
return links;
}
+1 -1
View File
@@ -17,7 +17,7 @@ const buttonVariants = cva(
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
link: "text-foreground underline-offset-4 hover:underline py-0 h-auto",
},
size: {
default: "h-9 px-4 py-2",