Skip to content

Commit 8352e44

Browse files
Zeff01claude
andcommitted
feat: add minimalist techy background animation to services page
- Created TechyBackground component with flowing gradient waves - Added subtle floating orbs with theme-matching colors - Integrated animation into services page with proper z-index layering - Uses customBlue theme colors (#583DFF, #404993) for brand consistency - Ultra-minimal design with low opacity for non-intrusive experience 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent e81aba3 commit 8352e44

97 files changed

Lines changed: 3038 additions & 801 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/codebility/app/(marketing)/_components/MarketingSection.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ interface SectionProps {
88

99
const Section: React.FC<SectionProps> = ({ children, className = "", id }) => {
1010
return (
11-
<section id={id} className={`mx-auto py-8 lg:py-16 ${className}`}>
12-
<div className="flex flex-col items-center justify-center space-y-16">
11+
<section id={id} className={`mx-auto py-4 lg:py-8 ${className}`}>
12+
<div className="flex flex-col items-center justify-center space-y-8">
1313
{children}
1414
</div>
1515
</section>

‎apps/codebility/app/(marketing)/careers/_components/CodevsOrbitingCirclesBg.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
IconNodeJS,
44
IconReact,
55
IconTailwind,
6-
} from "@/public/assets/svgs";
6+
} from "@/public/assets/svgs/techstack";
77

88
import OrbitingCircles from "./CodevsOrbitingCircles";
99

‎apps/codebility/app/(marketing)/codevs/_components/CodevsOrbitingCirclesBg.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
IconNodeJS,
44
IconReact,
55
IconTailwind,
6-
} from "@/public/assets/svgs";
6+
} from "@/public/assets/svgs/techstack";
77

88
import OrbitingCircles from "./CodevsOrbitingCircles";
99

‎apps/codebility/app/(marketing)/hire-a-codev/_components/CodevsOrbitingCirclesBg.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
IconNodeJS,
44
IconReact,
55
IconTailwind,
6-
} from "@/public/assets/svgs";
6+
} from "@/public/assets/svgs/techstack";
77

88
import OrbitingCircles from "./CodevsOrbitingCircles";
99

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

Lines changed: 112 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from "react";
1+
import { useState, memo, useMemo } from "react";
22
import Image from "next/image";
33
import Link from "next/link";
44
import 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

3839
interface 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);

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

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

3-
import { useEffect, useState } from "react";
3+
import { useEffect, useState, useMemo, memo } from "react";
44
import DefaultPagination from "@/components/ui/pagination";
55
import { CATEGORIES, pageSize } from "@/constants";
66
import usePagination from "@/hooks/use-pagination";
@@ -34,6 +34,7 @@ interface ProjectData {
3434
project_category_name?: string;
3535
created_at?: string;
3636
updated_at?: string;
37+
tech_stack?: string[];
3738
}
3839

