Squashed commit of the following:

commit b2b0470a1d9267d042ec0ac66523c6635bf5b199
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 13:13:38 2026 +0200

    chore: update .gitignore to include .vite-hooks and modify pnpm-lock.yaml for dependencies

commit d28fadb5cd8706c874e616102878b4a394ec84c1
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 13:08:04 2026 +0200

    fix: remove timestamp conflict guard

commit c6998d9dbab19d09d3c8054feef1d2e4117555eb
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 12:11:51 2026 +0200

    chore(release): v5.1.5

commit f33d168711804880e1f12e88d24290aae16cc258
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 11:58:35 2026 +0200

    revert: compose.yml

commit d961e6535811a10c335525fb33a08d03e737278d
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 11:58:08 2026 +0200

    refactor(agent): replace 'revert' terminology with 'restore' for clarity, resolves #3086

commit 17f351171be218e33f01c469d95e4164d4c8dc57
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 11:10:41 2026 +0200

    refactor(pdf): simplify sidebar section filtering and update summary feature logic

commit d55179b9d76879e3204de185e8b53fadd0a107ed
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 09:53:37 2026 +0200

    chore: update pnpm-lock.yaml and turbo.json

commit 7cade6980e1a04352536bd44ef773f338c4ef599
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 09:38:30 2026 +0200

    fix(polyfill): add tested polyfill for Map Upsert methods

commit 26d175bb9c53d93225d1e907678445252c13d660
Merge: 1cf33dc6c 5b1297fa2
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 09:23:29 2026 +0200

    Merge remote-tracking branch 'origin/main' into feat/explore-hono-orpc-migration

    # Conflicts:
    #	packages/api/src/services/agent-url.ts
    #	packages/runtime-externals/package.json

commit 1cf33dc6c9d81735730ad656e16dab6501c6d6a1
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 09:22:12 2026 +0200

    chore: preserve branch changes before main sync

commit b380a4b00fdbcdd81ff4f8ef72b330fd027ccda5
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Mon May 18 07:50:28 2026 +0200

    chore: lot of fixes for monorepo migration

commit 8fcf0ec64e1c29572ebaff494338368bfcf75760
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 13:57:17 2026 +0200

    chore: update knip version and refine web app routing with new SEO endpoints

commit 234e68086ff15610a93877354c98e2c020364533
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 12:10:06 2026 +0200

    refactor(auth): update OAuth routes to include API prefix and remove unused schema endpoint

commit 91c84b9a8496b0ce21d71cae9f8b2a027638c9ac
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:54:29 2026 +0200

    chore: update dependencies and enhance PWA metadata in web app

commit 150117d4a5a9dd6cd92c64891aad8cae90f6a7af
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:12:35 2026 +0200

    docs: revise manifest-only pwa testing scope

commit 6b939a55661aec9dd8122b184e4b60a5c7325fb5
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:11:33 2026 +0200

    docs: add manifest-only pwa design

commit 1422e1fc96c400948b273210a1067251087d15d4
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:05:04 2026 +0200

    chore(dev): simplify server proxy config

commit bc2ff5a9f6fda41e6c40333c8f163aa23a6c5e48
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:04:50 2026 +0200

    docs: add unsafe oauth redirect plan

commit 445359ebe9b96c1515bf1c4c3f73ba8a8448ec12
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:04:34 2026 +0200

    feat(auth): add unsafe oauth redirect flag

commit 73fffdd24598e56b2793f7657919bc794835892e
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 10:55:02 2026 +0200

    docs: design unsafe oauth redirect flag

commit c0066aa19c15fc8a4c8e5179ed49889c117519f4
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 10:22:04 2026 +0200

    chore: update translation source paths

commit 9033da082418d252aafd6c2eed72f71f014be3d9
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 10:09:25 2026 +0200

    refactor(arch): react spa + hono migration

commit 6f27936c11bda895977dc63ee550c3346d4ce24b
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 01:10:47 2026 +0200

    docs: add docker nightly tagging design

commit ecc1fd9a88a0ee1dca2f1977dfc17f74527fe1da
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Thu May 14 20:05:44 2026 +0200

    feat: migrate to hono spa server
