Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type Props = {

export const BaseCard = forwardRef<HTMLDivElement, Props>(
({ children, className, onClick }, ref) => (
<Tilt {...defaultTiltProps}>
<Tilt {...defaultTiltProps} style={{ touchAction: 'auto' }}>
<Card
ref={ref}
className={cn(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ import { cn } from "@reactive-resume/utils";
import dayjs from "dayjs";
import { AnimatePresence, motion } from "framer-motion";
import { useNavigate } from "react-router";
import { useCallback, useEffect, useRef, useState } from "react";

import { useDialog } from "@/client/stores/dialog";
import { useScroll } from "./scroll-context";

import { BaseCard } from "./base-card";

Expand All @@ -32,10 +34,44 @@ export const ResumeCard = ({ resume }: Props) => {
const navigate = useNavigate();
const { open } = useDialog<ResumeDto>("resume");
const { open: lockOpen } = useDialog<ResumeDto>("lock");
const [dropdownOpen, setDropdownOpen] = useState(false);
const cardRef = useRef<HTMLDivElement>(null);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const { isScrolling } = useScroll();

const template = resume.data.metadata.template;
const lastUpdated = dayjs().to(resume.updatedAt);

// Handle hover with delay to prevent accidental triggers
const handleMouseEnter = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
if (!isScrolling) {
timeoutRef.current = setTimeout(() => {
setDropdownOpen(true);
}, 200); // 200ms delay before showing dropdown
}
}, [isScrolling]);

const handleMouseLeave = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
setDropdownOpen(false);
}, 300); // 300ms delay before hiding dropdown for better UX
}, []);

// Clear timeout on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);

const onOpen = () => {
void navigate(`/builder/${resume.id}`);
};
Expand All @@ -57,70 +93,114 @@ export const ResumeCard = ({ resume }: Props) => {
};

return (
<DropdownMenu>
<DropdownMenuTrigger className="text-left">
<BaseCard className="cursor-context-menu space-y-0">
<AnimatePresence>
{resume.locked && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0 flex items-center justify-center bg-background/75 backdrop-blur-sm"
>
<Lock size={42} />
</motion.div>
)}
</AnimatePresence>

<div
className={cn(
"absolute inset-x-0 bottom-0 z-10 flex flex-col justify-end space-y-0.5 p-4 pt-12",
"bg-gradient-to-t from-background/80 to-transparent",
)}
<div
ref={cardRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className="relative"
>
<BaseCard className="cursor-context-menu space-y-0">
<AnimatePresence>
{resume.locked && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0 flex items-center justify-center bg-background/75 backdrop-blur-sm"
>
<Lock size={42} />
</motion.div>
)}
</AnimatePresence>

<div
className={cn(
"absolute inset-x-0 bottom-0 z-10 flex flex-col justify-end space-y-0.5 p-4 pt-12",
"bg-gradient-to-t from-background/80 to-transparent",
)}
>
<h4 className="line-clamp-2 font-medium">{resume.title}</h4>
<p className="line-clamp-1 text-xs opacity-75">{t`Last updated ${lastUpdated}`}</p>
</div>

<img
src={`/templates/jpg/${template}.jpg`}
alt={template}
className="rounded-sm opacity-80"
/>
</BaseCard>

{/* Custom Dropdown Menu that appears on hover */}
<AnimatePresence>
{dropdownOpen && (
<motion.div
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 5 }}
transition={{ duration: 0.15 }}
className="absolute inset-0 flex items-center justify-center bg-background/75 backdrop-blur-sm"
onClick={onOpen}
onMouseEnter={() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
setDropdownOpen(true);
}}
onMouseLeave={handleMouseLeave}
>
<h4 className="line-clamp-2 font-medium">{resume.title}</h4>
<p className="line-clamp-1 text-xs opacity-75">{t`Last updated ${lastUpdated}`}</p>
</div>

<img
src={`/templates/jpg/${template}.jpg`}
alt={template}
className="rounded-sm opacity-80"
/>
</BaseCard>
</DropdownMenuTrigger>

