1- import { useState } from "react" ;
1+ import { useState , memo , useMemo } from "react" ;
22import Image from "next/image" ;
33import Link from "next/link" ;
44import DefaultAvatar from "@/components/DefaultAvatar" ;
@@ -33,13 +33,14 @@ interface ServiceProject {
3333 created_at ?: string ;
3434 updated_at ?: string ;
3535 website_url ?: string ;
36+ tech_stack ?: string [ ] ;
3637}
3738
3839interface Props {
3940 service : ServiceProject ;
4041}
4142
42- export default function ServiceCard ( { service } : Props ) {
43+ function ServiceCard ( { service } : Props ) {
4344 const [ isDescriptionExpanded , setIsDescriptionExpanded ] = useState ( false ) ;
4445
4546 const {
@@ -49,81 +50,150 @@ export default function ServiceCard({ service }: Props) {
4950 members = [ ] ,
5051 website_url,
5152 project_category_name,
53+ tech_stack = [ ] ,
5254 } = service ;
5355
54- const imageUrl = main_image
55- ? main_image . startsWith ( "public" )
56- ? `${ process . env . NEXT_PUBLIC_SUPABASE_URL } /storage/v1/object/public/services-image/${ main_image } `
57- : main_image
58- : "https://codebility-cdn.pages.dev/assets/images/default-avatar-1248x845.jpg" ;
56+ const imageUrl = useMemo ( ( ) =>
57+ main_image
58+ ? main_image . startsWith ( "public" )
59+ ? `${ process . env . NEXT_PUBLIC_SUPABASE_URL } /storage/v1/object/public/services-image/${ main_image } `
60+ : main_image
61+ : "https://codebility-cdn.pages.dev/assets/images/default-avatar-1248x845.jpg"
62+ , [ main_image ] ) ;
5963
60- const hasValidWebsite =
64+ const hasValidWebsite = useMemo ( ( ) =>
6165 website_url &&
6266 website_url !== "" &&
6367 website_url . toLowerCase ( ) !== "n/a" &&
64- website_url !== "." ;
68+ website_url !== "."
69+ , [ website_url ] ) ;
6570
66- const teamLeader = members . length > 0 ? members [ 0 ] : null ;
67- const teamMembers = members . length > 1 ? members . slice ( 1 ) : [ ] ;
68- const isDescriptionLong = description && description . length > 120 ;
71+ const { teamLeader, teamMembers, isDescriptionLong } = useMemo ( ( ) => ( {
72+ teamLeader : members . length > 0 ? members [ 0 ] : null ,
73+ teamMembers : members . length > 1 ? members . slice ( 1 ) : [ ] ,
74+ isDescriptionLong : description && description . length > 120
75+ } ) , [ members , description ] ) ;
6976
7077 return (
71- < div className = "border-light-900/5 bg-light-700/10 flex h-full flex-1 flex-col rounded-lg border-2 p-4 text-white" >
72- < div className = "relative aspect-video w-full overflow-hidden rounded-lg" >
78+ < div
79+ className = "group relative flex h-full flex-col overflow-hidden rounded-2xl bg-white/5 border border-white/10 backdrop-blur-sm transition-all duration-500 hover:shadow-2xl hover:shadow-blue-500/20 hover:border-blue-400/30 hover:bg-white/10 transform-gpu hover:-translate-y-2 hover:scale-105"
80+ style = { {
81+ transformStyle : 'preserve-3d' ,
82+ perspective : '1000px' ,
83+ } }
84+ onMouseEnter = { ( e ) => {
85+ const card = e . currentTarget ;
86+ const rect = card . getBoundingClientRect ( ) ;
87+ const x = e . clientX - rect . left ;
88+ const y = e . clientY - rect . top ;
89+ const centerX = rect . width / 2 ;
90+ const centerY = rect . height / 2 ;
91+ const rotateX = ( y - centerY ) / 25 ;
92+ const rotateY = ( centerX - x ) / 25 ;
93+
94+ card . style . transform = `perspective(1000px) rotateX(${ rotateX } deg) rotateY(${ rotateY } deg) translateY(-4px) scale(1.01)` ;
95+ } }
96+ onMouseLeave = { ( e ) => {
97+ const card = e . currentTarget ;
98+ card . style . transform = 'perspective(1000px) rotateX(0deg) rotateY(0deg) translateY(0px) scale(1)' ;
99+ } }
100+ >
101+ < div className = "relative aspect-[4/3] w-full overflow-hidden" >
73102 < Image
74103 src = { imageUrl }
75104 alt = { name }
76105 fill
77106 sizes = "(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw"
78- className = "object-cover"
79- priority
80- quality = { 90 }
107+ className = "object-cover transition-all duration-500 group-hover:scale-105 group-hover:brightness-105 "
108+ loading = "lazy"
109+ quality = { 75 }
81110 />
82111 { project_category_name && (
83- < div className = "absolute left-2 top-2 rounded-md bg-customBlue-600/80 px-2 py-1 text-xs text-white" >
112+ < div className = "absolute left-3 top-3 inline-flex items-center px-3 py-1.5 text-xs font-semibold text-white rounded-full backdrop-blur-sm shadow-lg bg-blue-600/80 transition-all duration-300 group-hover:scale-105 group-hover:shadow-lg group-hover:shadow-blue-500/20 " >
84113 { project_category_name }
85114 </ div >
86115 ) }
116+
117+ { /* 3D Overlay Effect */ }
118+ < div className = "absolute inset-0 bg-gradient-to-br from-transparent via-transparent to-blue-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500" > </ div >
87119 </ div >
88120
89- < div className = "flex flex-1 flex-col pt-4" >
90- < h3 className = "mb-3 line-clamp-1 text-lg font-medium lg:text-2xl" >
91- { name }
92- </ h3 >
93-
94- < div className = "mb-4 min-h-[4.5rem]" >
95- < p
96- className = { `text-gray text-sm ${ ! isDescriptionExpanded ? "line-clamp-3" : "" } ` }
97- >
98- { description || "No description available." }
99- </ p >
100- { isDescriptionLong && (
101- < button
102- onClick = { ( ) => setIsDescriptionExpanded ( ! isDescriptionExpanded ) }
103- className = "mt-1 text-xs text-customBlue-400 hover:text-customBlue-300"
121+ < div className = "flex flex-1 flex-col p-4 text-white transition-all duration-500 group-hover:translate-z-4" >
122+ { /* Main Content */ }
123+ < div className = "flex-1 space-y-3" >
124+ < h3 className = "line-clamp-1 text-lg font-bold group-hover:text-blue-300 transition-colors lg:text-xl" >
125+ { name }
126+ </ h3 >
127+
128+ < div className = "min-h-[4rem]" >
129+ < p
130+ className = { `text-gray-300 text-sm leading-relaxed ${ ! isDescriptionExpanded ? "line-clamp-3" : "" } ` }
104131 >
105- { isDescriptionExpanded ? "Show less" : "Read more" }
106- </ button >
132+ { description || "No description available." }
133+ </ p >
134+ { isDescriptionLong && (
135+ < button
136+ onClick = { ( ) => setIsDescriptionExpanded ( ! isDescriptionExpanded ) }
137+ className = "mt-1 text-xs text-blue-400 hover:text-blue-300 transition-colors"
138+ >
139+ { isDescriptionExpanded ? "Show less" : "Read more" }
140+ </ button >
141+ ) }
142+ </ div >
143+
144+ { /* Tech Stack */ }
145+ { tech_stack && tech_stack . length > 0 && (
146+ < div className = "space-y-2" >
147+ < h4 className = "text-xs font-semibold text-gray-400 uppercase tracking-wide" >
148+ Tech Stack
149+ </ h4 >
150+ < div className = "flex flex-wrap gap-1.5" >
151+ { tech_stack . slice ( 0 , 4 ) . map ( ( tech , index ) => (
152+ < span
153+ key = { index }
154+ className = "inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-gradient-to-r from-blue-500/20 to-indigo-500/20 text-blue-300 border border-blue-500/30 hover:shadow-sm hover:shadow-blue-500/20 transition-all duration-300 hover:scale-105 hover:bg-gradient-to-r hover:from-blue-500/30 hover:to-indigo-500/30 hover:border-blue-400/40 animate-fade-in-up group-hover:translate-y-[-1px]"
155+ style = { {
156+ animationDelay : `${ index * 100 } ms` ,
157+ animationFillMode : 'both'
158+ } }
159+ >
160+ { tech }
161+ </ span >
162+ ) ) }
163+ { tech_stack . length > 4 && (
164+ < span
165+ className = "inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-gradient-to-r from-gray-600/20 to-gray-500/20 text-gray-300 border border-gray-500/30 hover:scale-110 transition-all duration-300 animate-fade-in-up"
166+ style = { {
167+ animationDelay : `${ 4 * 100 } ms` ,
168+ animationFillMode : 'both'
169+ } }
170+ >
171+ +{ tech_stack . length - 4 } more
172+ </ span >
173+ ) }
174+ </ div >
175+ </ div >
107176 ) }
108177 </ div >
109178
110- < div className = "mb-4 h-10" >
179+ { /* Bottom Section */ }
180+ < div className = "space-y-4 mt-auto" >
181+ { /* Website Link */ }
111182 { hasValidWebsite && (
112183 < Link
113184 href = { website_url ! }
114185 target = "_blank"
115- className = "inline-flex items-center gap-2 rounded-md bg-customBlue-600 px-4 py-2 text-sm transition-colors hover:bg-customBlue-700 "
186+ className = "inline-flex items-center gap-2 rounded-xl bg-white/10 hover:bg-gradient-to-r hover:from-blue-500/15 hover:to-indigo-500/15 border border-white/20 hover:border-blue-400/30 px-4 py-2 text-sm font-medium transition-all duration-300 hover:scale-105 hover:shadow-md hover:shadow-blue-500/15 hover:translate-y-[-1px] "
116187 >
117188 < IconLink className = "size-4" />
118189 < span > View Website</ span >
119190 </ Link >
120191 ) }
121- </ div >
122192
123- < div className = "mt-auto min-h-[80px]" >
193+ { /* Team Section */ }
124194 { members . length > 0 ? (
125195 < div >
126- < p className = "mb-2 text-sm text-gray-400" > Team</ p >
196+ < p className = "mb-2 text-xs font-semibold text-gray-400 uppercase tracking-wide " > Team</ p >
127197 < div className = "flex flex-wrap items-end" >
128198 { teamLeader && (
129199 < Tooltip >
@@ -198,3 +268,5 @@ export default function ServiceCard({ service }: Props) {
198268 </ div >
199269 ) ;
200270}
271+
272+ export default memo ( ServiceCard ) ;
0 commit comments