mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-24 23:32:19 +10:00
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:
-64
@@ -1,64 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
getResumeThumbnailCacheKey,
|
||||
getResumeThumbnailRenderSize,
|
||||
RESUME_THUMBNAIL_TARGET_WIDTH,
|
||||
} from "./resume-thumbnail.shared";
|
||||
|
||||
describe("getResumeThumbnailCacheKey", () => {
|
||||
it("composes id and updated-at epoch milliseconds with a colon", () => {
|
||||
const date = new Date("2024-01-15T00:00:00.000Z");
|
||||
expect(getResumeThumbnailCacheKey("abc", date)).toBe(`abc:${date.getTime()}`);
|
||||
});
|
||||
|
||||
it("differs when updatedAt changes", () => {
|
||||
const id = "resume-1";
|
||||
const a = getResumeThumbnailCacheKey(id, new Date(1000));
|
||||
const b = getResumeThumbnailCacheKey(id, new Date(2000));
|
||||
expect(a).not.toBe(b);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getResumeThumbnailRenderSize", () => {
|
||||
it("uses the default target width when not provided", () => {
|
||||
const size = getResumeThumbnailRenderSize({ width: 800, height: 1200 });
|
||||
|
||||
expect(size.width).toBe(RESUME_THUMBNAIL_TARGET_WIDTH);
|
||||
// Scale = 420/800 = 0.525 → height rounds to 630
|
||||
expect(size.height).toBe(630);
|
||||
expect(size.scale).toBeCloseTo(0.525, 5);
|
||||
});
|
||||
|
||||
it("scales relative to the provided target width", () => {
|
||||
const size = getResumeThumbnailRenderSize({ width: 800, height: 1200 }, 400);
|
||||
expect(size.width).toBe(400);
|
||||
expect(size.scale).toBeCloseTo(0.5, 5);
|
||||
expect(size.height).toBe(600);
|
||||
});
|
||||
|
||||
it("clamps pixelRatio to a minimum of 1", () => {
|
||||
const a = getResumeThumbnailRenderSize({ width: 800, height: 1200 }, 400, 0.5);
|
||||
const b = getResumeThumbnailRenderSize({ width: 800, height: 1200 }, 400, 1);
|
||||
expect(a).toEqual(b);
|
||||
});
|
||||
|
||||
it("clamps pixelRatio to a maximum of 2", () => {
|
||||
const a = getResumeThumbnailRenderSize({ width: 800, height: 1200 }, 400, 3);
|
||||
const b = getResumeThumbnailRenderSize({ width: 800, height: 1200 }, 400, 2);
|
||||
expect(a).toEqual(b);
|
||||
});
|
||||
|
||||
it("multiplies width/height by pixelRatio when within bounds", () => {
|
||||
const size = getResumeThumbnailRenderSize({ width: 800, height: 1200 }, 400, 2);
|
||||
// pageScale = 0.5, outputScale = 2 → width = 800, height = 1200
|
||||
expect(size.width).toBe(800);
|
||||
expect(size.height).toBe(1200);
|
||||
expect(size.scale).toBeCloseTo(1, 5);
|
||||
});
|
||||
|
||||
it("rounds the output dimensions to integers", () => {
|
||||
const size = getResumeThumbnailRenderSize({ width: 793, height: 1123 }, 421, 1.5);
|
||||
expect(Number.isInteger(size.width)).toBe(true);
|
||||
expect(Number.isInteger(size.height)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
type PageSize = {
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
|
||||
export const RESUME_THUMBNAIL_TARGET_WIDTH = 420;
|
||||
const MAX_THUMBNAIL_PIXEL_RATIO = 2;
|
||||
|
||||
export const getResumeThumbnailCacheKey = (resumeId: string, updatedAt: Date) => {
|
||||
return `${resumeId}:${updatedAt.getTime()}`;
|
||||
};
|
||||
|
||||
export const getResumeThumbnailRenderSize = (
|
||||
pageSize: PageSize,
|
||||
targetWidth = RESUME_THUMBNAIL_TARGET_WIDTH,
|
||||
pixelRatio = 1,
|
||||
) => {
|
||||
const outputScale = Math.min(Math.max(pixelRatio, 1), MAX_THUMBNAIL_PIXEL_RATIO);
|
||||
const pageScale = targetWidth / pageSize.width;
|
||||
|
||||
return {
|
||||
height: Math.round(pageSize.height * pageScale * outputScale),
|
||||
scale: pageScale * outputScale,
|
||||
width: Math.round(targetWidth * outputScale),
|
||||
};
|
||||
};
|
||||
@@ -6,84 +6,15 @@ import { useInView } from "motion/react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Spinner } from "@reactive-resume/ui/components/spinner";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
import { createResumePdfBlob } from "@/features/resume/export/pdf-document";
|
||||
import { createPdfFirstPageImageUrl } from "@/features/resume/preview/pdf-thumbnail";
|
||||
import { getResumeThumbnailCacheKey } from "@/features/resume/preview/resume-thumbnail.shared";
|
||||
import { orpc } from "@/libs/orpc/client";
|
||||
import { createResumePdfBlob } from "@/libs/resume/pdf-document";
|
||||
import {
|
||||
getResumeThumbnailCacheKey,
|
||||
getResumeThumbnailRenderSize,
|
||||
RESUME_THUMBNAIL_TARGET_WIDTH,
|
||||
} from "./resume-thumbnail.shared";
|
||||
|
||||
type ResumeListItem = RouterOutput["resume"]["list"][number];
|
||||
|
||||
type ThumbnailState = { status: "error" | "idle" | "loading" } | { status: "ready"; url: string };
|
||||
|
||||
const canvasToBlob = async (canvas: HTMLCanvasElement) => {
|
||||
return await new Promise<Blob>((resolve, reject) => {
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob) {
|
||||
reject(new Error("Failed to create resume thumbnail image."));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(blob);
|
||||
}, "image/png");
|
||||
});
|
||||
};
|
||||
|
||||
const createPdfFirstPageImageUrl = async (file: Blob) => {
|
||||
const { AnnotationMode, GlobalWorkerOptions, getDocument } = await import("pdfjs-dist");
|
||||
GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url).toString();
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const loadingTask = getDocument({ data: new Uint8Array(arrayBuffer) });
|
||||
let pdfDocument: Awaited<typeof loadingTask.promise> | undefined;
|
||||
|
||||
try {
|
||||
pdfDocument = await loadingTask.promise;
|
||||
const page = await pdfDocument.getPage(1);
|
||||
|
||||
try {
|
||||
const baseViewport = page.getViewport({ scale: 1 });
|
||||
const renderSize = getResumeThumbnailRenderSize(
|
||||
{ height: baseViewport.height, width: baseViewport.width },
|
||||
RESUME_THUMBNAIL_TARGET_WIDTH,
|
||||
window.devicePixelRatio || 1,
|
||||
);
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
const canvasContext = canvas.getContext("2d");
|
||||
|
||||
if (!canvasContext) throw new Error("Failed to create resume thumbnail canvas context.");
|
||||
|
||||
canvas.height = renderSize.height;
|
||||
canvas.width = renderSize.width;
|
||||
|
||||
const viewport = page.getViewport({ scale: renderSize.scale });
|
||||
const renderTask = page.render({
|
||||
canvas,
|
||||
canvasContext,
|
||||
viewport,
|
||||
annotationMode: AnnotationMode.DISABLE,
|
||||
background: "white",
|
||||
});
|
||||
|
||||
await renderTask.promise;
|
||||
|
||||
const image = await canvasToBlob(canvas);
|
||||
return URL.createObjectURL(image);
|
||||
} finally {
|
||||
page.cleanup();
|
||||
}
|
||||
} finally {
|
||||
if (pdfDocument) {
|
||||
void pdfDocument.destroy();
|
||||
} else {
|
||||
void loadingTask.destroy();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function useResumeThumbnail(data: ResumeData | undefined, cacheKey: string | undefined) {
|
||||
const [thumbnail, setThumbnail] = useState<ThumbnailState>({ status: "idle" });
|
||||
const currentUrlRef = useRef<string | null>(null);
|
||||
|
||||
@@ -74,7 +74,7 @@ function RouteComponent() {
|
||||
placeholder={t`Sort by`}
|
||||
onValueChange={(value) => {
|
||||
if (!value) return;
|
||||
void navigate({ search: (prev) => ({ ...prev, sort: value as SortOption }) });
|
||||
void navigate({ search: (prev: Search) => ({ ...prev, sort: value as SortOption }) });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -89,7 +89,7 @@ function RouteComponent() {
|
||||
options={tagOptions}
|
||||
placeholder={t`Filter by`}
|
||||
onValueChange={(value) => {
|
||||
void navigate({ search: (prev) => ({ ...prev, tags: value ?? [] }) });
|
||||
void navigate({ search: (prev: Search) => ({ ...prev, tags: value ?? [] }) });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -100,7 +100,7 @@ function RouteComponent() {
|
||||
value="grid"
|
||||
nativeButton={false}
|
||||
className="rounded-r-none"
|
||||
render={<Link to="." search={(prev) => ({ ...prev, view: "grid" })} />}
|
||||
render={<Link to="." search={(prev: Search) => ({ ...prev, view: "grid" })} />}
|
||||
>
|
||||
<GridFourIcon />
|
||||
<Trans>Grid</Trans>
|
||||
@@ -110,7 +110,7 @@ function RouteComponent() {
|
||||
value="list"
|
||||
nativeButton={false}
|
||||
className="rounded-l-none"
|
||||
render={<Link to="." search={(prev) => ({ ...prev, view: "list" })} />}
|
||||
render={<Link to="." search={(prev: Search) => ({ ...prev, view: "list" })} />}
|
||||
>
|
||||
<ListIcon />
|
||||
<Trans>List</Trans>
|
||||
|
||||
Reference in New Issue
Block a user