Skip to content

Commit 5132b82

Browse files
feat: added new hover animations
1 parent 4663960 commit 5132b82

2 files changed

Lines changed: 79 additions & 13 deletions

File tree

‎apps/codebility/app/(marketing)/services/_components/ui/ServicesServiceCard.tsx‎

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { memo, useMemo } from "react";
3+
import { memo, useEffect, useMemo, useRef, useState } from "react";
44
import Image from "next/image";
55
import Link from "next/link";
66
import { IconLink } from "@/public/assets/svgs";
@@ -81,6 +81,28 @@ export const ServicesServiceCard = memo(({ service, onSelect }: Props) => {
8181
[members],
8282
);
8383

84+
const [isHovered, setIsHovered] = useState(false);
85+
const [showDescription, setShowDescription] = useState(false);
86+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
87+
88+
useEffect(() => {
89+
if (!isHovered) {
90+
setShowDescription(false);
91+
if (timerRef.current) clearTimeout(timerRef.current);
92+
timerRef.current = null;
93+
return;
94+
}
95+
96+
// After 2s on hover, switch to description
97+
timerRef.current = setTimeout(() => {
98+
setShowDescription(true);
99+
}, 2000);
100+
101+
return () => {
102+
if (timerRef.current) clearTimeout(timerRef.current);
103+
};
104+
}, [isHovered]);
105+
84106
const handleClick = () => {
85107
if (onSelect) {
86108
onSelect(service);
@@ -92,7 +114,12 @@ export const ServicesServiceCard = memo(({ service, onSelect }: Props) => {
92114
return (
93115
<div className="group cursor-pointer overflow-hidden rounded-xl bg-white/[0.04] ring-1 ring-white/[0.08] transition-all duration-300 hover:bg-white/[0.07] hover:ring-white/[0.16] hover:shadow-[0_8px_30px_rgba(0,0,0,0.3)]">
94116
{/* Image — consistent 4:3 aspect ratio */}
95-
<div className="relative aspect-[4/3] w-full overflow-hidden" onClick={handleClick}>
117+
<div
118+
className="relative aspect-[4/3] w-full overflow-hidden"
119+
onClick={handleClick}
120+
onMouseEnter={() => setIsHovered(true)}
121+
onMouseLeave={() => setIsHovered(false)}
122+
>
96123
<Image
97124
src={imageUrl}
98125
alt={name}
@@ -102,28 +129,55 @@ export const ServicesServiceCard = memo(({ service, onSelect }: Props) => {
102129
quality={75}
103130
/>
104131

105-
{/* Hover overlay with action */}
106-
<div className="absolute inset-0 flex items-center justify-center bg-black/0 transition-all duration-300 group-hover:bg-black/40">
107-
<div className="flex translate-y-3 items-center gap-2 rounded-full bg-white/90 px-4 py-2 text-sm font-semibold text-gray-900 opacity-0 shadow-xl backdrop-blur-sm transition-all duration-300 group-hover:translate-y-0 group-hover:opacity-100">
108-
<Eye className="size-4" />
109-
View Project
110-
</div>
111-
</div>
112-
113-
{/* Category badges — top left */}
132+
{/* Category badges — dark glass pill */}
114133
{categories.length > 0 && (
115-
<div className="absolute left-2.5 top-2.5 flex max-w-[calc(100%-1.25rem)] flex-wrap gap-1">
134+
<div className="absolute left-3 top-3 flex max-w-[calc(100%-1.5rem)] flex-wrap gap-1.5">
116135
{categories.map((category) => (
117136
<span
118137
key={category.id}
119-
className="rounded-md bg-black/60 px-2 py-0.5 text-[11px] font-medium text-white backdrop-blur-md"
138+
className="rounded-md border border-white/10 bg-gray-950/75 px-2.5 py-1 text-[11px] font-semibold text-white/90"
120139
>
121140
{category.name}
122141
</span>
123142
))}
124143
</div>
125144
)}
126145

146+
{/* Bottom glass band — grows from single-line to 40% max on description */}
147+
<div
148+
className="pointer-events-none absolute inset-x-0 bottom-0 border-t border-white/10 bg-gray-950/75 px-4 py-3"
149+
style={{
150+
opacity: isHovered ? 1 : 0,
151+
maxHeight: showDescription ? "40%" : "auto",
152+
transition: "opacity 300ms cubic-bezier(0.4,0,0.2,1), max-height 300ms cubic-bezier(0.4,0,0.2,1)",
153+
}}
154+
>
155+
{!showDescription && (
156+
<span
157+
className="flex items-center gap-2 text-sm font-semibold text-white/90"
158+
style={{
159+
animation: "fadeSlideIn 300ms cubic-bezier(0.4,0,0.2,1) both",
160+
}}
161+
>
162+
<Eye className="size-4 text-white/60" />
163+
View Project
164+
</span>
165+
)}
166+
{showDescription && (
167+
<span
168+
className="block overflow-hidden text-ellipsis text-[13px] leading-relaxed text-white/80"
169+
style={{
170+
display: "-webkit-box",
171+
WebkitLineClamp: 3,
172+
WebkitBoxOrient: "vertical",
173+
animation: "fadeSlideIn 300ms cubic-bezier(0.4,0,0.2,1) both",
174+
}}
175+
>
176+
{description || "No description available."}
177+
</span>
178+
)}
179+
</div>
180+
127181
{/* Website link — top right */}
128182
{hasValidWebsite && (
129183
<Link

‎apps/codebility/app/globals.css‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -836,4 +836,16 @@ html body[data-scroll-locked] {
836836
border: none;
837837
border-top: 2px solid rgba(255, 255, 255, 0.2);
838838
margin: 1.5rem 0;
839+
}
840+
841+
/* Services grid hover animation */
842+
@keyframes fadeSlideIn {
843+
from {
844+
opacity: 0;
845+
transform: translateX(-8px);
846+
}
847+
to {
848+
opacity: 1;
849+
transform: translateX(0);
850+
}
839851
}

0 commit comments

Comments
 (0)