chore: lint using react-doctor, update translations, dynamic imports

This commit is contained in:
Amruth Pillai
2026-05-21 09:56:26 +02:00
parent 3596102c63
commit 39e88dd365
208 changed files with 5876 additions and 4778 deletions
@@ -13,7 +13,7 @@ import {
WarningIcon,
} from "@phosphor-icons/react";
import { Link } from "@tanstack/react-router";
import { AnimatePresence, motion } from "motion/react";
import { AnimatePresence, m } from "motion/react";
import { Avatar, AvatarFallback, AvatarImage } from "@reactive-resume/ui/components/avatar";
import { BrandIcon } from "@reactive-resume/ui/components/brand-icon";
import {
@@ -185,7 +185,7 @@ export function DashboardSidebar() {
<AnimatePresence>
{state === "expanded" && (
<motion.div
<m.div
key="copyright"
className="will-change-[transform,opacity]"
initial={{ y: 12, opacity: 0 }}
@@ -194,7 +194,7 @@ export function DashboardSidebar() {
transition={{ duration: 0.2, ease: "easeOut" }}
>
<Copyright className="wrap-break-word shrink-0 whitespace-normal p-2" />
</motion.div>
</m.div>
)}
</AnimatePresence>
</SidebarFooter>
@@ -22,7 +22,7 @@ export function BaseCard({ title, description, tags, className, children, ...pro
>
{children}
<div className="absolute inset-x-0 bottom-0 flex w-full flex-col justify-end space-y-0.5 bg-background/40 px-4 py-3 backdrop-blur-xs">
<div className="absolute inset-x-0 bottom-0 flex w-full flex-col justify-end gap-y-0.5 bg-background/40 px-4 py-3 backdrop-blur-xs">
<h3 className="truncate font-medium tracking-tight">{title}</h3>
<p className="truncate text-xs opacity-80">{description}</p>
@@ -3,7 +3,7 @@ import { t } from "@lingui/core/macro";
import { useLingui } from "@lingui/react";
import { LockSimpleIcon } from "@phosphor-icons/react";
import { Link } from "@tanstack/react-router";
import { AnimatePresence, motion } from "motion/react";
import { AnimatePresence, m } from "motion/react";
import { useMemo } from "react";
import { ResumeContextMenu } from "../menus/context-menu";
import { BaseCard } from "./base-card";
@@ -23,7 +23,7 @@ export function ResumeCard({ resume }: ResumeCardProps) {
return (
<ResumeContextMenu resume={resume}>
<Link to="/builder/$resumeId" params={{ resumeId: resume.id }} className="cursor-default">
<motion.div
<m.div
className="will-change-transform"
whileHover={{ y: -2, scale: 1.005 }}
whileTap={{ scale: 0.998 }}
@@ -34,7 +34,7 @@ export function ResumeCard({ resume }: ResumeCardProps) {
<ResumeLockOverlay isLocked={resume.isLocked} />
</BaseCard>
</motion.div>
</m.div>
</Link>
</ResumeContextMenu>
);
@@ -44,7 +44,7 @@ function ResumeLockOverlay({ isLocked }: { isLocked: boolean }) {
return (
<AnimatePresence>
{isLocked && (
<motion.div
<m.div
key="resume-lock-overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 0.6 }}
@@ -55,7 +55,7 @@ function ResumeLockOverlay({ isLocked }: { isLocked: boolean }) {
<div className="flex items-center justify-center rounded-full bg-popover p-6">
<LockSimpleIcon weight="thin" className="size-12 opacity-60" />
</div>
</motion.div>
</m.div>
)}
</AnimatePresence>
);
@@ -3,7 +3,7 @@ import type { RouterOutput } from "@/libs/orpc/client";
import { FileTextIcon } from "@phosphor-icons/react";
import { useQuery } from "@tanstack/react-query";
import { useInView } from "motion/react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useEffect, useRef } 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";
@@ -15,71 +15,52 @@ type ResumeListItem = RouterOutput["resume"]["list"][number];
type ThumbnailState = { status: "error" | "idle" | "loading" } | { status: "ready"; url: string };
function useResumeThumbnail(data: ResumeData | undefined, cacheKey: string | undefined) {
const [thumbnail, setThumbnail] = useState<ThumbnailState>({ status: "idle" });
const currentUrlRef = useRef<string | null>(null);
const throwIfAborted = (signal: AbortSignal) => {
if (signal.aborted) throw new DOMException("Thumbnail generation aborted.", "AbortError");
};
const revokeCurrentThumbnail = useCallback(() => {
if (!currentUrlRef.current) return;
URL.revokeObjectURL(currentUrlRef.current);
currentUrlRef.current = null;
}, []);
const createResumeThumbnailUrl = async (data: ResumeData, signal: AbortSignal) => {
const pdf = await createResumePdfBlob(data);
throwIfAborted(signal);
const url = await createPdfFirstPageImageUrl(pdf);
if (signal.aborted) {
URL.revokeObjectURL(url);
throwIfAborted(signal);
}
return url;
};
function useResumeThumbnail(data: ResumeData | undefined, cacheKey: string | undefined): ThumbnailState {
const thumbnailQuery = useQuery({
queryKey: ["resume-thumbnail", cacheKey],
queryFn: ({ signal }) => {
if (!data) throw new Error("Resume data is required to generate a thumbnail.");
return createResumeThumbnailUrl(data, signal);
},
enabled: Boolean(data && cacheKey),
gcTime: 0,
});
useEffect(() => {
return () => {
revokeCurrentThumbnail();
};
}, [revokeCurrentThumbnail]);
if (thumbnailQuery.error) console.error("Failed to generate resume thumbnail", thumbnailQuery.error);
}, [thumbnailQuery.error]);
useEffect(() => {
if (!data || !cacheKey) {
revokeCurrentThumbnail();
setThumbnail({ status: "idle" });
return;
}
let isCancelled = false;
let nextUrl: string | null = null;
setThumbnail({ status: "loading" });
const generateThumbnail = async () => {
try {
const pdf = await createResumePdfBlob(data);
if (isCancelled) return;
nextUrl = await createPdfFirstPageImageUrl(pdf);
if (isCancelled) {
URL.revokeObjectURL(nextUrl);
nextUrl = null;
return;
}
revokeCurrentThumbnail();
currentUrlRef.current = nextUrl;
setThumbnail({ status: "ready", url: nextUrl });
nextUrl = null;
} catch (error) {
if (isCancelled) return;
console.error("Failed to generate resume thumbnail", error);
revokeCurrentThumbnail();
setThumbnail({ status: "error" });
}
};
void generateThumbnail();
const url = thumbnailQuery.data;
return () => {
isCancelled = true;
if (nextUrl) {
URL.revokeObjectURL(nextUrl);
}
if (url) URL.revokeObjectURL(url);
};
}, [data, cacheKey, revokeCurrentThumbnail]);
}, [thumbnailQuery.data]);
return thumbnail;
if (!data || !cacheKey) return { status: "idle" };
if (thumbnailQuery.isError) return { status: "error" };
if (thumbnailQuery.data) return { status: "ready", url: thumbnailQuery.data };
return { status: "loading" };
}
export function ResumeThumbnail({ isLocked, resume }: { isLocked: boolean; resume: ResumeListItem }) {
@@ -1,5 +1,5 @@
import type { RouterOutput } from "@/libs/orpc/client";
import { AnimatePresence, motion } from "motion/react";
import { AnimatePresence, m } from "motion/react";
import { CreateResumeCard } from "./cards/create-card";
import { ImportResumeCard } from "./cards/import-card";
import { ResumeCard } from "./cards/resume-card";
@@ -13,7 +13,7 @@ type Props = {
export function GridView({ resumes }: Props) {
return (
<div className="grid 3xl:grid-cols-6 grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
<motion.div
<m.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
@@ -21,9 +21,9 @@ export function GridView({ resumes }: Props) {
className="will-change-[transform,opacity]"
>
<CreateResumeCard />
</motion.div>
</m.div>
<motion.div
<m.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
@@ -31,11 +31,11 @@ export function GridView({ resumes }: Props) {
className="will-change-[transform,opacity]"
>
<ImportResumeCard />
</motion.div>
</m.div>
<AnimatePresence initial={false} mode="popLayout">
{resumes?.map((resume, index) => (
<motion.div
<m.div
layout
key={resume.id}
initial={{ opacity: 0, y: -20 }}
@@ -49,7 +49,7 @@ export function GridView({ resumes }: Props) {
className="will-change-[transform,opacity]"
>
<ResumeCard resume={resume} />
</motion.div>
</m.div>
))}
</AnimatePresence>
</div>
@@ -3,7 +3,7 @@ import { useLingui } from "@lingui/react";
import { Trans } from "@lingui/react/macro";
import { DotsThreeIcon, DownloadSimpleIcon, PlusIcon } from "@phosphor-icons/react";
import { Link } from "@tanstack/react-router";
import { AnimatePresence, motion } from "motion/react";
import { AnimatePresence, m } from "motion/react";
import { useMemo } from "react";
import { Button } from "@reactive-resume/ui/components/button";
import { useDialogStore } from "@/dialogs/store";
@@ -28,7 +28,7 @@ export function ListView({ resumes }: Props) {
return (
<div className="flex flex-col gap-y-1">
<motion.div
<m.div
className="will-change-[transform,opacity]"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
@@ -50,9 +50,9 @@ export function ListView({ resumes }: Props) {
<Trans>Start building your resume from scratch</Trans>
</p>
</Button>
</motion.div>
</m.div>
<motion.div
<m.div
className="will-change-[transform,opacity]"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
@@ -75,11 +75,11 @@ export function ListView({ resumes }: Props) {
<Trans>Continue where you left off</Trans>
</p>
</Button>
</motion.div>
</m.div>
<AnimatePresence initial={false} mode="popLayout">
{resumes?.map((resume, index) => (
<motion.div
<m.div
layout
key={resume.id}
className="will-change-[transform,opacity]"
@@ -89,7 +89,7 @@ export function ListView({ resumes }: Props) {
transition={{ duration: 0.18, delay: Math.min(0.12, (index + 2) * 0.02), ease: "easeOut" }}
>
<ResumeListItem resume={resume} />
</motion.div>
</m.div>
))}
</AnimatePresence>
</div>