<DropdownMenuContent>
<DropdownMenuItem onClick={onOpen}>
<FolderOpen size={14} className="mr-2" />
{t`Open`}
</DropdownMenuItem>
<DropdownMenuItem onClick={onUpdate}>
<PencilSimple size={14} className="mr-2" />
{t`Rename`}
</DropdownMenuItem>
<DropdownMenuItem onClick={onDuplicate}>
<CopySimple size={14} className="mr-2" />
{t`Duplicate`}
</DropdownMenuItem>
{resume.locked ? (
<DropdownMenuItem onClick={onLockChange}>
<LockOpen size={14} className="mr-2" />
{t`Unlock`}
</DropdownMenuItem>
) : (
<DropdownMenuItem onClick={onLockChange}>
<Lock size={14} className="mr-2" />
{t`Lock`}
</DropdownMenuItem>
<div
className="flex w-full max-w-[180px] flex-col rounded-md bg-card shadow-md"
onClick={(e) => e.stopPropagation()}
>
<button
className="flex w-full items-center px-4 py-2 text-sm hover:bg-accent hover:text-accent-foreground"
onClick={onOpen}
>
<FolderOpen size={16} className="mr-2" />
{t`Open`}
</button>
<button
className="flex w-full items-center px-4 py-2 text-sm hover:bg-accent hover:text-accent-foreground"
onClick={onUpdate}
>
<PencilSimple size={16} className="mr-2" />
{t`Rename`}
</button>
<button
className="flex w-full items-center px-4 py-2 text-sm hover:bg-accent hover:text-accent-foreground"
onClick={onDuplicate}
>
<CopySimple size={16} className="mr-2" />
{t`Duplicate`}
</button>
<button
className="flex w-full items-center px-4 py-2 text-sm hover:bg-accent hover:text-accent-foreground"
onClick={onLockChange}
>
{resume.locked ? (
<>
<LockOpen size={16} className="mr-2" />
{t`Unlock`}
</>
) : (
<>
<Lock size={16} className="mr-2" />
{t`Lock`}
</>
)}
</button>
<div className="mx-2 my-1 h-px bg-border"></div>
<button
className="flex w-full items-center px-4 py-2 text-sm text-error hover:bg-accent hover:text-error"
onClick={onDelete}
>
<TrashSimple size={16} className="mr-2" />
{t`Delete`}
</button>
</div>
</motion.div>
)}
<DropdownMenuSeparator />
<DropdownMenuItem className="text-error" onClick={onDelete}>
<TrashSimple size={14} className="mr-2" />
{t`Delete`}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</AnimatePresence>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { createContext, useContext, useEffect, useState } from "react";

type ScrollContextType = {
isScrolling: boolean;
};

const ScrollContext = createContext<ScrollContextType>({ isScrolling: false });

export const useScroll = () => useContext(ScrollContext);

export const ScrollProvider = ({ children }: { children: React.ReactNode }) => {
const [isScrolling, setIsScrolling] = useState(false);

useEffect(() => {
let scrollTimeout: NodeJS.Timeout;

const handleScroll = () => {
setIsScrolling(true);

if (scrollTimeout) clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
setIsScrolling(false);
}, 150); // Debounce time for scroll end detection
};

window.addEventListener("scroll", handleScroll, { passive: true });
return () => {
window.removeEventListener("scroll", handleScroll);
if (scrollTimeout) clearTimeout(scrollTimeout);
};
}, []);

return (
<ScrollContext.Provider value={{ isScrolling }}>
{children}
</ScrollContext.Provider>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
//ignore this file ive created this for just trying to enhnace that mouse 3d hovering of that resume card this file is nothing has to do with this currrent implemented feaure!
79 changes: 41 additions & 38 deletions apps/client/src/pages/dashboard/resumes/_layouts/grid/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,51 +7,54 @@ import { BaseCard } from "./_components/base-card";
import { CreateResumeCard } from "./_components/create-card";
import { ImportResumeCard } from "./_components/import-card";
import { ResumeCard } from "./_components/resume-card";
import { ScrollProvider } from "./_components/scroll-context";

export const GridView = () => {
const { resumes, loading } = useResumes();

return (
<div className="grid grid-cols-1 gap-8 sm:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
<motion.div initial={{ opacity: 0, x: -50 }} animate={{ opacity: 1, x: 0 }}>
<CreateResumeCard />
</motion.div>
<ScrollProvider>
<div className="grid grid-cols-1 gap-8 sm:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
<motion.div initial={{ opacity: 0, x: -50 }} animate={{ opacity: 1, x: 0 }}>
<CreateResumeCard />
</motion.div>

<motion.div
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0, transition: { delay: 0.1 } }}
>
<ImportResumeCard />
</motion.div>
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0, transition: { delay: 0.1 } }}
>
<ImportResumeCard />
</motion.div>

{loading &&
Array.from({ length: 4 }).map((_, i) => (
<div
key={i}
className="duration-300 animate-in fade-in"
style={{ animationFillMode: "backwards", animationDelay: `${i * 300}ms` }}
>
<BaseCard />
</div>
))}
{loading &&
Array.from({ length: 4 }).map((_, i) => (
<div
key={i}
className="duration-300 animate-in fade-in"
style={{ animationFillMode: "backwards", animationDelay: `${i * 300}ms` }}
>
<BaseCard />
</div>
))}

{resumes && (
<AnimatePresence>
{resumes
.sort((a, b) => sortByDate(a, b, "updatedAt"))
.map((resume, index) => (
<motion.div
key={resume.id}
layout
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0, transition: { delay: (index + 2) * 0.1 } }}
exit={{ opacity: 0, filter: "blur(8px)", transition: { duration: 0.5 } }}
>
<ResumeCard resume={resume} />
</motion.div>
))}
</AnimatePresence>
)}
</div>
{resumes && (
<AnimatePresence>
{resumes
.sort((a, b) => sortByDate(a, b, "updatedAt"))
.map((resume, index) => (
<motion.div
key={resume.id}
layout
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0, transition: { delay: (index + 2) * 0.1 } }}
exit={{ opacity: 0, filter: "blur(8px)", transition: { duration: 0.5 } }}
>
<ResumeCard resume={resume} />
</motion.div>
))}
</AnimatePresence>
)}
</div>
</ScrollProvider>
);
};