This commit is contained in:
Amruth Pillai
2026-05-19 13:14:21 +02:00
parent 5b1297fa2b
commit 62f8270b3e
518 changed files with 29398 additions and 26871 deletions
+3 -3
View File
@@ -3,11 +3,12 @@ import type { RouterOutput } from "@/libs/orpc/client";
import { ORPCError } from "@orpc/client";
import { createFileRoute, lazyRouteComponent, notFound, redirect } from "@tanstack/react-router";
import { orpc } from "@/libs/orpc/client";
import { createNoindexFollowMeta } from "@/libs/seo";
type LoaderData = Omit<RouterOutput["resume"]["getBySlug"], "data"> & { data: ResumeData };
export const Route = createFileRoute("/$username/$slug")({
component: lazyRouteComponent(() => import("./-components/public-resume"), "PublicResumeRoute"),
component: lazyRouteComponent(() => import("@/features/resume/public/public-resume"), "PublicResumeRoute"),
loader: async ({ context, params }) => {
const { username, slug } = params;
const resume = await context.queryClient.ensureQueryData(
@@ -19,7 +20,7 @@ export const Route = createFileRoute("/$username/$slug")({
head: ({ loaderData }) => {
const resume = loaderData?.resume;
const title = resume ? resume.name || resume.data.basics.name || "Resume" : "Reactive Resume";
return { meta: [{ title: `${title} - Reactive Resume` }] };
return { meta: [{ title: `${title} - Reactive Resume` }, createNoindexFollowMeta()] };
},
onError: (error) => {
if (error instanceof ORPCError && error.code === "NEED_PASSWORD") {
@@ -37,5 +38,4 @@ export const Route = createFileRoute("/$username/$slug")({
throw notFound();
},
ssr: "data-only",
});
@@ -1,11 +0,0 @@
.pdf-viewer .pdfViewer {
padding-block: 0;
}
.pdf-viewer .pdfViewer .page {
margin-block: 0 1rem;
}
.pdf-viewer .pdfViewer .page:last-child {
margin-bottom: 0;
}
@@ -1,119 +0,0 @@
// @vitest-environment happy-dom
import { render, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { sampleResumeData } from "@reactive-resume/schema/resume/sample";
const pdfViewerMock = vi.hoisted(() => {
const pdfDocument = {
destroy: vi.fn(),
numPages: 1,
};
const loadingTask = {
destroy: vi.fn(),
promise: Promise.resolve(pdfDocument),
};
return {
constructorOptions: [] as Array<{ abortSignal?: AbortSignal; container: HTMLDivElement }>,
createResumePdfBlob: vi.fn(async () => new Blob(["%PDF"], { type: "application/pdf" })),
getDocument: vi.fn(() => loadingTask),
instances: [] as Array<{
abortSignal?: AbortSignal;
setDocument: ReturnType<typeof vi.fn>;
update: ReturnType<typeof vi.fn>;
}>,
loadingTask,
pdfDocument,
};
});
vi.mock("@/libs/resume/pdf-document", () => ({
createResumePdfBlob: pdfViewerMock.createResumePdfBlob,
}));
vi.mock("pdfjs-dist/legacy/build/pdf.mjs", () => ({
AnnotationMode: { ENABLE_FORMS: 2 },
getDocument: pdfViewerMock.getDocument,
GlobalWorkerOptions: {},
}));
vi.mock("pdfjs-dist/legacy/web/pdf_viewer.mjs", () => {
class EventBus {
private readonly listeners = new Map<string, Set<() => void>>();
on(name: string, listener: () => void) {
const listeners = this.listeners.get(name) ?? new Set<() => void>();
listeners.add(listener);
this.listeners.set(name, listeners);
}
off(name: string, listener: () => void) {
this.listeners.get(name)?.delete(listener);
}
dispatch(name: string) {
for (const listener of this.listeners.get(name) ?? []) listener();
}
}
class PDFLinkService {
setDocument = vi.fn();
setViewer = vi.fn();
}
class PDFViewer {
currentScaleValue = "";
setDocument: ReturnType<typeof vi.fn>;
update = vi.fn();
constructor(options: { abortSignal?: AbortSignal; container: HTMLDivElement; eventBus: EventBus }) {
pdfViewerMock.constructorOptions.push(options);
this.setDocument = vi.fn((document: unknown) => {
if (document) options.eventBus.dispatch("pagesinit");
});
pdfViewerMock.instances.push(this);
}
}
return {
EventBus,
LinkTarget: { BLANK: 2 },
PDFLinkService,
PDFViewer,
};
});
const { PdfViewer } = await import("./pdf-viewer");
beforeEach(() => {
pdfViewerMock.constructorOptions.length = 0;
pdfViewerMock.instances.length = 0;
pdfViewerMock.createResumePdfBlob.mockClear();
pdfViewerMock.getDocument.mockClear();
pdfViewerMock.loadingTask.destroy.mockClear();
pdfViewerMock.pdfDocument.destroy.mockClear();
});
describe("PdfViewer", () => {
it("uses PDF.js' absolute container contract and disposes viewer state", async () => {
const view = render(<PdfViewer data={sampleResumeData} />);
await waitFor(() => expect(pdfViewerMock.constructorOptions).toHaveLength(1));
const [{ abortSignal, container }] = pdfViewerMock.constructorOptions;
const [viewer] = pdfViewerMock.instances;
expect(container).toHaveClass("absolute", "inset-0", "overflow-visible");
expect(abortSignal).toBeInstanceOf(AbortSignal);
expect(viewer.setDocument).toHaveBeenCalledWith(pdfViewerMock.pdfDocument);
view.unmount();
expect(abortSignal?.aborted).toBe(true);
expect(viewer.setDocument).toHaveBeenCalledWith(null);
expect(pdfViewerMock.pdfDocument.destroy).toHaveBeenCalledTimes(1);
expect(pdfViewerMock.loadingTask.destroy).toHaveBeenCalledTimes(1);
});
});
@@ -1,190 +0,0 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { PDFDocumentLoadingTask, PDFDocumentProxy } from "pdfjs-dist";
import { AnnotationMode, GlobalWorkerOptions, getDocument } from "pdfjs-dist/legacy/build/pdf.mjs";
import { EventBus, LinkTarget, PDFLinkService, PDFViewer } from "pdfjs-dist/legacy/web/pdf_viewer.mjs";
import { useEffect, useRef, useState } from "react";
import { Spinner } from "@reactive-resume/ui/components/spinner";
import { cn } from "@reactive-resume/utils/style";
import { createResumePdfBlob } from "@/libs/resume/pdf-document";
import "pdfjs-dist/legacy/web/pdf_viewer.css";
import "./pdf-viewer.css";
GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/legacy/build/pdf.worker.min.mjs", import.meta.url).toString();
type PdfViewerProps = {
className?: string;
data: ResumeData;
};
type PdfViewerOptions = ConstructorParameters<typeof PDFViewer>[0] & {
abortSignal: AbortSignal;
};
const clearPdfViewerDocument = (pdfViewer: PDFViewer) => {
(pdfViewer.setDocument as (document: PDFDocumentProxy | null) => void)(null);
};
export function PdfViewer({ className, data }: PdfViewerProps) {
const rootRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const viewerRef = useRef<HTMLDivElement>(null);
const [file, setFile] = useState<Blob | null>(null);
const [error, setError] = useState(false);
const [isReady, setIsReady] = useState(false);
const [viewerHeight, setViewerHeight] = useState<number | null>(null);
useEffect(() => {
let isCancelled = false;
setError(false);
setFile(null);
setIsReady(false);
setViewerHeight(null);
void createResumePdfBlob(data)
.then((blob) => {
if (!isCancelled) setFile(blob);
})
.catch((error: unknown) => {
if (!isCancelled) {
console.error("Failed to generate public resume PDF", error);
setError(true);
}
});
return () => {
isCancelled = true;
};
}, [data]);
useEffect(() => {
const root = rootRef.current;
const container = containerRef.current;
const viewer = viewerRef.current;
if (!file || !root || !container || !viewer) return;
let isCancelled = false;
let animationFrameId = 0;
let resizeObserver: ResizeObserver | undefined;
const abortController = new AbortController();
let loadingTask: PDFDocumentLoadingTask | undefined;
let pdfDocument: PDFDocumentProxy | undefined;
let pdfViewer: PDFViewer | undefined;
const eventBus = new EventBus();
const linkService = new PDFLinkService({
eventBus,
externalLinkTarget: LinkTarget.BLANK,
externalLinkRel: "noreferrer",
});
const syncViewerHeight = () => {
if (isCancelled) return;
window.cancelAnimationFrame(animationFrameId);
animationFrameId = window.requestAnimationFrame(() => {
if (isCancelled) return;
const nextHeight = Math.ceil(viewer.scrollHeight);
if (nextHeight > 0) setViewerHeight(nextHeight);
pdfViewer?.update();
});
};
const setInitialScale = () => {
if (!isCancelled && pdfViewer) {
pdfViewer.currentScaleValue = "page-width";
syncViewerHeight();
}
};
eventBus.on("pagesinit", setInitialScale);
eventBus.on("pagesloaded", syncViewerHeight);
eventBus.on("pagerendered", syncViewerHeight);
viewer.replaceChildren();
setError(false);
setIsReady(false);
setViewerHeight(null);
resizeObserver = new ResizeObserver(syncViewerHeight);
resizeObserver.observe(viewer);
const loadDocument = async () => {
const arrayBuffer = await file.arrayBuffer();
if (isCancelled) return;
loadingTask = getDocument({
data: new Uint8Array(arrayBuffer),
docBaseUrl: window.location.href,
});
const nextDocument = await loadingTask.promise;
if (isCancelled) {
void nextDocument.destroy();
return;
}
pdfDocument = nextDocument;
const pdfViewerOptions = {
annotationMode: AnnotationMode.ENABLE_FORMS,
container,
eventBus,
linkService,
removePageBorders: true,
abortSignal: abortController.signal,
viewer,
} satisfies PdfViewerOptions;
pdfViewer = new PDFViewer(pdfViewerOptions);
linkService.setViewer(pdfViewer);
pdfViewer.setDocument(pdfDocument);
linkService.setDocument(pdfDocument);
syncViewerHeight();
setIsReady(true);
};
void loadDocument().catch((error: unknown) => {
if (!isCancelled) {
console.error("Failed to render public resume PDF with PDF.js", error);
setError(true);
}
});
return () => {
isCancelled = true;
eventBus.off("pagesinit", setInitialScale);
eventBus.off("pagesloaded", syncViewerHeight);
eventBus.off("pagerendered", syncViewerHeight);
abortController.abort();
window.cancelAnimationFrame(animationFrameId);
resizeObserver?.disconnect();
if (pdfViewer) clearPdfViewerDocument(pdfViewer);
void pdfDocument?.destroy();
void loadingTask?.destroy();
viewer.replaceChildren();
};
}, [file]);
return (
<div
ref={rootRef}
className={cn("pdf-viewer relative bg-neutral-100", viewerHeight ? "min-h-0" : "min-h-48", className)}
style={viewerHeight ? { height: viewerHeight } : undefined}
>
<div ref={containerRef} className="absolute inset-0 overflow-visible">
<div ref={viewerRef} className="pdfViewer" />
</div>
{error ? (
<div className="absolute inset-0 flex items-center justify-center bg-background px-6 text-center text-muted-foreground text-sm">
Unable to display PDF preview.
</div>
) : isReady ? null : (
<div className="absolute inset-0 flex items-center justify-center bg-background">
<Spinner className="size-6" />
</div>
)}
</div>
);
}
@@ -1,97 +0,0 @@
// @vitest-environment happy-dom
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { ReactNode } from "react";
import { render, screen } from "@testing-library/react";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { sampleResumeData } from "@reactive-resume/schema/resume/sample";
const publicResumeMock = vi.hoisted(() => ({
createResumePdfBlob: vi.fn(async () => new Blob(["%PDF"], { type: "application/pdf" })),
downloadWithAnchor: vi.fn(),
generateFilename: vi.fn((name: string, extension: string) => `${name}.${extension}`),
PdfViewer: vi.fn<(_props: { className?: string; data: ResumeData }) => ReactNode>(() => null),
resume: undefined as
| undefined
| {
data: ResumeData;
name: string;
slug: string;
},
}));
vi.mock("@tanstack/react-query", () => ({
useQuery: () => ({ data: publicResumeMock.resume }),
}));
vi.mock("@tanstack/react-router", () => ({
getRouteApi: () => ({
useParams: () => ({ username: "amruth", slug: "sample" }),
}),
}));
vi.mock("@reactive-resume/utils/file", () => ({
downloadWithAnchor: publicResumeMock.downloadWithAnchor,
generateFilename: publicResumeMock.generateFilename,
}));
vi.mock("./pdf-viewer", () => ({
PdfViewer: publicResumeMock.PdfViewer,
}));
vi.mock("@/libs/orpc/client", () => ({
orpc: { resume: { getBySlug: { queryOptions: () => ({}) } } },
}));
vi.mock("@/libs/resume/pdf-document", () => ({
createResumePdfBlob: publicResumeMock.createResumePdfBlob,
}));
const { PublicResumeRoute } = await import("./public-resume");
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
beforeEach(() => {
publicResumeMock.resume = {
data: sampleResumeData,
name: "Sample Resume",
slug: "sample",
};
publicResumeMock.PdfViewer.mockClear();
publicResumeMock.PdfViewer.mockImplementation(({ className }) => (
<div className={className} data-testid="pdf-viewer" />
));
});
const renderPublicResumeRoute = () =>
render(
<I18nProvider i18n={i18n}>
<PublicResumeRoute />
</I18nProvider>,
);
describe("PublicResumeRoute", () => {
it("renders the public resume through the route-local PDF.js viewer", () => {
renderPublicResumeRoute();
expect(screen.getByTestId("pdf-viewer")).toHaveClass("block", "w-full");
expect(publicResumeMock.PdfViewer).toHaveBeenCalledWith(
expect.objectContaining({ data: sampleResumeData }),
undefined,
);
});
it("lets the public resume page grow to the full PDF length", () => {
renderPublicResumeRoute();
const viewerFrame = screen.getByTestId("pdf-viewer").parentElement;
const page = viewerFrame?.parentElement;
expect(page).not.toHaveClass("min-h-svh", "h-svh", "max-h-svh", "overflow-hidden");
expect(viewerFrame).not.toHaveClass("min-h-0", "flex-1", "overflow-hidden");
});
});
@@ -1,69 +0,0 @@
import { t } from "@lingui/core/macro";
import { CircleNotchIcon, DownloadSimpleIcon } from "@phosphor-icons/react";
import { useQuery } from "@tanstack/react-query";
import { getRouteApi } from "@tanstack/react-router";
import { useCallback, useState } from "react";
import { toast } from "sonner";
import { BrandIcon } from "@reactive-resume/ui/components/brand-icon";
import { Button } from "@reactive-resume/ui/components/button";
import { downloadWithAnchor, generateFilename } from "@reactive-resume/utils/file";
import { LoadingScreen } from "@/components/layout/loading-screen";
import { orpc } from "@/libs/orpc/client";
import { createResumePdfBlob } from "@/libs/resume/pdf-document";
import { PdfViewer } from "./pdf-viewer";
const publicResumeRoute = getRouteApi("/$username/$slug");
export function PublicResumeRoute() {
const { username, slug } = publicResumeRoute.useParams();
const { data: resume } = useQuery(orpc.resume.getBySlug.queryOptions({ input: { username, slug } }));
const [isPrinting, setIsPrinting] = useState(false);
const onDownloadPDF = useCallback(async () => {
if (!resume) return;
const filename = generateFilename(resume.name || resume.data.basics.name || resume.slug, "pdf");
const toastId = toast.loading(t`Please wait while your PDF is being generated...`);
setIsPrinting(true);
try {
const blob = await createResumePdfBlob(resume.data);
downloadWithAnchor(blob, filename);
} catch {
toast.error(t`There was a problem while generating the PDF, please try again.`);
} finally {
setIsPrinting(false);
toast.dismiss(toastId);
}
}, [resume]);
if (!resume) return <LoadingScreen />;
return (
<>
<div className="mx-auto flex w-full flex-col items-center gap-6 px-4 py-6 print:m-0 print:block print:max-w-full print:p-0">
<div className="w-full max-w-5xl bg-white print:max-w-full">
<PdfViewer data={resume.data} className="block w-full" />
</div>
<footer className="flex justify-center print:hidden">
<BrandIcon variant="icon" className="size-8 opacity-60" />
</footer>
</div>
<Button
size="icon-lg"
variant="outline"
disabled={isPrinting}
onClick={onDownloadPDF}
aria-label={t`Download PDF`}
title={t`Download PDF`}
className="fixed right-6 bottom-6 z-50 rounded-full bg-background/95 opacity-70 shadow-lg backdrop-blur transition-opacity hover:opacity-100 print:hidden"
>
{isPrinting ? <CircleNotchIcon className="size-5 animate-spin" /> : <DownloadSimpleIcon className="size-5" />}
</Button>
</>
);
}