Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions app/components/CountdownTimer.tsx
Original file line number Diff line number Diff line change
@@ -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<CountdownProps> = ({ targetDate }) => {
const [timeLeft, setTimeLeft] = useState<TimeLeft>(
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 (
<div className="flex gap-2 justify-center">
{['days', 'hours', 'minutes', 'seconds'].map((unit, index) => (
<Fragment key={unit}>
{index > 0 && (
<span className="h-[2rem] grid place-content-center">:</span>
)}

<div className={`${unit} grid grid-cols-2 gap-x-1 gap-y-1.5`}>
<span className="h-[2.3rem] aspect-[6/7] grid place-content-center rounded-sm bg-[#f9f4da] bg-opacity-10 font-semibold">
{formatNumber(timeLeft[unit as keyof TimeLeft]).charAt(0)}
</span>
<span className="h-[2.3rem] aspect-[6/7] grid place-content-center rounded-sm bg-[#f9f4da] bg-opacity-10 font-semibold">
{formatNumber(timeLeft[unit as keyof TimeLeft]).charAt(1)}
</span>
<p className="col-span-full text-xs">{unit}</p>
</div>
</Fragment>
))}
</div>
)
}

export default Countdown
83 changes: 83 additions & 0 deletions app/components/CountdownTimerSmall.tsx
Original file line number Diff line number Diff line change
@@ -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<CountdownProps> = ({ targetDate }) => {
const [timeLeft, setTimeLeft] = useState<TimeLeft>(
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 (
<div className="countdown flex gap-1.5 justify-center">
{['days', 'hours', 'minutes'].map((unit, index) => (
<Fragment key={unit}>
{index > 0 && (
<span className="h-[1.4em] grid place-content-center">:</span>
)}

<div className={`${unit} grid grid-cols-2 gap-x-1 gap-y-1.5`}>
<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">
{formatNumber(timeLeft[unit as keyof TimeLeft]).charAt(0)}
</span>
<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">
{formatNumber(timeLeft[unit as keyof TimeLeft]).charAt(1)}
</span>
<p className="col-span-full text-[.65rem]">{unit}</p>
</div>
</Fragment>
))}
</div>
)
}

export default Countdown
16 changes: 14 additions & 2 deletions app/components/DocsCalloutQueryGG.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LogoQueryGGSmall } from '~/components/LogoQueryGGSmall'
import CountdownTimerSmall from '~/components/CountdownTimerSmall'
import { useQueryGGPPPDiscount } from '~/hooks/useQueryGGPPPDiscount'

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

<blockquote className="text-sm -indent-[.45em] pl-2">
{/*<blockquote className="text-sm -indent-[.45em] pl-2">
“If you're serious about *really* understanding React Query, there's
no better way than with query.gg”
<cite className="italic block text-right">—Tanner Linsley</cite>
</blockquote>
</blockquote>*/}

{/*<div className="grid justify-center bg-gray-800 dark:bg-gray-100 text-gray-100 dark:text-gray-800 z-10">*/}
<div className="p-2 uppercase text-center place-self-center">
<h2 className="mt-1 mb-1 px-2 text-md font-semibold">
Launch week sale
</h2>
<p className="normal-case mb-4 text-sm text-balance">
Up to 25% off through May 10th
</p>
<CountdownTimerSmall targetDate="2025-05-10" />
</div>

{ppp && (
<>
Expand Down
51 changes: 51 additions & 0 deletions app/components/QueryGGBannerSale.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>) {
return (
<aside
{...props}
className="mx-auto w-full max-w-[1200px] p-8 -mt-32 flex justify-between items-center"
>
<div className="w-full xl:flex xl:gap-6 bg-[#f9f4da] border-4 border-[#231f20]">
<a
href="https://query.gg?s=tanstack"
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]"
>
<img src={cornerTopLeft} alt="sun" className="" />
<img
src={headerCourse}
alt="Query.gg - The Official React Query Course"
className="-mt-[1px] w-10/12 max-w-[400px] justify-self-center"
/>
<img src={cornerTopRight} alt="moon" className="xl:hidden" />
</a>
<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>
<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">
<div className="my-2 uppercase text-center place-self-center">
{/* <h2 className="mt-1 mb-3 px-2 text-sm font-semibold">Launch sale happening now</h2> */}
<h2 className="mb-1 text-lg lg:text-2xl xl:text-3xl font-semibold">
Launch week sale
</h2>
<p className="normal-case mb-4">Up to 25% off through May 10th</p>
<CountdownTimer targetDate="2025-05-10" />
<a
href="https://query.gg?s=tanstack"
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"
>
Join now
</a>
</div>
<img
src={cornerFishBottomRight}
alt="mutated fish"
className="hidden xl:block self-end"
/>
</div>
</div>
</aside>
)
}
17 changes: 14 additions & 3 deletions app/routes/_libraries/query.$version.index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -48,6 +49,7 @@ export default function VersionIndex() {
<div className="flex flex-1 min-h-0 relative justify-center overflow-x-hidden">
<div className="flex flex-col gap-20 md:gap-32 max-w-full py-32">
<div className="flex flex-col items-center gap-8 text-center px-4">
<QueryGGBannerSale />
<h1 className="font-black flex gap-3 items-center text-4xl md:text-6xl lg:text-7xl xl:text-8xl uppercase [letter-spacing:-.05em]">
<span>TanStack</span>
<span className={twMerge(gradientText)}>Query</span>
Expand Down Expand Up @@ -83,9 +85,18 @@ export default function VersionIndex() {
>
Read the Docs
</Link>
<p>(or check out our official course 👇)</p>
<p>
(or{' '}
<a
href="https://query.gg?s=tanstack"
className="font-semibold underline"
>
check out our official course
</a>
. It’s on sale!)
</p>
</div>
<QueryGGBanner />
{/* <QueryGGBanner /> */}
</div>
<LibraryFeatureHighlights
featureHighlights={library.featureHighlights}
Expand Down
Loading