diff --git a/src/components/CountdownTimer.tsx b/src/components/CountdownTimer.tsx new file mode 100644 index 000000000..1969e0576 --- /dev/null +++ b/src/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/src/components/CountdownTimerSmall.tsx b/src/components/CountdownTimerSmall.tsx new file mode 100644 index 000000000..d0b7d2c0f --- /dev/null +++ b/src/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/src/components/DocsCalloutQueryGG.tsx b/src/components/DocsCalloutQueryGG.tsx index 1e0643c1f..da8e1d914 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,13 +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 -
+
*/} -
+ {/*
*/} +
+

+ 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..0b64e23b8 --- /dev/null +++ b/src/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/src/routes/_libraries/index.tsx b/src/routes/_libraries/index.tsx index ba781a425..67d8ff095 100644 --- a/src/routes/_libraries/index.tsx +++ b/src/routes/_libraries/index.tsx @@ -391,51 +391,58 @@ function Index() {
- {recentPosts.map(({ slug, title, published, excerpt, authors }) => { - return ( - { + return ( + -
-
{title}
-
-

- by {formatAuthors(authors)} - {published ? ( - - ) : null} -

-
- {excerpt && ( + > +
+
{title}
- +

+ by {formatAuthors(authors)} + {published ? ( + + ) : null} +

+
+ {excerpt && ( +
+ +
+ )} +
+
+
+ Read More →
- )} -
-
-
- Read More →
-
- - ) - })} + + ) + } + )}
-

+

- + {/* */} +
diff --git a/src/routes/_libraries/tenets.tsx b/src/routes/_libraries/tenets.tsx index a3aa381d0..267fab09c 100644 --- a/src/routes/_libraries/tenets.tsx +++ b/src/routes/_libraries/tenets.tsx @@ -200,10 +200,10 @@ function RouteComp() { 4. Predictable, Explicit, Type-Safe Behavior

- We minimize magic and maximize clarity. State, side effects, and data - flow should be understandable from code, not guessed from hidden - behavior. Type safety should guide correct usage without drowning - users in generics. + We minimize magic and maximize clarity. State, side effects, and + data flow should be understandable from code, not guessed from + hidden behavior. Type safety should guide correct usage without + drowning users in generics.

  • @@ -247,15 +247,13 @@ function RouteComp() {

    How to Use These Tenets

    -

    - These tenets serve multiple purposes for different audiences: -

    +

    These tenets serve multiple purposes for different audiences:

    • For developers evaluating TanStack: These tenets - define what you can expect from our libraries—quality, portability, - and a commitment to your freedom to compose and deploy however you - see fit. + define what you can expect from our libraries—quality, + portability, and a commitment to your freedom to compose and + deploy however you see fit.
    • For contributors: When proposing features or @@ -292,4 +290,3 @@ function RouteComp() {
) } -