Skip to content

Commit 1f9bf7b

Browse files
committed
feat(landing): add Supporters Section with Marquee Animation
- Introduced a new Supporters component to showcase logos of supporting companies. - Implemented marquee animations for horizontal and vertical scrolling of logos. - Added corresponding CSS keyframes for marquee effects. - Included SVG logos for Canva, Cloudflare, Fibery, GitHub, Monday, Okta, Sentry, TechSoup. - Updated the main page to include the Supporters section.
1 parent bf21fa1 commit 1f9bf7b

File tree

11 files changed

+308
-0
lines changed

11 files changed

+308
-0
lines changed

app/globals.css

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,24 @@
145145
}
146146
}
147147

148+
@keyframes marquee {
149+
from {
150+
transform: translateX(0);
151+
}
152+
to {
153+
transform: translateX(calc(-100% - 1rem));
154+
}
155+
}
156+
157+
@keyframes marquee-vertical {
158+
from {
159+
transform: translateY(0);
160+
}
161+
to {
162+
transform: translateY(calc(-100% - 1rem));
163+
}
164+
}
165+
148166
@layer utilities {
149167
.animate-typing {
150168
animation:
@@ -157,6 +175,14 @@
157175
animation: typing 2.5s steps(30, end) forwards;
158176
width: 0;
159177
}
178+
179+
.animate-marquee {
180+
animation: marquee var(--duration, 20s) linear infinite;
181+
}
182+
183+
.animate-marquee-vertical {
184+
animation: marquee-vertical var(--duration, 20s) linear infinite;
185+
}
160186
}
161187

162188
/* Custom prose overrides for dark mode */

app/page.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import DonateCta from '@/components/pages/home/donate-cta';
22
import Hero from '@/components/pages/home/hero';
33
import Projects from '@/components/pages/home/projects';
44
import Stats from '@/components/pages/home/stats';
5+
import Supporters from '@/components/pages/home/supporters';
56
import Testimonials from '@/components/pages/home/testimonials';
67
import { getPageMetadata } from './metadata';
78
import type { Metadata } from 'next';
@@ -42,6 +43,13 @@ export default function Home() {
4243
</div>
4344
</section>
4445

46+
{/* Supporters - regular background */}
47+
<section className="py-16 md:py-20">
48+
<div className="container mx-auto px-4">
49+
<Supporters />
50+
</div>
51+
</section>
52+
4553
{/* Donate CTA - regular background */}
4654
<section className="py-16 md:py-20">
4755
<div className="container mx-auto px-4">
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
'use client';
2+
3+
import { memo, useState } from 'react';
4+
5+
import Image from 'next/image';
6+
7+
import Marquee from '@/components/ui/marquee';
8+
9+
const SUPPORTERS = [
10+
{
11+
name: 'Canva',
12+
logo: '/images/supporters/canva.svg',
13+
},
14+
{
15+
name: 'Cloudflare',
16+
logo: '/images/supporters/cloudflare.svg',
17+
},
18+
{
19+
name: 'TechSoup',
20+
logo: '/images/supporters/techsoup.svg',
21+
},
22+
{
23+
name: 'Fibery',
24+
logo: '/images/supporters/fibery.svg',
25+
},
26+
{
27+
name: 'Monday',
28+
logo: '/images/supporters/monday.png',
29+
},
30+
{
31+
name: 'Okta',
32+
logo: '/images/supporters/okta.svg',
33+
},
34+
{
35+
name: 'GitHub',
36+
logo: '/images/supporters/github.png',
37+
},
38+
{
39+
name: 'Sentry',
40+
logo: '/images/supporters/sentry.svg',
41+
},
42+
] as const;
43+
44+
const SupporterLogo = memo(({
45+
name,
46+
logo,
47+
onHover
48+
}: {
49+
name: string;
50+
logo: string;
51+
onHover: (hovering: boolean) => void;
52+
}) => {
53+
const isSvg = logo.endsWith('.svg');
54+
const isMonday = name === 'Monday';
55+
const isGitHub = name === 'GitHub';
56+
const isTechSoup = name === 'TechSoup';
57+
58+
let logoClassName = 'h-12 w-auto object-contain transition-all duration-300 opacity-95 hover:opacity-100 hover:scale-105';
59+
const logoStyle: React.CSSProperties = {
60+
filter: isSvg && !isTechSoup ? 'brightness(0) saturate(100%) invert(1)' : 'none',
61+
};
62+
63+
if (isMonday) {
64+
logoClassName = 'h-[48px] w-auto max-w-[180px] object-contain transition-all duration-300 opacity-95 hover:opacity-100 hover:scale-105';
65+
} else if (isGitHub) {
66+
logoClassName = 'h-[48px] w-auto max-w-[180px] object-contain transition-all duration-300 opacity-95 hover:opacity-100 hover:scale-105';
67+
} else if (isTechSoup) {
68+
logoClassName = 'h-[60px] w-auto max-w-[240px] object-contain transition-all duration-300 opacity-95 hover:opacity-100 hover:scale-105';
69+
}
70+
71+
return (
72+
<div
73+
className="flex items-center justify-center px-12 py-6"
74+
role="presentation"
75+
onMouseEnter={() => onHover(true)}
76+
onMouseLeave={() => onHover(false)}
77+
>
78+
<Image
79+
src={logo}
80+
alt={`${name} logo`}
81+
width={240}
82+
height={80}
83+
className={logoClassName}
84+
style={logoStyle}
85+
unoptimized={isSvg}
86+
onError={(e) => {
87+
console.error(`Failed to load logo: ${logo}`, e);
88+
}}
89+
/>
90+
</div>
91+
);
92+
});
93+
94+
SupporterLogo.displayName = 'SupporterLogo';
95+
96+
const Supporters = memo(() => {
97+
const [isHovering, setIsHovering] = useState(false);
98+
99+
return (
100+
<section className="py-4 md:py-6 relative overflow-hidden">
101+
<div className="mx-auto max-w-7xl">
102+
<div className="mx-auto max-w-2xl text-center mb-12">
103+
<h2 className="mb-4 text-2xl font-semibold md:text-3xl">
104+
Thank you to our supporters
105+
</h2>
106+
<p className="text-base text-muted-foreground">
107+
We&apos;re grateful to these companies for their generous support through
108+
discounted rates, donations, and special plans that help us serve our
109+
community.
110+
</p>
111+
</div>
112+
113+
<div className="relative">
114+
<div className="absolute inset-y-0 left-0 w-20 bg-linear-to-r from-background to-transparent z-10 pointer-events-none" />
115+
<div className="absolute inset-y-0 right-0 w-20 bg-linear-to-l from-background to-transparent z-10 pointer-events-none" />
116+
117+
<div className={isHovering ? '**:[animation-play-state:paused]' : ''}>
118+
<Marquee
119+
className="[--duration:40s] gap-8"
120+
repeat={3}
121+
aria-label="Supporters logos"
122+
>
123+
{SUPPORTERS.map((supporter) => (
124+
<SupporterLogo
125+
key={supporter.name}
126+
{...supporter}
127+
onHover={setIsHovering}
128+
/>
129+
))}
130+
</Marquee>
131+
</div>
132+
</div>
133+
</div>
134+
</section>
135+
);
136+
});
137+
138+
Supporters.displayName = 'Supporters';
139+
140+
export default Supporters;

public/images/supporters/canva.svg

Lines changed: 31 additions & 0 deletions
Loading
Lines changed: 26 additions & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading
36.7 KB
Loading
21.1 KB
Loading

public/images/supporters/okta.svg

Lines changed: 51 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)