mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-23 23:02:17 +10:00
chore: lint using react-doctor, update translations, dynamic imports
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user