refactor(web): let the homepage, command palette and ats checker compile with the react compiler

The ATS sample fetch checks the response and reports failures, the ats-pdf chunk loads alongside extraction, and the feature explorer and command palette pages are split into one component per view.
This commit is contained in:
Amruth Pillai
2026-09-29 18:13:08 +02:00
parent 38447bd041
commit c0e9d3fc62
8 changed files with 537 additions and 471 deletions
+18 -5
View File
@@ -138,6 +138,15 @@ export function AtsChecker({ signedIn, importPending }: AtsCheckerProps) {
);
}
async function fetchSample() {
try {
const res = await fetch(SAMPLE.url);
return res.ok ? await res.blob() : null;
} catch {
return null;
}
}
type IdleProps = {
error?: string | undefined;
posting: string;
@@ -150,9 +159,13 @@ function Idle({ error, posting, onPosting, onFile }: IdleProps) {
const input = useRef<HTMLInputElement>(null);
const [dragging, setDragging] = useState(false);
const [sampleError, setSampleError] = useState<string>();
const sample = async () => {
const blob = await (await fetch(SAMPLE.url)).blob();
onFile(new File([blob], SAMPLE.name, { type: "application/pdf" }));
setSampleError(undefined);
const blob = await fetchSample();
if (blob) onFile(new File([blob], SAMPLE.name, { type: "application/pdf" }));
else setSampleError(t`Couldn't load the sample file. Try again.`);
};
return (
@@ -217,9 +230,9 @@ function Idle({ error, posting, onPosting, onFile }: IdleProps) {
/>
</div>
{error && (
{(error ?? sampleError) && (
<p role="alert" className="rounded-lg bg-danger-soft px-3 py-2.5 text-danger-text text-sm">
{error}
{error ?? sampleError}
</p>
)}
@@ -278,7 +291,7 @@ const verdict = (score: number) =>
type Row = { key: string; title: string; count: string; tone: "danger" | "warn" | "accent"; body: React.ReactNode };
/** One row per category with something to fix, worst first, plus the posting's terms when one was pasted. */
export function issueRows(report: PdfAtsReport): Row[] {
function issueRows(report: PdfAtsReport): Row[] {
const byCategory = new Map<PdfCategory, PdfAtsReport["findings"]>();
for (const finding of report.findings)
byCategory.set(finding.category, [...(byCategory.get(finding.category) ?? []), finding]);
@@ -21,12 +21,15 @@ export type RunAtsCheckOptions = {
* who never uploads a file never downloads either.
*/
export async function runAtsCheck(file: File, options: RunAtsCheckOptions = {}): Promise<AtsCheckResult> {
const engine = import("@reactive-resume/resume/ats-pdf");
// Awaited below; this only keeps a failed chunk load from surfacing as unhandled when extraction throws first.
engine.catch(() => {});
const raw = await extractPdf(file, {
...(options.onProgress ? { onProgress: options.onProgress } : {}),
...(options.signal ? { signal: options.signal } : {}),
});
const { analyzePdfResume, buildExtractedDocument } = await import("@reactive-resume/resume/ats-pdf");
const { analyzePdfResume, buildExtractedDocument } = await engine;
const report = analyzePdfResume(raw, {
...(options.jobDescription?.trim() ? { jobDescription: options.jobDescription } : {}),
@@ -173,7 +173,6 @@ describe("ResumesCommandGroup", () => {
renderGroup();
expect(mocks.resumeQueryOptions).toHaveBeenCalledWith({
enabled: true,
input: { sort: "lastUpdatedAt", tags: [] },
});
expect(screen.getByText("Evil Apricot Pike")).toBeInTheDocument();
@@ -16,6 +16,7 @@ import { BaseCommandGroup } from "./base";
type Application = RouterOutput["applications"]["list"][number];
type Thread = RouterOutput["agent"]["threads"]["list"][number];
type SearchPage = "resumes" | "applications" | "threads";
type SearchPageProps = { page: SearchPage };
const isSearchPage = (page: string | undefined): page is SearchPage =>
page === "resumes" || page === "applications" || page === "threads";
@@ -26,81 +27,12 @@ const matchesSearch = (search: string, values: Array<string | null | undefined>)
};
export function ResumesCommandGroup() {
const navigate = useNavigate();
const { openDialog } = useDialogStore();
const { session } = useRouteContext({ strict: false });
const reset = useCommandPaletteStore((state) => state.reset);
const peekPage = useCommandPaletteStore((state) => state.peekPage);
const pushPage = useCommandPaletteStore((state) => state.pushPage);
const search = useCommandPaletteStore((state) => state.search);
const commandPage = peekPage();
const commandSearchPage = isSearchPage(commandPage) ? commandPage : undefined;
const searchPage = commandSearchPage;
const { data: resumes, isLoading } = useQuery(
orpc.resume.list.queryOptions({
enabled: !!session && searchPage === "resumes",
input: { sort: "lastUpdatedAt", tags: [] },
}),
);
const { data: applications, isLoading: isLoadingApplications } = useQuery({
...applicationsListQueryOptions(),
enabled: !!session && searchPage === "applications",
});
const { data: threads, isLoading: isLoadingThreads } = useQuery(
orpc.agent.threads.list.queryOptions({
enabled: !!session && searchPage === "threads",
}),
);
const filteredResumes = (resumes ?? []).filter((resume) => matchesSearch(search, [resume.name, resume.slug]));
const filteredApplications = (applications ?? []).filter((application) =>
matchesSearch(search, [application.company, application.role]),
);
const filteredThreads = (threads ?? []).filter((thread) =>
matchesSearch(search, [thread.title, thread.resumeName, thread.coverLetterName]),
);
const onCreate = () => {
reset();
openDialog("document.new", undefined);
};
const onNavigate = async (path: string) => {
await navigate({ to: path });
reset();
};
const onCreateApplication = async () => {
await navigate({ to: "/dashboard/applications", search: { create: true } });
reset();
};
const onOpenApplication = async (application: Application) => {
await navigate({
to: "/dashboard/applications",
search: { applicationId: application.id },
});
reset();
};
// A conversation opens its document with the assistant showing it.
const onOpenThread = async (thread: Thread) => {
if (thread.coverLetterId)
await navigate({
to: "/builder/letter/$coverLetterId",
params: { coverLetterId: thread.coverLetterId },
search: { assistant: thread.id },
});
else if (thread.workingResumeId)
await navigate({
to: "/builder/$resumeId",
params: { resumeId: thread.workingResumeId },
search: { assistant: thread.id },
});
reset();
};
const searchPage = isSearchPage(commandPage) ? commandPage : undefined;
if (!session) return null;
@@ -127,93 +59,168 @@ export function ResumesCommandGroup() {
</CommandItem>
</BaseCommandGroup>
{searchPage === "resumes" ? (
<BaseCommandGroup page={commandSearchPage} heading={<Trans>Resumes</Trans>}>
<CommandItem value="resumes.create" onSelect={onCreate}>
<Icon name="add" size={16} />
<Trans>Create a new resume</Trans>
</CommandItem>
{isLoading ? (
<CommandLoading>
<Trans>Loading resumes…</Trans>
</CommandLoading>
) : (
filteredResumes.map((resume) => (
<CommandItem
key={resume.id}
value={`resume.${resume.id}`}
keywords={[resume.name, resume.slug]}
onSelect={() => onNavigate(`/builder/${resume.id}`)}
>
<Icon name="description" size={16} />
{resume.name}
<CommandShortcut className="opacity-0 group-data-[selected=true]/command-item:opacity-100">
<Trans comment="Command palette hint that pressing Enter opens the selected resume">
Press <Kbd>Enter</Kbd> to open
</Trans>
</CommandShortcut>
</CommandItem>
))
)}
</BaseCommandGroup>
) : null}
{searchPage === "applications" ? (
<BaseCommandGroup page={commandSearchPage} heading={<Trans>Applications</Trans>}>
<CommandItem value="applications.create" onSelect={onCreateApplication}>
<Icon name="add" size={16} />
<Trans>New Application</Trans>
</CommandItem>
{isLoadingApplications ? (
<CommandLoading>
<Trans>Loading applications…</Trans>
</CommandLoading>
) : (
filteredApplications.map((application) => (
<CommandItem
key={application.id}
value={`application.${application.id}`}
keywords={[application.company, application.role]}
onSelect={() => onOpenApplication(application)}
>
<Icon name="work" size={16} />
<span className="min-w-0 truncate">{application.company}</span>
<span className="truncate text-ink-3 text-xs">{application.role}</span>
</CommandItem>
))
)}
</BaseCommandGroup>
) : null}
{searchPage === "threads" ? (
<BaseCommandGroup page={commandSearchPage} heading={<Trans>Assistant conversations</Trans>}>
{isLoadingThreads ? (
<CommandLoading>
<Trans>Loading conversations…</Trans>
</CommandLoading>
) : (
filteredThreads.map((thread) => {
const documentName = thread.coverLetterName ?? thread.resumeName ?? "";
return (
<CommandItem
key={thread.id}
value={`thread.${thread.id}`}
keywords={[thread.title, documentName]}
onSelect={() => onOpenThread(thread)}
>
<Icon name="chat" size={16} />
<span className="min-w-0 truncate">{thread.title}</span>
<span className="truncate text-ink-3 text-xs">{documentName}</span>
</CommandItem>
);
})
)}
</BaseCommandGroup>
) : null}
{searchPage === "resumes" ? <ResumesPage page={searchPage} /> : null}
{searchPage === "applications" ? <ApplicationsPage page={searchPage} /> : null}
{searchPage === "threads" ? <ThreadsPage page={searchPage} /> : null}
</>
);
}
function ResumesPage({ page }: SearchPageProps) {
const navigate = useNavigate();
const { openDialog } = useDialogStore();
const reset = useCommandPaletteStore((state) => state.reset);
const search = useCommandPaletteStore((state) => state.search);
const { data: resumes, isLoading } = useQuery(
orpc.resume.list.queryOptions({ input: { sort: "lastUpdatedAt", tags: [] } }),
);
const filteredResumes = (resumes ?? []).filter((resume) => matchesSearch(search, [resume.name, resume.slug]));
const onCreate = () => {
reset();
openDialog("document.new", undefined);
};
const onNavigate = async (path: string) => {
await navigate({ to: path });
reset();
};
return (
<BaseCommandGroup page={page} heading={<Trans>Resumes</Trans>}>
<CommandItem value="resumes.create" onSelect={onCreate}>
<Icon name="add" size={16} />
<Trans>Create a new resume</Trans>
</CommandItem>
{isLoading ? (
<CommandLoading>
<Trans>Loading resumes…</Trans>
</CommandLoading>
) : (
filteredResumes.map((resume) => (
<CommandItem
key={resume.id}
value={`resume.${resume.id}`}
keywords={[resume.name, resume.slug]}
onSelect={() => onNavigate(`/builder/${resume.id}`)}
>
<Icon name="description" size={16} />
{resume.name}
<CommandShortcut className="opacity-0 group-data-[selected=true]/command-item:opacity-100">
<Trans comment="Command palette hint that pressing Enter opens the selected resume">
Press <Kbd>Enter</Kbd> to open
</Trans>
</CommandShortcut>
</CommandItem>
))
)}
</BaseCommandGroup>
);
}
function ApplicationsPage({ page }: SearchPageProps) {
const navigate = useNavigate();
const reset = useCommandPaletteStore((state) => state.reset);
const search = useCommandPaletteStore((state) => state.search);
const { data: applications, isLoading } = useQuery(applicationsListQueryOptions());
const filteredApplications = (applications ?? []).filter((application) =>
matchesSearch(search, [application.company, application.role]),
);
const onCreateApplication = async () => {
await navigate({ to: "/dashboard/applications", search: { create: true } });
reset();
};
const onOpenApplication = async (application: Application) => {
await navigate({
to: "/dashboard/applications",
search: { applicationId: application.id },
});
reset();
};
return (
<BaseCommandGroup page={page} heading={<Trans>Applications</Trans>}>
<CommandItem value="applications.create" onSelect={onCreateApplication}>
<Icon name="add" size={16} />
<Trans>New Application</Trans>
</CommandItem>
{isLoading ? (
<CommandLoading>
<Trans>Loading applications…</Trans>
</CommandLoading>
) : (
filteredApplications.map((application) => (
<CommandItem
key={application.id}
value={`application.${application.id}`}
keywords={[application.company, application.role]}
onSelect={() => onOpenApplication(application)}
>
<Icon name="work" size={16} />
<span className="min-w-0 truncate">{application.company}</span>
<span className="truncate text-ink-3 text-xs">{application.role}</span>
</CommandItem>
))
)}
</BaseCommandGroup>
);
}
function ThreadsPage({ page }: SearchPageProps) {
const navigate = useNavigate();
const reset = useCommandPaletteStore((state) => state.reset);
const search = useCommandPaletteStore((state) => state.search);
const { data: threads, isLoading } = useQuery(orpc.agent.threads.list.queryOptions());
const filteredThreads = (threads ?? []).filter((thread) =>
matchesSearch(search, [thread.title, thread.resumeName, thread.coverLetterName]),
);
// A conversation opens its document with the assistant showing it.
const onOpenThread = async (thread: Thread) => {
if (thread.coverLetterId)
await navigate({
to: "/builder/letter/$coverLetterId",
params: { coverLetterId: thread.coverLetterId },
search: { assistant: thread.id },
});
else if (thread.workingResumeId)
await navigate({
to: "/builder/$resumeId",
params: { resumeId: thread.workingResumeId },
search: { assistant: thread.id },
});
reset();
};
return (
<BaseCommandGroup page={page} heading={<Trans>Assistant conversations</Trans>}>
{isLoading ? (
<CommandLoading>
<Trans>Loading conversations…</Trans>
</CommandLoading>
) : (
filteredThreads.map((thread) => {
const documentName = thread.coverLetterName ?? thread.resumeName ?? "";
return (
<CommandItem
key={thread.id}
value={`thread.${thread.id}`}
keywords={[thread.title, documentName]}
onSelect={() => onOpenThread(thread)}
>
<Icon name="chat" size={16} />
<span className="min-w-0 truncate">{thread.title}</span>
<span className="truncate text-ink-3 text-xs">{documentName}</span>
</CommandItem>
);
})
)}
</BaseCommandGroup>
);
}
@@ -6,6 +6,21 @@ import { useEffect, useId, useRef, useState } from "react";
const focusRing = "focus-visible:outline-2 focus-visible:outline-(--home-accent) focus-visible:outline-offset-4";
const message = "max-w-[300px] text-[14px] leading-[1.8]";
/** The extracted text of the chosen file, or of the bundled sample; null when the read was cancelled midway. */
async function readPdf(selected: File | null, signal: AbortSignal) {
let source = selected;
if (!source) {
const response = await fetch("/templates/pdf/rhyhorn.pdf", { signal });
if (!response.ok) throw new Error("Sample PDF unavailable");
source = new File([await response.blob()], "rhyhorn.pdf", { type: "application/pdf" });
}
if (signal.aborted) return null;
const { runAtsCheck } = await import("@/features/ats-checker/run-ats-check");
if (signal.aborted) return null;
const result = await runAtsCheck(source, { signal });
return signal.aborted ? null : result.fullText;
}
export default function AtsPlayground() {
const inputId = useId();
const controllerRef = useRef<AbortController | null>(null);
@@ -38,18 +53,9 @@ export default function AtsPlayground() {
}
setPhase("reading");
try {
let source = selected;
if (!source) {
const response = await fetch("/templates/pdf/rhyhorn.pdf", { signal: controller.signal });
if (!response.ok) throw new Error("Sample PDF unavailable");
source = new File([await response.blob()], "rhyhorn.pdf", { type: "application/pdf" });
}
if (controller.signal.aborted) return;
const { runAtsCheck } = await import("@/features/ats-checker/run-ats-check");
if (controller.signal.aborted) return;
const result = await runAtsCheck(source, { signal: controller.signal });
if (controller.signal.aborted) return;
setText(result.fullText);
const fullText = await readPdf(selected, controller.signal);
if (fullText === null) return;
setText(fullText);
setPhase("done");
} catch (caught) {
if (controller.signal.aborted) return;
@@ -164,14 +164,14 @@ export default function ExportPlayground({ name, accent, typeface, template }: E
]);
const download = async () => {
const action = actions.get(format);
setBusy(true);
try {
await actions.get(format)?.();
if (action) await action();
} catch {
toast.add({ type: "error", description: t`Could not prepare the sample. Please try again.` });
} finally {
setBusy(false);
}
setBusy(false);
};
return (
@@ -41,19 +41,37 @@ export function FeatureExplorer() {
const ref = useRef<HTMLDivElement>(null);
const visible = useInView(ref);
const reducedMotion = useReducedMotion();
const revisedText = t`Designed a shared component library that six product teams use to build consistent interfaces.`;
const sharingOptions = [
{ id: "public", label: t`Public` },
{ id: "password", label: t`Password` },
{ id: "private", label: t`Private` },
];
// State lives here so a demo keeps its place when you switch away and back.
const features = [
{ id: "writing", label: t`A hand with the writing`, hint: t`AI, when you want it`, icon: PencilSimpleIcon },
{ id: "sharing", label: t`A link worth sharing`, hint: t`Choose who can see it`, icon: GlobeIcon },
{ id: "tracking", label: t`Know where things stand`, hint: t`Your applications, together`, icon: KanbanIcon },
{ id: "tools", label: t`Fits into your workflow`, hint: t`API and MCP access`, icon: CodeIcon },
{
id: "writing",
label: t`A hand with the writing`,
hint: t`AI, when you want it`,
icon: PencilSimpleIcon,
demo: <WritingDemo accepted={accepted} onAcceptedChange={setAccepted} />,
},
{
id: "sharing",
label: t`A link worth sharing`,
hint: t`Choose who can see it`,
icon: GlobeIcon,
demo: <SharingDemo sharing={sharing} onSharingChange={setSharing} />,
},
{
id: "tracking",
label: t`Know where things stand`,
hint: t`Your applications, together`,
icon: KanbanIcon,
demo: <TrackingDemo stage={stage} onStageChange={setStage} />,
},
{
id: "tools",
label: t`Fits into your workflow`,
hint: t`API and MCP access`,
icon: CodeIcon,
demo: <ToolsDemo playing={flowPlaying} onPlayingChange={setFlowPlaying} visible={visible} />,
},
];
const stages = [t`Applied`, t`Interview`, t`Offer`];
return (
<div
ref={ref}
@@ -91,285 +109,306 @@ export function FeatureExplorer() {
))}
</fieldset>
<div className="min-h-[492px] overflow-hidden rounded-[4px] border border-(--home-line) bg-[#19191b]">
{feature === "writing" && (
<div className={demoClass}>
<div className={demoTopClass}>
<span>
<Trans>Example edit</Trans>
</span>
<span>Alex Morgan</span>
</div>
<div className="transform-[perspective(1000px)_rotateX(3deg)_rotateY(-3deg)] mx-auto mt-[25px] min-h-[224px] max-w-[430px] rounded-[2px] border border-[#d9d6d0] bg-[#efeee9] px-[30px] pt-7 pb-[38px] text-[#343239] shadow-[0_10px_18px_#0002] max-[540px]:px-6 max-[540px]:pt-[23px] max-[540px]:pb-8">
<span className="text-[#735c9a] text-[11px]">
<Trans>Experience</Trans>
</span>
<h3 className="mt-[9px] font-[Manrope_Variable,sans-serif] font-semibold text-[18px] leading-[1.3] tracking-[-0.03em]">
<Trans>Product designer</Trans>
</h3>
<p
className={cn(
"-mx-[5px] mt-3 rounded-[2px] px-[5px] py-1 text-[12px] leading-[1.8]",
accepted && "bg-[#d6e5d6]",
)}
>
{accepted ? (
<>
<span className="sr-only" role="status">
{revisedText}
</span>
<span aria-hidden="true">
{revisedText.split(" ").map((word, index) => (
<span
key={`${index}-${word}`}
className="animate-[home-word-arrive_180ms_ease-out_both] group-data-[instant=true]/explorer:animate-none"
style={{ animationDelay: `${index * 45}ms` }}
>
{word}{" "}
</span>
))}
</span>
</>
) : (
t`Responsible for working on the design system and helping other teams with their designs.`
)}
</p>
<div className="mt-[15px] h-[3px] w-[95%] bg-[#dad8d4]" />
<div className="mt-[7px] h-[3px] w-[66%] bg-[#dad8d4]" />
</div>
<div className="relative -mt-4 flex items-start gap-3 rounded-[4px] border border-[#514b59] bg-[#2c2732] px-[17px] py-[19px] shadow-[0_10px_20px_#0003] max-[1100px]:flex-wrap max-[540px]:flex-wrap max-[900px]:flex-nowrap max-[540px]:gap-[9px] max-[540px]:px-3 max-[540px]:py-4">
<PencilSimpleIcon size={20} aria-hidden="true" className="mt-[2px] shrink-0 text-[#c9b7dc]" />
<div className="max-[540px]:flex-[1_0_calc(100%_-_30px)]">
<strong className="block font-medium text-[13px]">
{accepted ? t`Your edit is in.` : t`Say what you actually did.`}
</strong>
<p className="mt-[5px] max-w-[250px] text-[#b8b0c0] text-[12px]">
{accepted
? t`You can always go back to the original.`
: t`Lead with the work. Add a concrete detail so someone can picture it.`}
</p>
</div>
<button
type="button"
className={`ml-auto flex min-h-11 shrink-0 items-center gap-[9px] rounded-[4px] border border-[#655971] bg-[#3e3449] px-2.5 py-2 text-[#efebf2] text-[12px] ${pressTransition} max-[1100px]:ml-[31px] max-[540px]:ml-[29px] max-[900px]:ml-auto`}
onClick={() => setAccepted(!accepted)}
>
{accepted ? t`Undo` : t`Use this wording`}
{accepted ? (
<CheckIcon size={16} aria-hidden="true" />
) : (
<ArrowRightIcon size={16} aria-hidden="true" />
)}
</button>
</div>
<p className={demoCaptionClass}>
<Trans>
This is a sample edit. In the app, connect your own AI provider. Turn on Review edits to approve changes
before they’re applied.
</Trans>
</p>
</div>
)}
{feature === "sharing" && (
<div className={demoClass}>
<div className={demoTopClass}>
<span>
<Trans>Sharing preview</Trans>
</span>
<GlobeIcon size={18} aria-hidden="true" />
</div>
<div className="mt-[25px] mb-5 flex justify-center gap-[3px]">
{sharingOptions.map(({ id, label }) => (
<button
type="button"
key={id}
className={`min-h-11 rounded-[4px] border border-transparent bg-transparent px-[18px] py-2 text-(--home-muted) text-[13px] ${pressTransition} aria-pressed:border-[#47474a] aria-pressed:bg-[#323235] aria-pressed:text-(--home-ink) group-data-[instant=true]/explorer:transition-none`}
aria-pressed={sharing === id}
onClick={() => setSharing(id)}
>
{label}
</button>
))}
</div>
<div className="m-auto max-w-[430px] overflow-hidden rounded-[4px] border border-[#404043] shadow-[0_12px_30px_#0002]">
<div className="flex items-center justify-center gap-2 bg-[#252527] p-2.5 text-[#939399] text-[10px]">
<LockSimpleIcon size={13} aria-hidden="true" />
<Trans>Your resume link</Trans>
</div>
<div className="grid bg-[#202022]">
{sharingOptions.map(({ id }) => (
<div
key={id}
className="invisible col-start-1 row-start-1 flex min-h-[215px] flex-col items-center justify-center p-6 text-center opacity-0 [transition:opacity_180ms_ease,visibility_180ms] data-[active=true]:visible data-[active=true]:opacity-100 group-data-[instant=true]/explorer:transition-none"
data-active={sharing === id}
aria-hidden={sharing !== id}
>
{id === "public" ? (
<>
<div className="grid size-11 place-items-center rounded-full bg-[#45382e] text-[#decfbf] text-[12px]">
AM
</div>
<h3 className="mt-3 font-medium text-[19px] tracking-[-0.03em]">Alex Morgan</h3>
<p className="mt-1.5 text-(--home-muted) text-[12px]">
<Trans>Product designer</Trans>
</p>
<div className="mt-[15px] grid w-[110px] gap-[5px]">
<i className="h-[2px] bg-[#444447]" />
<i className="h-[2px] bg-[#444447]" />
<i className="h-[2px] w-[70%] bg-[#444447]" />
</div>
</>
) : (
<>
<LockSimpleIcon size={34} weight="light" aria-hidden="true" />
<h3 className="mt-3 font-medium text-[19px] tracking-[-0.03em]">
{id === "password" ? t`Password required` : t`Resume not found`}
</h3>
<p className="mt-1.5 text-(--home-muted) text-[12px]">
{id === "password"
? t`Visitors need your password to open the resume.`
: t`A private resume does not appear at its public link.`}
</p>
</>
)}
</div>
))}
</div>
</div>
<p className={demoCaptionClass}>
<Trans>
Try the settings to see what visitors can access. When you create your resume, you decide how to share
it.
</Trans>
</p>
</div>
)}
{feature === "tracking" && (
<div className={demoClass}>
<div className={demoTopClass}>
<span>
<Trans>Example application</Trans>
</span>
<button
type="button"
className="-my-1.5 min-h-8 px-2 py-1 text-(--home-muted)"
onClick={() => setStage(0)}
>
<Trans>Reset</Trans>
</button>
</div>
<div className="mt-[34px] [--home-board-gap:12px] max-[540px]:[--home-board-gap:7px]">
<div className="grid grid-cols-3 gap-(--home-board-gap)">
{stages.map((label, index) => (
<div
key={label}
className="flex items-center gap-1.5 text-(--home-muted) text-[11px] max-[540px]:gap-1 max-[540px]:text-[9px]"
>
<i className={cn("size-1.5 rounded-full", stageDotColors[index])} />
{label}
<span className="ml-auto text-[#85858c]">{stage === index ? 1 : 0}</span>
</div>
))}
</div>
<div
className="relative mt-[15px] grid grid-cols-3 gap-(--home-board-gap)"
style={{ "--stage": stage } as CSSProperties}
>
{stages.map((label) => (
<div
key={label}
className={`${jobCardClass} border-[#323237] border-dashed text-transparent`}
aria-hidden="true"
/>
))}
<div
className={`${jobCardClass} transform-[translateX(calc(var(--stage)_*_(100%_+_var(--home-board-gap))))_rotate(calc(var(--stage)_*_3deg_-_2deg))] absolute top-0 left-0 w-[calc((100%_-_2_*_var(--home-board-gap))_/_3)] border-[#4b4b51] bg-[#2a2a2e] text-(--home-ink) shadow-[0_5px_12px_#0002] [transition:transform_460ms_var(--ease-out-strong)] group-data-[instant=true]/explorer:transition-none`}
>
<div className="mb-4 grid size-[30px] place-items-center rounded-[4px] bg-[#d8c4ac] font-[Georgia,serif] font-semibold text-[#34302a] text-[19px] leading-none">
n.
</div>
<strong className="block font-medium text-[12px] max-[540px]:text-[10px]">
<Trans>Product designer</Trans>
</strong>
<p className="mt-[5px] text-(--home-muted) text-[10px] max-[540px]:text-[9px]">Northstar Studio</p>
<span className="mt-[18px] block text-[#bab4c7] text-[9px] max-[540px]:text-[8px]">
<Trans>Resume attached</Trans>
</span>
</div>
</div>
</div>
<div className="mt-7 flex items-center justify-between text-(--home-muted) text-[12px]">
<span role="status">{stages[stage]}</span>
<button
type="button"
className={`inline-flex min-h-11 items-center gap-2.5 rounded-[4px] border border-[#4b4b51] bg-[#2b2b2e] px-[13px] py-2 text-(--home-ink) text-[12px] ${pressTransition}`}
onClick={() => setStage((stage + 1) % stages.length)}
>
{stage === 0 ? t`Move to interview` : stage === 1 ? t`Move to offer` : t`Start again`}
<ArrowRightIcon size={17} aria-hidden="true" />
</button>
</div>
<p className={demoCaptionClass}>
<Trans>
Keep the company, role, resume, and follow-up date together. Switch between a board, table, and insights
in the app.
</Trans>
</p>
</div>
)}
{feature === "tools" && (
<div className={demoClass}>
<div className={demoTopClass}>
<span>
<Trans>Your tools, connected</Trans>
</span>
{!reducedMotion && (
<button
type="button"
className={`-my-3 grid min-h-11 min-w-11 place-items-center rounded-[4px] px-2 py-1 text-(--home-muted) ${pressTransition} hover:bg-[#323235]`}
aria-label={flowPlaying ? t`Pause data flow animation` : t`Play data flow animation`}
onClick={() => setFlowPlaying(!flowPlaying)}
>
{flowPlaying ? <PauseIcon size={16} aria-hidden="true" /> : <PlayIcon size={16} aria-hidden="true" />}
</button>
)}
</div>
<div className="mx-auto mt-[55px] mb-[38px] flex max-w-[410px] items-center">
<div className={toolBoxClass}>
<CodeIcon size={32} weight="light" aria-hidden="true" />
<span className={toolLabelClass}>
<Trans>Your AI app</Trans>
</span>
</div>
<div className="group/wire relative h-px flex-1 bg-[#66666d]" data-playing={flowPlaying && visible}>
<span className={`absolute -top-[25px] w-full text-center text-(--home-muted) ${toolLabelClass}`}>
MCP
</span>
<i
className="absolute inset-x-0 -top-[3px] h-[7px] animate-[home-data-flow_3.5s_cubic-bezier(0.45,0,0.55,1)_infinite_alternate_paused] after:-ml-[3px] after:block after:size-[7px] after:rounded-full after:bg-[#ddc8eb] after:shadow-[0_0_0_4px_#c5a4db14,0_0_11px_#c5a4db50] after:content-[''] group-data-[playing=true]/wire:[animation-play-state:running]"
aria-hidden="true"
/>
</div>
<div className={toolBoxClass}>
<img src="/icon/dark.svg" width="38" height="38" alt="" />
<span className={toolLabelClass}>Reactive Resume</span>
</div>
</div>
<h3 className="font-[Manrope_Variable,sans-serif] font-medium text-[23px] leading-[1.3] tracking-[-0.035em] max-[540px]:text-[22px]">
<Trans>Work on your resume from your AI app.</Trans>
</h3>
<p className="mt-[15px] text-(--home-muted) text-[14px] leading-[1.7]">
<Trans>
Connect through MCP to create and update resumes in a conversation. Or use the API to build your own
workflow.
</Trans>
</p>
<a href="https://docs.rxresu.me/guides/using-the-mcp-server" className={cn(textLink, "mt-4")}>
<Trans>See how to connect</Trans>
<ArrowUpRightIcon size={17} aria-hidden="true" />
</a>
</div>
)}
{features.find((item) => item.id === feature)?.demo}
</div>
</div>
);
}
type WritingDemoProps = { accepted: boolean; onAcceptedChange: (accepted: boolean) => void };
function WritingDemo({ accepted, onAcceptedChange }: WritingDemoProps) {
const revisedText = t`Designed a shared component library that six product teams use to build consistent interfaces.`;
return (
<div className={demoClass}>
<div className={demoTopClass}>
<span>
<Trans>Example edit</Trans>
</span>
<span>Alex Morgan</span>
</div>
<div className="transform-[perspective(1000px)_rotateX(3deg)_rotateY(-3deg)] mx-auto mt-[25px] min-h-[224px] max-w-[430px] rounded-[2px] border border-[#d9d6d0] bg-[#efeee9] px-[30px] pt-7 pb-[38px] text-[#343239] shadow-[0_10px_18px_#0002] max-[540px]:px-6 max-[540px]:pt-[23px] max-[540px]:pb-8">
<span className="text-[#735c9a] text-[11px]">
<Trans>Experience</Trans>
</span>
<h3 className="mt-[9px] font-[Manrope_Variable,sans-serif] font-semibold text-[18px] leading-[1.3] tracking-[-0.03em]">
<Trans>Product designer</Trans>
</h3>
<p
className={cn(
"-mx-[5px] mt-3 rounded-[2px] px-[5px] py-1 text-[12px] leading-[1.8]",
accepted && "bg-[#d6e5d6]",
)}
>
{accepted ? (
<>
<span className="sr-only" role="status">
{revisedText}
</span>
<span aria-hidden="true">
{revisedText.split(" ").map((word, index) => (
<span
key={`${index}-${word}`}
className="animate-[home-word-arrive_180ms_ease-out_both] group-data-[instant=true]/explorer:animate-none"
style={{ animationDelay: `${index * 45}ms` }}
>
{word}{" "}
</span>
))}
</span>
</>
) : (
t`Responsible for working on the design system and helping other teams with their designs.`
)}
</p>
<div className="mt-[15px] h-[3px] w-[95%] bg-[#dad8d4]" />
<div className="mt-[7px] h-[3px] w-[66%] bg-[#dad8d4]" />
</div>
<div className="relative -mt-4 flex items-start gap-3 rounded-[4px] border border-[#514b59] bg-[#2c2732] px-[17px] py-[19px] shadow-[0_10px_20px_#0003] max-[1100px]:flex-wrap max-[540px]:flex-wrap max-[900px]:flex-nowrap max-[540px]:gap-[9px] max-[540px]:px-3 max-[540px]:py-4">
<PencilSimpleIcon size={20} aria-hidden="true" className="mt-[2px] shrink-0 text-[#c9b7dc]" />
<div className="max-[540px]:flex-[1_0_calc(100%_-_30px)]">
<strong className="block font-medium text-[13px]">
{accepted ? t`Your edit is in.` : t`Say what you actually did.`}
</strong>
<p className="mt-[5px] max-w-[250px] text-[#b8b0c0] text-[12px]">
{accepted
? t`You can always go back to the original.`
: t`Lead with the work. Add a concrete detail so someone can picture it.`}
</p>
</div>
<button
type="button"
className={`ml-auto flex min-h-11 shrink-0 items-center gap-[9px] rounded-[4px] border border-[#655971] bg-[#3e3449] px-2.5 py-2 text-[#efebf2] text-[12px] ${pressTransition} max-[1100px]:ml-[31px] max-[540px]:ml-[29px] max-[900px]:ml-auto`}
onClick={() => onAcceptedChange(!accepted)}
>
{accepted ? t`Undo` : t`Use this wording`}
{accepted ? <CheckIcon size={16} aria-hidden="true" /> : <ArrowRightIcon size={16} aria-hidden="true" />}
</button>
</div>
<p className={demoCaptionClass}>
<Trans>
This is a sample edit. In the app, connect your own AI provider. Turn on Review edits to approve changes
before they’re applied.
</Trans>
</p>
</div>
);
}
type SharingDemoProps = { sharing: string; onSharingChange: (sharing: string) => void };
function SharingDemo({ sharing, onSharingChange }: SharingDemoProps) {
const sharingOptions = [
{ id: "public", label: t`Public` },
{ id: "password", label: t`Password` },
{ id: "private", label: t`Private` },
];
return (
<div className={demoClass}>
<div className={demoTopClass}>
<span>
<Trans>Sharing preview</Trans>
</span>
<GlobeIcon size={18} aria-hidden="true" />
</div>
<div className="mt-[25px] mb-5 flex justify-center gap-[3px]">
{sharingOptions.map(({ id, label }) => (
<button
type="button"
key={id}
className={`min-h-11 rounded-[4px] border border-transparent bg-transparent px-[18px] py-2 text-(--home-muted) text-[13px] ${pressTransition} aria-pressed:border-[#47474a] aria-pressed:bg-[#323235] aria-pressed:text-(--home-ink) group-data-[instant=true]/explorer:transition-none`}
aria-pressed={sharing === id}
onClick={() => onSharingChange(id)}
>
{label}
</button>
))}
</div>
<div className="m-auto max-w-[430px] overflow-hidden rounded-[4px] border border-[#404043] shadow-[0_12px_30px_#0002]">
<div className="flex items-center justify-center gap-2 bg-[#252527] p-2.5 text-[#939399] text-[10px]">
<LockSimpleIcon size={13} aria-hidden="true" />
<Trans>Your resume link</Trans>
</div>
<div className="grid bg-[#202022]">
{sharingOptions.map(({ id }) => (
<div
key={id}
className="invisible col-start-1 row-start-1 flex min-h-[215px] flex-col items-center justify-center p-6 text-center opacity-0 [transition:opacity_180ms_ease,visibility_180ms] data-[active=true]:visible data-[active=true]:opacity-100 group-data-[instant=true]/explorer:transition-none"
data-active={sharing === id}
aria-hidden={sharing !== id}
>
{id === "public" ? (
<>
<div className="grid size-11 place-items-center rounded-full bg-[#45382e] text-[#decfbf] text-[12px]">
AM
</div>
<h3 className="mt-3 font-medium text-[19px] tracking-[-0.03em]">Alex Morgan</h3>
<p className="mt-1.5 text-(--home-muted) text-[12px]">
<Trans>Product designer</Trans>
</p>
<div className="mt-[15px] grid w-[110px] gap-[5px]">
<i className="h-[2px] bg-[#444447]" />
<i className="h-[2px] bg-[#444447]" />
<i className="h-[2px] w-[70%] bg-[#444447]" />
</div>
</>
) : (
<>
<LockSimpleIcon size={34} weight="light" aria-hidden="true" />
<h3 className="mt-3 font-medium text-[19px] tracking-[-0.03em]">
{id === "password" ? t`Password required` : t`Resume not found`}
</h3>
<p className="mt-1.5 text-(--home-muted) text-[12px]">
{id === "password"
? t`Visitors need your password to open the resume.`
: t`A private resume does not appear at its public link.`}
</p>
</>
)}
</div>
))}
</div>
</div>
<p className={demoCaptionClass}>
<Trans>
Try the settings to see what visitors can access. When you create your resume, you decide how to share it.
</Trans>
</p>
</div>
);
}
type TrackingDemoProps = { stage: number; onStageChange: (stage: number) => void };
function TrackingDemo({ stage, onStageChange }: TrackingDemoProps) {
const stages = [t`Applied`, t`Interview`, t`Offer`];
return (
<div className={demoClass}>
<div className={demoTopClass}>
<span>
<Trans>Example application</Trans>
</span>
<button
type="button"
className="-my-1.5 min-h-8 px-2 py-1 text-(--home-muted)"
onClick={() => onStageChange(0)}
>
<Trans>Reset</Trans>
</button>
</div>
<div className="mt-[34px] [--home-board-gap:12px] max-[540px]:[--home-board-gap:7px]">
<div className="grid grid-cols-3 gap-(--home-board-gap)">
{stages.map((label, index) => (
<div
key={label}
className="flex items-center gap-1.5 text-(--home-muted) text-[11px] max-[540px]:gap-1 max-[540px]:text-[9px]"
>
<i className={cn("size-1.5 rounded-full", stageDotColors[index])} />
{label}
<span className="ml-auto text-[#85858c]">{stage === index ? 1 : 0}</span>
</div>
))}
</div>
<div
className="relative mt-[15px] grid grid-cols-3 gap-(--home-board-gap)"
style={{ "--stage": stage } as CSSProperties}
>
{stages.map((label) => (
<div
key={label}
className={`${jobCardClass} border-[#323237] border-dashed text-transparent`}
aria-hidden="true"
/>
))}
<div
className={`${jobCardClass} transform-[translateX(calc(var(--stage)_*_(100%_+_var(--home-board-gap))))_rotate(calc(var(--stage)_*_3deg_-_2deg))] absolute top-0 left-0 w-[calc((100%_-_2_*_var(--home-board-gap))_/_3)] border-[#4b4b51] bg-[#2a2a2e] text-(--home-ink) shadow-[0_5px_12px_#0002] [transition:transform_460ms_var(--ease-out-strong)] group-data-[instant=true]/explorer:transition-none`}
>
<div className="mb-4 grid size-[30px] place-items-center rounded-[4px] bg-[#d8c4ac] font-[Georgia,serif] font-semibold text-[#34302a] text-[19px] leading-none">
n.
</div>
<strong className="block font-medium text-[12px] max-[540px]:text-[10px]">
<Trans>Product designer</Trans>
</strong>
<p className="mt-[5px] text-(--home-muted) text-[10px] max-[540px]:text-[9px]">Northstar Studio</p>
<span className="mt-[18px] block text-[#bab4c7] text-[9px] max-[540px]:text-[8px]">
<Trans>Resume attached</Trans>
</span>
</div>
</div>
</div>
<div className="mt-7 flex items-center justify-between text-(--home-muted) text-[12px]">
<span role="status">{stages[stage]}</span>
<button
type="button"
className={`inline-flex min-h-11 items-center gap-2.5 rounded-[4px] border border-[#4b4b51] bg-[#2b2b2e] px-[13px] py-2 text-(--home-ink) text-[12px] ${pressTransition}`}
onClick={() => onStageChange((stage + 1) % stages.length)}
>
{stage === 0 ? t`Move to interview` : stage === 1 ? t`Move to offer` : t`Start again`}
<ArrowRightIcon size={17} aria-hidden="true" />
</button>
</div>
<p className={demoCaptionClass}>
<Trans>
Keep the company, role, resume, and follow-up date together. Switch between a board, table, and insights in
the app.
</Trans>
</p>
</div>
);
}
type ToolsDemoProps = { playing: boolean; onPlayingChange: (playing: boolean) => void; visible: boolean };
function ToolsDemo({ playing, onPlayingChange, visible }: ToolsDemoProps) {
const reducedMotion = useReducedMotion();
return (
<div className={demoClass}>
<div className={demoTopClass}>
<span>
<Trans>Your tools, connected</Trans>
</span>
{!reducedMotion && (
<button
type="button"
className={`-my-3 grid min-h-11 min-w-11 place-items-center rounded-[4px] px-2 py-1 text-(--home-muted) ${pressTransition} hover:bg-[#323235]`}
aria-label={playing ? t`Pause data flow animation` : t`Play data flow animation`}
onClick={() => onPlayingChange(!playing)}
>
{playing ? <PauseIcon size={16} aria-hidden="true" /> : <PlayIcon size={16} aria-hidden="true" />}
</button>
)}
</div>
<div className="mx-auto mt-[55px] mb-[38px] flex max-w-[410px] items-center">
<div className={toolBoxClass}>
<CodeIcon size={32} weight="light" aria-hidden="true" />
<span className={toolLabelClass}>
<Trans>Your AI app</Trans>
</span>
</div>
<div className="group/wire relative h-px flex-1 bg-[#66666d]" data-playing={playing && visible}>
<span className={`absolute -top-[25px] w-full text-center text-(--home-muted) ${toolLabelClass}`}>MCP</span>
<i
className="absolute inset-x-0 -top-[3px] h-[7px] animate-[home-data-flow_3.5s_cubic-bezier(0.45,0,0.55,1)_infinite_alternate_paused] after:-ml-[3px] after:block after:size-[7px] after:rounded-full after:bg-[#ddc8eb] after:shadow-[0_0_0_4px_#c5a4db14,0_0_11px_#c5a4db50] after:content-[''] group-data-[playing=true]/wire:[animation-play-state:running]"
aria-hidden="true"
/>
</div>
<div className={toolBoxClass}>
<img src="/icon/dark.svg" width="38" height="38" alt="" />
<span className={toolLabelClass}>Reactive Resume</span>
</div>
</div>
<h3 className="font-[Manrope_Variable,sans-serif] font-medium text-[23px] leading-[1.3] tracking-[-0.035em] max-[540px]:text-[22px]">
<Trans>Work on your resume from your AI app.</Trans>
</h3>
<p className="mt-[15px] text-(--home-muted) text-[14px] leading-[1.7]">
<Trans>
Connect through MCP to create and update resumes in a conversation. Or use the API to build your own workflow.
</Trans>
</p>
<a href="https://docs.rxresu.me/guides/using-the-mcp-server" className={cn(textLink, "mt-4")}>
<Trans>See how to connect</Trans>
<ArrowUpRightIcon size={17} aria-hidden="true" />
</a>
</div>
);
}
@@ -1,7 +1,7 @@
import type { CSSProperties, PointerEvent } from "react";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { m, useMotionTemplate, useReducedMotion, useSpring } from "motion/react";
import { m, useReducedMotion, useSpring } from "motion/react";
import { useId, useRef, useState } from "react";
import { cn } from "@reactive-resume/utils/style";
import "./resume-sculpture.css";
@@ -60,7 +60,6 @@ export function ResumeSculpture({
const reducedMotion = useReducedMotion();
const rotateX = useSpring(10, { stiffness: 180, damping: 26 });
const rotateY = useSpring(-19, { stiffness: 180, damping: 26 });
const transform = useMotionTemplate`rotateX(${rotateX}deg) rotateY(${rotateY}deg) rotateZ(-7deg)`;
function startDrag(event: PointerEvent<HTMLDivElement>) {
if (event.pointerType === "touch" || event.button !== 0 || dragOrigin.current || reducedMotion) return;
@@ -114,7 +113,7 @@ export function ResumeSculpture({
className="sculpture-rig group/rig transform-3d absolute inset-0"
data-spread={spread}
data-instant={keyboardAction}
style={{ transform }}
style={{ rotateX, rotateY, rotateZ: -7 }}
>
{templates.map((paperTemplate, index) => {
const position = (index - templates.indexOf(template) + templates.length) % templates.length;