From c36494948b03e38e2041ac9bf005986fa44a6325 Mon Sep 17 00:00:00 2001 From: Lynn Fisher Date: Thu, 20 Nov 2025 11:15:22 -0700 Subject: [PATCH 1/7] update query.gg banners for Black Friday sale --- src/components/CountdownTimer.tsx | 90 +++++++++++++++++++ src/components/CountdownTimerSmall.tsx | 85 ++++++++++++++++++ src/components/DocsCalloutQueryGG.tsx | 18 +++- src/components/QueryGGBannerSale.tsx | 48 ++++++++++ .../_libraries/query.$version.index.tsx | 6 +- 5 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 src/components/CountdownTimer.tsx create mode 100644 src/components/CountdownTimerSmall.tsx create mode 100644 src/components/QueryGGBannerSale.tsx diff --git a/src/components/CountdownTimer.tsx b/src/components/CountdownTimer.tsx new file mode 100644 index 000000000..b62d17c2c --- /dev/null +++ b/src/components/CountdownTimer.tsx @@ -0,0 +1,90 @@ +import { Fragment, useEffect, useState } from "react"; + +interface CountdownProps { + targetDate: string; // YYYY-MM-DD format +} + +interface TimeLeft { + days: number; + hours: number; + minutes: number; + seconds: number; +} + +function calculateTimeLeft(targetDate: string): TimeLeft { + const target = new Date(`${targetDate}T00:00:00-08:00`); + const now = new Date(); + const difference = +target - +now; + + if (difference <= 0) { + return { + days: 0, + hours: 0, + minutes: 0, + seconds: 0, + }; + } + + return { + days: Math.floor(difference / (1000 * 60 * 60 * 24)), + hours: Math.floor((difference / (1000 * 60 * 60)) % 24), + minutes: Math.floor((difference / 1000 / 60) % 60), + seconds: Math.floor((difference / 1000) % 60), + }; +} + +const formatNumber = (number: number) => number.toString().padStart(2, "0"); + +const Countdown: React.FC = ({ targetDate }) => { + const [timeLeft, setTimeLeft] = useState( + calculateTimeLeft(targetDate), + ); + + useEffect(() => { + const timer = setInterval(() => { + const newTimeLeft = calculateTimeLeft(targetDate); + setTimeLeft(newTimeLeft); + if ( + newTimeLeft.days === 0 && + newTimeLeft.hours === 0 && + newTimeLeft.minutes === 0 && + newTimeLeft.seconds === 0 + ) { + clearInterval(timer); + } + }, 1000); + + return () => clearInterval(timer); + }, [targetDate]); + + if ( + timeLeft.days === 0 && + timeLeft.hours === 0 && + timeLeft.minutes === 0 && + timeLeft.seconds === 0 + ) { + return null; + } + + return ( +
+ {["days", "hours", "minutes", "seconds"].map((unit, index) => ( + + {index > 0 && :} + +
+ + {formatNumber(timeLeft[unit as keyof TimeLeft]).charAt(0)} + + + {formatNumber(timeLeft[unit as keyof TimeLeft]).charAt(1)} + +

{unit}

+
+
+ ))} +
+ ); +}; + +export default Countdown; \ No newline at end of file diff --git a/src/components/CountdownTimerSmall.tsx b/src/components/CountdownTimerSmall.tsx new file mode 100644 index 000000000..e618972c6 --- /dev/null +++ b/src/components/CountdownTimerSmall.tsx @@ -0,0 +1,85 @@ +import { Fragment, useEffect, useState } from "react"; + +interface CountdownProps { + targetDate: string; // YYYY-MM-DD format +} + +interface TimeLeft { + days: number; + hours: number; + minutes: number; +} + +function calculateTimeLeft(targetDate: string): TimeLeft { + const target = new Date(`${targetDate}T00:00:00-08:00`); + const now = new Date(); + const difference = +target - +now; + + if (difference <= 0) { + return { + days: 0, + hours: 0, + minutes: 0, + }; + } + + return { + days: Math.floor(difference / (1000 * 60 * 60 * 24)), + hours: Math.floor((difference / (1000 * 60 * 60)) % 24), + minutes: Math.floor((difference / 1000 / 60) % 60), + }; +} + +const formatNumber = (number: number) => number.toString().padStart(2, "0"); + +const Countdown: React.FC = ({ targetDate }) => { + const [timeLeft, setTimeLeft] = useState( + calculateTimeLeft(targetDate), + ); + + useEffect(() => { + const timer = setInterval(() => { + const newTimeLeft = calculateTimeLeft(targetDate); + setTimeLeft(newTimeLeft); + if ( + newTimeLeft.days === 0 && + newTimeLeft.hours === 0 && + newTimeLeft.minutes === 0 + ) { + clearInterval(timer); + } + }, 1000); + + return () => clearInterval(timer); + }, [targetDate]); + + if ( + timeLeft.days === 0 && + timeLeft.hours === 0 && + timeLeft.minutes === 0 + ) { + return null; + } + + return ( +
+ {["days", "hours", "minutes"].map((unit, index) => ( + + {index > 0 && :} + +
+ + {formatNumber(timeLeft[unit as keyof TimeLeft]).charAt(0)} + + + {formatNumber(timeLeft[unit as keyof TimeLeft]).charAt(1)} + +

{unit}

+
+
+ ))} +
+ ); +}; + +export default Countdown; \ No newline at end of file diff --git a/src/components/DocsCalloutQueryGG.tsx b/src/components/DocsCalloutQueryGG.tsx index 1e0643c1f..2351ad1de 100644 --- a/src/components/DocsCalloutQueryGG.tsx +++ b/src/components/DocsCalloutQueryGG.tsx @@ -1,5 +1,6 @@ import { LogoQueryGGSmall } from '~/components/LogoQueryGGSmall' import { useQueryGGPPPDiscount } from '~/hooks/useQueryGGPPPDiscount' +import CountdownTimerSmall from '~/components/CountdownTimerSmall' export function DocsCalloutQueryGG() { const ppp = useQueryGGPPPDiscount() @@ -17,14 +18,23 @@ export function DocsCalloutQueryGG() { -
+ {/*
“If you’re serious about *really* understanding React Query, there’s no better way than with query.gg” —Tanner Linsley -
- -
+
*/} + {/*
*/} +
+

+ Black Friday Sale +

+

+ Get 30% off through December 6th +

+ +
+ {ppp && ( <>

diff --git a/src/components/QueryGGBannerSale.tsx b/src/components/QueryGGBannerSale.tsx new file mode 100644 index 000000000..f6c261de8 --- /dev/null +++ b/src/components/QueryGGBannerSale.tsx @@ -0,0 +1,48 @@ +import headerCourse from '~/images/query-header-course.svg'; +import cornerTopLeft from '~/images/query-corner-top-left.svg'; +import cornerTopRight from '~/images/query-corner-top-right.svg'; +import cornerFishBottomRight from '~/images/query-corner-fish-bottom-right.svg'; +import CountdownTimer from '~/components/CountdownTimer' + +export function QueryGGBannerSale(props: React.HTMLProps) { + return ( +

+ ) +} diff --git a/src/routes/_libraries/query.$version.index.tsx b/src/routes/_libraries/query.$version.index.tsx index 99d031a9b..6fe704284 100644 --- a/src/routes/_libraries/query.$version.index.tsx +++ b/src/routes/_libraries/query.$version.index.tsx @@ -7,7 +7,8 @@ import { LazySponsorSection } from '~/components/LazySponsorSection' import { PartnersSection } from '~/components/PartnersSection' import { BottomCTA } from '~/components/BottomCTA' import { StackBlitzEmbed } from '~/components/StackBlitzEmbed' -import { QueryGGBanner } from '~/components/QueryGGBanner' +// import { QueryGGBanner } from '~/components/QueryGGBanner' +import { QueryGGBannerSale } from '~/components/QueryGGBannerSale' import { queryProject } from '~/libraries/query' import { Framework, getBranch, getLibrary } from '~/libraries' import { seo } from '~/utils/seo' @@ -56,7 +57,8 @@ function VersionIndex() { }} />
- + {/* */} +
From c538980d9e3da9aa5473863d5b0742e03c4034be Mon Sep 17 00:00:00 2001 From: Lynn Fisher Date: Mon, 24 Nov 2025 10:17:47 -0700 Subject: [PATCH 2/7] fix linting issue --- src/components/CountdownTimer.tsx | 54 ++++++++++++------------ src/components/CountdownTimerSmall.tsx | 58 +++++++++++++------------- src/components/DocsCalloutQueryGG.tsx | 2 +- src/components/QueryGGBannerSale.tsx | 39 +++++++++-------- 4 files changed, 78 insertions(+), 75 deletions(-) diff --git a/src/components/CountdownTimer.tsx b/src/components/CountdownTimer.tsx index b62d17c2c..1969e0576 100644 --- a/src/components/CountdownTimer.tsx +++ b/src/components/CountdownTimer.tsx @@ -1,20 +1,20 @@ -import { Fragment, useEffect, useState } from "react"; +import { Fragment, useEffect, useState } from 'react' interface CountdownProps { - targetDate: string; // YYYY-MM-DD format + targetDate: string // YYYY-MM-DD format } interface TimeLeft { - days: number; - hours: number; - minutes: number; - seconds: number; + days: number + hours: number + minutes: number + seconds: number } function calculateTimeLeft(targetDate: string): TimeLeft { - const target = new Date(`${targetDate}T00:00:00-08:00`); - const now = new Date(); - const difference = +target - +now; + const target = new Date(`${targetDate}T00:00:00-08:00`) + const now = new Date() + const difference = +target - +now if (difference <= 0) { return { @@ -22,7 +22,7 @@ function calculateTimeLeft(targetDate: string): TimeLeft { hours: 0, minutes: 0, seconds: 0, - }; + } } return { @@ -30,32 +30,32 @@ function calculateTimeLeft(targetDate: string): TimeLeft { hours: Math.floor((difference / (1000 * 60 * 60)) % 24), minutes: Math.floor((difference / 1000 / 60) % 60), seconds: Math.floor((difference / 1000) % 60), - }; + } } -const formatNumber = (number: number) => number.toString().padStart(2, "0"); +const formatNumber = (number: number) => number.toString().padStart(2, '0') const Countdown: React.FC = ({ targetDate }) => { const [timeLeft, setTimeLeft] = useState( - calculateTimeLeft(targetDate), - ); + calculateTimeLeft(targetDate) + ) useEffect(() => { const timer = setInterval(() => { - const newTimeLeft = calculateTimeLeft(targetDate); - setTimeLeft(newTimeLeft); + const newTimeLeft = calculateTimeLeft(targetDate) + setTimeLeft(newTimeLeft) if ( newTimeLeft.days === 0 && newTimeLeft.hours === 0 && newTimeLeft.minutes === 0 && newTimeLeft.seconds === 0 ) { - clearInterval(timer); + clearInterval(timer) } - }, 1000); + }, 1000) - return () => clearInterval(timer); - }, [targetDate]); + return () => clearInterval(timer) + }, [targetDate]) if ( timeLeft.days === 0 && @@ -63,14 +63,16 @@ const Countdown: React.FC = ({ targetDate }) => { timeLeft.minutes === 0 && timeLeft.seconds === 0 ) { - return null; + return null } return (
- {["days", "hours", "minutes", "seconds"].map((unit, index) => ( + {['days', 'hours', 'minutes', 'seconds'].map((unit, index) => ( - {index > 0 && :} + {index > 0 && ( + : + )}
@@ -84,7 +86,7 @@ const Countdown: React.FC = ({ targetDate }) => { ))}
- ); -}; + ) +} -export default Countdown; \ No newline at end of file +export default Countdown diff --git a/src/components/CountdownTimerSmall.tsx b/src/components/CountdownTimerSmall.tsx index e618972c6..d0b7d2c0f 100644 --- a/src/components/CountdownTimerSmall.tsx +++ b/src/components/CountdownTimerSmall.tsx @@ -1,71 +1,69 @@ -import { Fragment, useEffect, useState } from "react"; +import { Fragment, useEffect, useState } from 'react' interface CountdownProps { - targetDate: string; // YYYY-MM-DD format + targetDate: string // YYYY-MM-DD format } interface TimeLeft { - days: number; - hours: number; - minutes: number; + days: number + hours: number + minutes: number } function calculateTimeLeft(targetDate: string): TimeLeft { - const target = new Date(`${targetDate}T00:00:00-08:00`); - const now = new Date(); - const difference = +target - +now; + const target = new Date(`${targetDate}T00:00:00-08:00`) + const now = new Date() + const difference = +target - +now if (difference <= 0) { return { days: 0, hours: 0, minutes: 0, - }; + } } return { days: Math.floor(difference / (1000 * 60 * 60 * 24)), hours: Math.floor((difference / (1000 * 60 * 60)) % 24), minutes: Math.floor((difference / 1000 / 60) % 60), - }; + } } -const formatNumber = (number: number) => number.toString().padStart(2, "0"); +const formatNumber = (number: number) => number.toString().padStart(2, '0') const Countdown: React.FC = ({ targetDate }) => { const [timeLeft, setTimeLeft] = useState( - calculateTimeLeft(targetDate), - ); + calculateTimeLeft(targetDate) + ) useEffect(() => { const timer = setInterval(() => { - const newTimeLeft = calculateTimeLeft(targetDate); - setTimeLeft(newTimeLeft); + const newTimeLeft = calculateTimeLeft(targetDate) + setTimeLeft(newTimeLeft) if ( newTimeLeft.days === 0 && newTimeLeft.hours === 0 && newTimeLeft.minutes === 0 ) { - clearInterval(timer); + clearInterval(timer) } - }, 1000); + }, 1000) - return () => clearInterval(timer); - }, [targetDate]); + return () => clearInterval(timer) + }, [targetDate]) - if ( - timeLeft.days === 0 && - timeLeft.hours === 0 && - timeLeft.minutes === 0 - ) { - return null; + if (timeLeft.days === 0 && timeLeft.hours === 0 && timeLeft.minutes === 0) { + return null } return (
- {["days", "hours", "minutes"].map((unit, index) => ( + {['days', 'hours', 'minutes'].map((unit, index) => ( - {index > 0 && :} + {index > 0 && ( + : + )}
@@ -79,7 +77,7 @@ const Countdown: React.FC = ({ targetDate }) => { ))}
- ); -}; + ) +} -export default Countdown; \ No newline at end of file +export default Countdown diff --git a/src/components/DocsCalloutQueryGG.tsx b/src/components/DocsCalloutQueryGG.tsx index 2351ad1de..da8e1d914 100644 --- a/src/components/DocsCalloutQueryGG.tsx +++ b/src/components/DocsCalloutQueryGG.tsx @@ -34,7 +34,7 @@ export function DocsCalloutQueryGG() {

- + {ppp && ( <>

diff --git a/src/components/QueryGGBannerSale.tsx b/src/components/QueryGGBannerSale.tsx index f6c261de8..0b64e23b8 100644 --- a/src/components/QueryGGBannerSale.tsx +++ b/src/components/QueryGGBannerSale.tsx @@ -1,40 +1,43 @@ -import headerCourse from '~/images/query-header-course.svg'; -import cornerTopLeft from '~/images/query-corner-top-left.svg'; -import cornerTopRight from '~/images/query-corner-top-right.svg'; -import cornerFishBottomRight from '~/images/query-corner-fish-bottom-right.svg'; +import headerCourse from '~/images/query-header-course.svg' +import cornerTopLeft from '~/images/query-corner-top-left.svg' +import cornerTopRight from '~/images/query-corner-top-right.svg' +import cornerFishBottomRight from '~/images/query-corner-fish-bottom-right.svg' import CountdownTimer from '~/components/CountdownTimer' export function QueryGGBannerSale(props: React.HTMLProps) { return ( -