Skip to content

Commit 6244192

Browse files
Add query.gg launch sale banner (#395)
* add launch banners to query page and docs * style: prettier formatting --------- Co-authored-by: SeanCassiere <33615041+SeanCassiere@users.noreply.github.com>
1 parent 775ff6f commit 6244192

File tree

5 files changed

+254
-5
lines changed

5 files changed

+254
-5
lines changed

app/components/CountdownTimer.tsx

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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="flex gap-2 justify-center">
71+
{['days', 'hours', 'minutes', 'seconds'].map((unit, index) => (
72+
<Fragment key={unit}>
73+
{index > 0 && (
74+
<span className="h-[2rem] grid place-content-center">:</span>
75+
)}
76+
77+
<div className={`${unit} grid grid-cols-2 gap-x-1 gap-y-1.5`}>
78+
<span className="h-[2.3rem] aspect-[6/7] grid place-content-center rounded-sm bg-[#f9f4da] bg-opacity-10 font-semibold">
79+
{formatNumber(timeLeft[unit as keyof TimeLeft]).charAt(0)}
80+
</span>
81+
<span className="h-[2.3rem] aspect-[6/7] grid place-content-center rounded-sm bg-[#f9f4da] bg-opacity-10 font-semibold">
82+
{formatNumber(timeLeft[unit as keyof TimeLeft]).charAt(1)}
83+
</span>
84+
<p className="col-span-full text-xs">{unit}</p>
85+
</div>
86+
</Fragment>
87+
))}
88+
</div>
89+
)
90+
}
91+
92+
export default Countdown
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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+
}
12+
13+
function calculateTimeLeft(targetDate: string): TimeLeft {
14+
const target = new Date(`${targetDate}T00:00:00-08:00`)
15+
const now = new Date()
16+
const difference = +target - +now
17+
18+
if (difference <= 0) {
19+
return {
20+
days: 0,
21+
hours: 0,
22+
minutes: 0,
23+
}
24+
}
25+
26+
return {
27+
days: Math.floor(difference / (1000 * 60 * 60 * 24)),
28+
hours: Math.floor((difference / (1000 * 60 * 60)) % 24),
29+
minutes: Math.floor((difference / 1000 / 60) % 60),
30+
}
31+
}
32+
33+
const formatNumber = (number: number) => number.toString().padStart(2, '0')
34+
35+
const Countdown: React.FC<CountdownProps> = ({ targetDate }) => {
36+
const [timeLeft, setTimeLeft] = useState<TimeLeft>(
37+
calculateTimeLeft(targetDate)
38+
)
39+
40+
useEffect(() => {
41+
const timer = setInterval(() => {
42+
const newTimeLeft = calculateTimeLeft(targetDate)
43+
setTimeLeft(newTimeLeft)
44+
if (
45+
newTimeLeft.days === 0 &&
46+
newTimeLeft.hours === 0 &&
47+
newTimeLeft.minutes === 0
48+
) {
49+
clearInterval(timer)
50+
}
51+
}, 1000)
52+
53+
return () => clearInterval(timer)
54+
}, [targetDate])
55+
56+
if (timeLeft.days === 0 && timeLeft.hours === 0 && timeLeft.minutes === 0) {
57+
return null
58+
}
59+
60+
return (
61+
<div className="countdown flex gap-1.5 justify-center">
62+
{['days', 'hours', 'minutes'].map((unit, index) => (
63+
<Fragment key={unit}>
64+
{index > 0 && (
65+
<span className="h-[1.4em] grid place-content-center">:</span>
66+
)}
67+
68+
<div className={`${unit} grid grid-cols-2 gap-x-1 gap-y-1.5`}>
69+
<span className="h-[1.8em] w-[1.7em] grid place-content-center rounded-sm bg-gray-200 bg-opacity-75 dark:bg-gray-600 dark:bg-opacity-50 text-sm font-semibold">
70+
{formatNumber(timeLeft[unit as keyof TimeLeft]).charAt(0)}
71+
</span>
72+
<span className="h-[1.8em] w-[1.7em] grid place-content-center rounded-sm bg-gray-200 bg-opacity-75 dark:bg-gray-600 dark:bg-opacity-50 text-sm font-semibold">
73+
{formatNumber(timeLeft[unit as keyof TimeLeft]).charAt(1)}
74+
</span>
75+
<p className="col-span-full text-[.65rem]">{unit}</p>
76+
</div>
77+
</Fragment>
78+
))}
79+
</div>
80+
)
81+
}
82+
83+
export default Countdown

app/components/DocsCalloutQueryGG.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { LogoQueryGGSmall } from '~/components/LogoQueryGGSmall'
2+
import CountdownTimerSmall from '~/components/CountdownTimerSmall'
23
import { useQueryGGPPPDiscount } from '~/hooks/useQueryGGPPPDiscount'
34

