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 src/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 src/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="mb-4 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-100 bg-opacity-10 dark:bg-gray-800 dark:bg-opacity-10 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-100 bg-opacity-10 dark:bg-gray-800 dark:bg-opacity-10 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: 13 additions & 3 deletions src/components/DocsCalloutQueryGG.tsx
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -17,13 +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>
{/* <div className="grid justify-center bg-gray-800 dark:bg-gray-100 text-gray-100 dark:text-gray-800 z-10"></div> */}
<div className="p-2 uppercase text-center place-self-center">
<h2 className="mt-1 mb-1 px-2 text-md font-semibold">
Black Friday Sale
</h2>
<p className="normal-case mb-4 text-sm text-balance">
Get 30% off through December 6th
</p>
<CountdownTimerSmall targetDate="2025-12-06" />
</div>

{ppp && (
<>
Expand Down
51 changes: 51 additions & 0 deletions src/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-[55%] 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-[-55px] 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-[45%] 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-xl lg:text-2xl xl:text-3xl font-semibold">
Black Friday sale
</h2>
<p className="normal-case mb-4">Get 30% off through December 6th</p>
<CountdownTimer targetDate="2025-12-06" />
<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>
)
}
84 changes: 47 additions & 37 deletions src/routes/_libraries/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -391,51 +391,58 @@ function Index() {
</a>
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{recentPosts.map(({ slug, title, published, excerpt, authors }) => {
return (
<Link
key={slug}
to="/blog/$"
params={{ _splat: slug }}
className={`flex flex-col gap-3 justify-between
{recentPosts.map(
({ slug, title, published, excerpt, authors }) => {
return (
<Link
key={slug}
to="/blog/$"
params={{ _splat: slug }}
className={`flex flex-col gap-3 justify-between
border-2 border-transparent rounded-lg p-4
transition-all bg-white/90 dark:bg-black/40
shadow-md dark:shadow-lg dark:shadow-blue-500/20
hover:border-blue-500 hover:shadow-xl
`}
>
<div>
<div className={`text-base font-bold`}>{title}</div>
<div className={`text-xs italic font-light mt-1 text-gray-600 dark:text-gray-400`}>
<p>
by {formatAuthors(authors)}
{published ? (
<time
dateTime={published}
title={format(new Date(published), 'MMM dd, yyyy')}
>
{' '}
on {format(new Date(published), 'MMM dd, yyyy')}
</time>
) : null}
</p>
</div>
{excerpt && (
>
<div>
<div className={`text-base font-bold`}>{title}</div>
<div
className={`text-xs mt-3 text-gray-700 dark:text-gray-300 line-clamp-2 leading-relaxed`}
className={`text-xs italic font-light mt-1 text-gray-600 dark:text-gray-400`}
>
<Markdown rawContent={excerpt} />
<p>
by {formatAuthors(authors)}
{published ? (
<time
dateTime={published}
title={format(
new Date(published),
'MMM dd, yyyy'
)}
>
{' '}
on {format(new Date(published), 'MMM dd, yyyy')}
</time>
) : null}
</p>
</div>
{excerpt && (
<div
className={`text-xs mt-3 text-gray-700 dark:text-gray-300 line-clamp-2 leading-relaxed`}
>
<Markdown rawContent={excerpt} />
</div>
)}
</div>
<div>
<div className="text-blue-500 uppercase font-bold text-xs">
Read More →
</div>
)}
</div>
<div>
<div className="text-blue-500 uppercase font-bold text-xs">
Read More →
</div>
</div>
</Link>
)
})}
</Link>
)
}
)}
</div>
<div className="text-center mt-6">
<Link
Expand Down Expand Up @@ -517,7 +524,10 @@ function Index() {
</div>

<div className="px-4 lg:max-w-(--breakpoint-lg) md:mx-auto">
<h3 id="maintainers" className={`text-4xl font-light mb-6 scroll-mt-24`}>
<h3
id="maintainers"
className={`text-4xl font-light mb-6 scroll-mt-24`}
>
<a
href="#maintainers"
className="hover:underline decoration-gray-400 dark:decoration-gray-600"
Expand Down
6 changes: 4 additions & 2 deletions src/routes/_libraries/query.$version.index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -56,7 +57,8 @@ function VersionIndex() {
}}
/>
<div className="px-4">
<QueryGGBanner />
{/* <QueryGGBanner /> */}
<QueryGGBannerSale />
</div>

<div className="w-fit mx-auto px-4">
Expand Down
Loading
Loading