3940
interface Props {
@@ -43,16 +44,11 @@ interface Props {
4344
// Define a special ID for the "All" category
4445
const ALL_CATEGORY_ID = 0;
4546

46-
export default function ServicesTab({ servicesData }: Props) {
47+
function ServicesTab({ servicesData }: Props) {
4748
// Initialize with "All" category
4849
const [currentCategory, setCurrentCategory] =
4950
useState<number>(ALL_CATEGORY_ID);
5051

51-
const [projects, setProjects] = useState(() => {
52-
// Start with all projects for the "All" category
53-
return servicesData;
54-
});
55-
5652
const [tabPages, setTabPages] = useState(() => {
5753
const pages: Record<number, number> = {
5854
[ALL_CATEGORY_ID]: 1, // Add "All" category to tracked pages
@@ -63,6 +59,15 @@ export default function ServicesTab({ servicesData }: Props) {
6359
return pages;
6460
});
6561

62+
// Memoize filtered projects to prevent unnecessary recalculations
63+
const projects = useMemo(() => {
64+
return currentCategory === ALL_CATEGORY_ID
65+
? servicesData
66+
: servicesData.filter(
67+
(project) => project.project_category_id === currentCategory,
68+
);
69+
}, [currentCategory, servicesData]);
70+
6671
const {
6772
paginatedData: paginatedProjects,
6873
currentPage,
@@ -72,20 +77,10 @@ export default function ServicesTab({ servicesData }: Props) {
7277
setCurrentPage,
7378
} = usePagination(projects, pageSize.services);
7479

75-
// Update projects when category changes
80+
// Update current page when category changes
7681
useEffect(() => {
77-
// If "All" category is selected, show all projects
78-
// Otherwise, filter by the selected category
79-
const filteredData =
80-
currentCategory === ALL_CATEGORY_ID
81-
? servicesData
82-
: servicesData.filter(
83-
(project) => project.project_category_id === currentCategory,
84-
);
85-
86-
setProjects(filteredData);
8782
setCurrentPage(tabPages[currentCategory] || 1);
88-
}, [currentCategory, servicesData]);
83+
}, [currentCategory, setCurrentPage, tabPages]);
8984

9085
const handleTabClick = (categoryId: number) => {
9186
setTabPages((prev) => ({
@@ -98,39 +93,48 @@ export default function ServicesTab({ servicesData }: Props) {
9893
return (
9994
<Section className="relative">
10095
<Container className="relative z-0">
101-
<div className="flex flex-col gap-10">
96+
<div className="flex flex-col gap-4">
10297
{/* Category Tabs with "All" option */}
103-
<div className="mx-auto flex flex-wrap justify-center gap-5 xl:gap-16">
104-
<p
98+
<div className="mx-auto flex flex-wrap justify-center gap-2 p-1 bg-white/10 dark:bg-white/5 rounded-2xl backdrop-blur-sm border border-white/20">
99+
<button
105100
onClick={() => handleTabClick(ALL_CATEGORY_ID)}
106-
className={`cursor-pointer px-2 pb-2 text-customViolet-200 xl:text-xl ${
101+
className={`px-3 py-1.5 rounded-xl text-sm font-semibold transition-all duration-200 ${
107102
currentCategory === ALL_CATEGORY_ID
108-
? "border-customViolet-100 text-customViolet-100border-b-2"
109-
: "text-white"
103+
? "bg-white text-gray-900 shadow-lg"
104+
: "text-white hover:bg-white/20 hover:text-white"
110105
}`}
111106
>
112107
All
113-
</p>
108+
</button>
114109
{CATEGORIES.map((cat) => (
115-
<p
110+
<button
116111
key={cat.id}
117112
onClick={() => handleTabClick(cat.id)}
118-
className={`cursor-pointer px-2 pb-2 text-customViolet-200 xl:text-xl ${
113+
className={`px-3 py-1.5 rounded-xl text-sm font-semibold transition-all duration-200 ${
119114
currentCategory === cat.id
120-
? "border-customViolet-100 text-customViolet-100 border-b-2"
121-
: "text-white"
115+
? "bg-white text-gray-900 shadow-lg"
116+
: "text-white hover:bg-white/20 hover:text-white"
122117
}`}
123118
>
124119
{cat.name}
125-
</p>
120+
</button>
126121
))}
127122
</div>
128123

129124
{/* Projects Grid */}
130-
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3 lg:gap-5">
125+
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
131126
{paginatedProjects && paginatedProjects.length > 0 ? (
132-
paginatedProjects.map((project) => (
133-
<ServiceCard key={project.id} service={project} />
127+
paginatedProjects.map((project, index) => (
128+
<div
129+
key={project.id}
130+
className="animate-fade-in-up"
131+
style={{
132+
animationDelay: `${index * 150}ms`,
133+
animationFillMode: 'both'
134+
}}
135+
>
136+
<ServiceCard service={project} />
137+
</div>
134138
))
135139
) : (
136140
<div className="col-span-full text-center text-white">
@@ -156,3 +160,5 @@ export default function ServicesTab({ servicesData }: Props) {
156160
</Section>
157161
);
158162
}
163+
164+
export default memo(ServicesTab);

0 commit comments

Comments
 (0)