Skip to content

Commit 5f11199

Browse files
Merge pull request #335 from uidotdev/lf/launch-banner
Add reactgg launch banner
2 parents 90fbbb4 + 31d2ebc commit 5f11199

File tree

4 files changed

+230
-0
lines changed

4 files changed

+230
-0
lines changed

usehooks.com/public/img/banner-sale-reactgg.svg

Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { Fragment, useEffect, useState } from "react";
2+
3+
interface CountdownProps {
4+
targetDate: string; // YYYY-MM-DD format
5+
}
6+
7+
interface TimeLeft {
8+
days: number;
9+
hours: number;
10+
minutes: number;
11+
seconds: number;
12+
}
13+
14+
function calculateTimeLeft(targetDate: string): TimeLeft {
15+
const target = new Date(`${targetDate}T00:00:00-08:00`);
16+
const now = new Date();
17+
const difference = +target - +now;
18+
19+
if (difference <= 0) {
20+
return {
21+
days: 0,
22+
hours: 0,
23+
minutes: 0,
24+
seconds: 0,
25+
};
26+
}
27+
28+
return {
29+
days: Math.floor(difference / (1000 * 60 * 60 * 24)),
30+
hours: Math.floor((difference / (1000 * 60 * 60)) % 24),
31+
minutes: Math.floor((difference / 1000 / 60) % 60),
32+
seconds: Math.floor((difference / 1000) % 60),
33+
};
34+
}
35+
36+
const formatNumber = (number: number) => number.toString().padStart(2, "0");
37+
38+
const Countdown: React.FC<CountdownProps> = ({ targetDate }) => {
39+
const [timeLeft, setTimeLeft] = useState<TimeLeft>(
40+
calculateTimeLeft(targetDate)
41+
);
42+
43+
useEffect(() => {
44+
const timer = setInterval(() => {
45+
const newTimeLeft = calculateTimeLeft(targetDate);
46+
setTimeLeft(newTimeLeft);
47+
if (
48+
newTimeLeft.days === 0 &&
49+
newTimeLeft.hours === 0 &&
50+
newTimeLeft.minutes === 0 &&
51+
newTimeLeft.seconds === 0
52+
) {
53+
clearInterval(timer);
54+
}
55+
}, 1000);
56+
57+
return () => clearInterval(timer);
58+
}, [targetDate]);
59+
60+
if (
61+
timeLeft.days === 0 &&
62+
timeLeft.hours === 0 &&
63+
timeLeft.minutes === 0 &&
64+
timeLeft.seconds === 0
65+
) {
66+
return null;
67+
}
68+
69+
return (
70+
<div className="countdown flex gap-1 justify-center text-md">
71+
{["days", "hours", "minutes", "seconds"].map((unit, index) => (
72+
<Fragment key={unit}>
73+
{index > 0 && <span className="countdown-colon pt-1 grid justify-center">:</span>}
74+
<div className={`${unit} grid grid-cols-2 gap-x-1 gap-y-1.5`}>
75+
<span className="countdown-number h-[2.2em] aspect-[6/7] grid place-content-center rounded-sm bg-brand-beige font-semibold">
76+
{formatNumber(timeLeft[unit as keyof TimeLeft]).charAt(0)}
77+
</span>
78+
<span className="countdown-number h-[2.2em] aspect-[6/7] grid place-content-center rounded-sm bg-brand-beige font-semibold">
79+
{formatNumber(timeLeft[unit as keyof TimeLeft]).charAt(1)}
80+
</span>
81+
<p className="countdown-label col-span-full text-sm">{unit}</p>
82+
</div>
83+
</Fragment>
84+
))}
85+
</div>
86+
);
87+
};
88+
89+
export default Countdown;
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
---
2+
import Button from "./Button.astro";
3+
import CountdownTimer from "./CountdownTimer";
4+
---
5+
6+
<aside class="reactgg-container container">
7+
<div class="reactgg-banner">
8+
<a href="https://react.gg?s=usehooks" class="reactgg-header">
9+
<img
10+
src={"/img/banner-sale-reactgg.svg"}
11+
alt="react.gg - The interactive way to master modern React"
12+
class="reactgg-headline"
13+
/>
14+
</a>
15+
<div class="reactgg-spacer"></div>
16+
<div class="reactgg-cta">
17+
<div class="reactgg-cta-container">
18+
<h2>react.gg Launch Sale</h2>
19+
<p>Up to 25% off through May 16th</p>
20+
<CountdownTimer client:only targetDate="2025-05-16" />
21+
<Button
22+
text="Join Now"
23+
size="small"
24+
href="https://react.gg?s=usehooks"
25+
type="link"
26+
color="yellow"
27+
class="join-now mt-4 mb-1 lg:mb-2 inline-block"
28+
/>
29+
</div>
30+
</div>
31+
</div>
32+
</aside>
33+
34+
<style>
35+
.reactgg-container {
36+
width: 100%;
37+
max-width: 1300px;
38+
margin: 0 auto 30px;
39+
}
40+
41+
.reactgg-banner {
42+
width: 100%;
43+
background-color: var(--brand-charcoal);
44+
border: 2px solid var(--brand-charcoal);
45+
border-radius: 8px;
46+
box-shadow: 0.2rem 0.2rem 0 var(--brand-charcoal);
47+
overflow: hidden;
48+
@media (min-width: 1024px) {
49+
display: grid;
50+
grid-template-columns: 1fr 40px 1fr;
51+
}
52+
}
53+
54+
.reactgg-header {
55+
display: grid;
56+
align-self: center;
57+
place-items: center;
58+
align-content: center;
59+
height: 190px;
60+
overflow: hidden;
61+
@media (min-width: 1024px) {
62+
height: 280px;
63+
grid-column: 1 / 3;
64+
grid-row: 1;
65+
}
66+
}
67+
68+
.reactgg-headline {
69+
width: 110%;
70+
max-width: none;
71+
}
72+
73+
.reactgg-spacer {
74+
width: 40px;
75+
margin-left: 20px;
76+
display: none;
77+
background-color: var(--brand-green);
78+
z-index: 0;
79+
transform: skew(-7deg);
80+
@media (min-width: 1024px) {
81+
display: block;
82+
grid-column: 2;
83+
grid-row: 1;
84+
height: 100%;
85+
}
86+
}
87+
88+
.reactgg-cta {
89+
padding: 0.5rem;
90+
display: grid;
91+
justify-content: center;
92+
background-color: var(--brand-green);
93+
color: var(--brand-charcoal);
94+
z-index: 10;
95+
@media (min-width: 1024px) {
96+
padding-bottom: 0;
97+
grid-row: 1;
98+
grid-column: 3;
99+
}
100+
}
101+
102+
.reactgg-cta-container {
103+
margin-top: 0.5rem;
104+
margin-bottom: 0.5rem;
105+
place-self: center;
106+
text-align: center;
107+
text-transform: uppercase;
108+
109+
h2 {
110+
margin-top: 0;
111+
margin-bottom: 0.5rem;
112+
font-size: 1.25rem;
113+
line-height: 1.75rem;
114+
font-weight: 600;
115+
color: var(--brand-coal);
116+
@media (min-width: 1024px) {
117+
font-size: 1.5rem;
118+
line-height: 2rem;
119+
}
120+
@media (min-width: 1280px) {
121+
font-size: 1.875rem;
122+
line-height: 2.25rem;
123+
}
124+
}
125+
126+
h2 + p {
127+
margin-top: 0;
128+
margin-bottom: 1.2rem;
129+
text-transform: none;
130+
font-size: 90%;
131+
}
132+
}
133+
134+
:global(.countdown-colon),
135+
:global(.countdown-number) {
136+
font-size: clamp(1rem, 4vw, 1.5rem);
137+
}
138+
</style>

usehooks.com/src/pages/index.astro

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import NavMain from "../sections/NavMain.astro";
55
import HomeHero from "../sections/HomeHero.astro";
66
import HooksList from "../components/search/HooksList";
77
import Footer from "../sections/Footer.astro";
8+
import QueryGGBanner from "../components/QueryGGBanner.astro";
89
910
const hooks = await getCollection("hooks");
1011
---
@@ -13,6 +14,7 @@ const hooks = await getCollection("hooks");
1314
title="useHooks – The React Hooks Library"
1415
description="A collection of modern, server-safe React hooks – from the ui.dev team"
1516
>
17+
<QueryGGBanner />
1618
<NavMain />
1719
<HomeHero />
1820
<HooksList client:load hooks={hooks} />

0 commit comments

Comments
 (0)