diff --git a/app/components/CountdownTimer.tsx b/app/components/CountdownTimer.tsx new file mode 100644 index 000000000..1969e0576 --- /dev/null +++ b/app/components/CountdownTimer.tsx @@ -0,0 +1,92 @@ +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 diff --git a/app/components/CountdownTimerSmall.tsx b/app/components/CountdownTimerSmall.tsx new file mode 100644 index 000000000..8693b2cbc --- /dev/null +++ b/app/components/CountdownTimerSmall.tsx @@ -0,0 +1,83 @@ +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 diff --git a/app/components/DocsCalloutQueryGG.tsx b/app/components/DocsCalloutQueryGG.tsx index 5ab6aa71c..39ecfcafa 100644 --- a/app/components/DocsCalloutQueryGG.tsx +++ b/app/components/DocsCalloutQueryGG.tsx @@ -1,4 +1,5 @@ import { LogoQueryGGSmall } from '~/components/LogoQueryGGSmall' +import CountdownTimerSmall from '~/components/CountdownTimerSmall' import { useQueryGGPPPDiscount } from '~/hooks/useQueryGGPPPDiscount' export function DocsCalloutQueryGG() { @@ -17,11 +18,22 @@ export function DocsCalloutQueryGG() { -
+ {/*
“If you're serious about *really* understanding React Query, there's no better way than with query.gg” —Tanner Linsley -
+
*/} + + {/*
*/} +
+

+ Launch week sale +

+

+ Up to 25% off through May 10th +

+ +
{ppp && ( <> diff --git a/app/components/QueryGGBannerSale.tsx b/app/components/QueryGGBannerSale.tsx new file mode 100644 index 000000000..1b42214e8 --- /dev/null +++ b/app/components/QueryGGBannerSale.tsx @@ -0,0 +1,51 @@ +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/app/routes/_libraries/query.$version.index.tsx b/app/routes/_libraries/query.$version.index.tsx index e9a4879b5..f54bcf80d 100644 --- a/app/routes/_libraries/query.$version.index.tsx +++ b/app/routes/_libraries/query.$version.index.tsx @@ -7,7 +7,8 @@ import { Carbon } from '~/components/Carbon' import { Footer } from '~/components/Footer' import { TbHeartHandshake } from 'react-icons/tb' import SponsorPack from '~/components/SponsorPack' -import { QueryGGBanner } from '~/components/QueryGGBanner' +// import { QueryGGBanner } from '~/components/QueryGGBanner' +import { QueryGGBannerSale } from '~/components/QueryGGBannerSale' import { queryProject } from '~/libraries/query' import { createFileRoute } from '@tanstack/react-router' import { Framework, getBranch, getLibrary } from '~/libraries' @@ -48,6 +49,7 @@ export default function VersionIndex() {
+

TanStack Query @@ -83,9 +85,18 @@ export default function VersionIndex() { > Read the Docs -

(or check out our official course 👇)

+

+ (or{' '} + + check out our official course + + . It’s on sale!) +

- + {/* */}