45
export function DocsCalloutQueryGG() {
@@ -17,11 +18,22 @@ export function DocsCalloutQueryGG() {
1718
</h6>
1819
<LogoQueryGGSmall className="w-full" />
1920

20-
<blockquote className="text-sm -indent-[.45em] pl-2">
21+
{/*<blockquote className="text-sm -indent-[.45em] pl-2">
2122
“If you're serious about *really* understanding React Query, there's
2223
no better way than with query.gg”
2324
<cite className="italic block text-right">—Tanner Linsley</cite>
24-
</blockquote>
25+
</blockquote>*/}
26+
27+
{/*<div className="grid justify-center bg-gray-800 dark:bg-gray-100 text-gray-100 dark:text-gray-800 z-10">*/}
28+
<div className="p-2 uppercase text-center place-self-center">
29+
<h2 className="mt-1 mb-1 px-2 text-md font-semibold">
30+
Launch week sale
31+
</h2>
32+
<p className="normal-case mb-4 text-sm text-balance">
33+
Up to 25% off through May 10th
34+
</p>
35+
<CountdownTimerSmall targetDate="2025-05-10" />
36+
</div>
2537

2638
{ppp && (
2739
<>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import headerCourse from '~/images/query-header-course.svg'
2+
import cornerTopLeft from '~/images/query-corner-top-left.svg'
3+
import cornerTopRight from '~/images/query-corner-top-right.svg'
4+
import cornerFishBottomRight from '~/images/query-corner-fish-bottom-right.svg'
5+
import CountdownTimer from '~/components/CountdownTimer'
6+
7+
export function QueryGGBannerSale(props: React.HTMLProps<HTMLDivElement>) {
8+
return (
9+
<aside
10+
{...props}
11+
className="mx-auto w-full max-w-[1200px] p-8 -mt-32 flex justify-between items-center"
12+
>
13+
<div className="w-full xl:flex xl:gap-6 bg-[#f9f4da] border-4 border-[#231f20]">
14+
<a
15+
href="https://query.gg?s=tanstack"
16+
className="xl:w-[50%] pb-4 grid grid-cols-[70px_1fr_70px] sm:grid-cols-[100px_1fr_100px] md:grid-cols-[140px_1fr_140px] xl:grid-cols-[110px_1fr] 2xl:grid-cols-[150px_1fr]"
17+
>
18+
<img src={cornerTopLeft} alt="sun" className="" />
19+
<img
20+
src={headerCourse}
21+
alt="Query.gg - The Official React Query Course"
22+
className="-mt-[1px] w-10/12 max-w-[400px] justify-self-center"
23+
/>
24+
<img src={cornerTopRight} alt="moon" className="xl:hidden" />
25+
</a>
26+
<div className="hidden xl:block w-[80px] mr-[-60px] bg-[#231f20] border-4 border-r-0 border-[#f9f4da] border-s-[#f9f4da] shadow-[-4px_0_0_#231f20] -skew-x-[15deg] z-0"></div>
27+
<div className="xl:w-[50%] py-2 xl:pb-0 grid xl:grid-cols-[1fr_90px] 2xl:grid-cols-[1fr_120px] justify-center bg-[#231f20] border-2 xl:border-4 xl:border-l-0 border-[#f9f4da] text-[#f9f4da] z-10">
28+
<div className="my-2 uppercase text-center place-self-center">
29+
{/* <h2 className="mt-1 mb-3 px-2 text-sm font-semibold">Launch sale happening now</h2> */}
30+
<h2 className="mb-1 text-lg lg:text-2xl xl:text-3xl font-semibold">
31+
Launch week sale
32+
</h2>
33+
<p className="normal-case mb-4">Up to 25% off through May 10th</p>
34+
<CountdownTimer targetDate="2025-05-10" />
35+
<a
36+
href="https://query.gg?s=tanstack"
37+
className="mt-4 mb-1 xl:mb-2 px-6 py-2 inline-block bg-[#fcba28] text-[#231f20] rounded-full uppercase border border-black cursor-pointer font-black"
38+
>
39+
Join now
40+
</a>
41+
</div>
42+
<img
43+
src={cornerFishBottomRight}
44+
alt="mutated fish"
45+
className="hidden xl:block self-end"
46+
/>
47+
</div>
48+
</div>
49+
</aside>
50+
)
51+
}

app/routes/_libraries/query.$version.index.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { Carbon } from '~/components/Carbon'
77
import { Footer } from '~/components/Footer'
88
import { TbHeartHandshake } from 'react-icons/tb'
99
import SponsorPack from '~/components/SponsorPack'
10-
import { QueryGGBanner } from '~/components/QueryGGBanner'
10+
// import { QueryGGBanner } from '~/components/QueryGGBanner'
11+
import { QueryGGBannerSale } from '~/components/QueryGGBannerSale'
1112
import { queryProject } from '~/libraries/query'
1213
import { createFileRoute } from '@tanstack/react-router'
1314
import { Framework, getBranch, getLibrary } from '~/libraries'
@@ -48,6 +49,7 @@ export default function VersionIndex() {
4849
<div className="flex flex-1 min-h-0 relative justify-center overflow-x-hidden">
4950
<div className="flex flex-col gap-20 md:gap-32 max-w-full py-32">
5051
<div className="flex flex-col items-center gap-8 text-center px-4">
52+
<QueryGGBannerSale />
5153
<h1 className="font-black flex gap-3 items-center text-4xl md:text-6xl lg:text-7xl xl:text-8xl uppercase [letter-spacing:-.05em]">
5254
<span>TanStack</span>
5355
<span className={twMerge(gradientText)}>Query</span>
@@ -83,9 +85,18 @@ export default function VersionIndex() {
8385
>
8486
Read the Docs
8587
</Link>
86-
<p>(or check out our official course 👇)</p>
88+
<p>
89+
(or{' '}
90+
<a
91+
href="https://query.gg?s=tanstack"
92+
className="font-semibold underline"
93+
>
94+
check out our official course
95+
</a>
96+
. It’s on sale!)
97+
</p>
8798
</div>
88-
<QueryGGBanner />
99+
{/* <QueryGGBanner /> */}
89100
</div>
90101
<LibraryFeatureHighlights
91102
featureHighlights={library.featureHighlights}

0 commit comments

Comments
 (0)