Skip to content

Commit a4c3b5b

Browse files
committed
new ad system
1 parent 6f0da40 commit a4c3b5b

26 files changed

+717
-175
lines changed

convex/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const schema = defineSchema({
3232
v.union(...validCapabilities.map((cap) => v.literal(cap)))
3333
),
3434
adsDisabled: v.optional(v.boolean()),
35+
interestedInHidingAds: v.optional(v.boolean()),
3536
}).searchIndex('search_email', {
3637
searchField: 'email',
3738
}),

convex/users.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export const listUsers = query({
5555
v.union(v.literal('admin'), v.literal('disableAds'), v.literal('builder'))
5656
),
5757
adsDisabledFilter: v.optional(v.boolean()),
58+
interestedInHidingAdsFilter: v.optional(v.boolean()),
5859
},
5960
handler: async (ctx, args) => {
6061
// Validate admin capability
@@ -91,6 +92,9 @@ export const listUsers = query({
9192
if (typeof args.adsDisabledFilter === 'boolean') {
9293
if (Boolean(user.adsDisabled) !== args.adsDisabledFilter) return false
9394
}
95+
if (typeof args.interestedInHidingAdsFilter === 'boolean') {
96+
if (Boolean(user.interestedInHidingAds) !== args.interestedInHidingAdsFilter) return false
97+
}
9498
return true
9599
})
96100

@@ -171,3 +175,23 @@ export const adminSetAdsDisabled = mutation({
171175
return { success: true }
172176
},
173177
})
178+
179+
// Set interest in hiding ads (for opt-in waitlist)
180+
export const setInterestedInHidingAds = mutation({
181+
args: {
182+
interested: v.boolean(),
183+
},
184+
handler: async (ctx, args) => {
185+
const user = await getCurrentUserConvex(ctx)
186+
if (!user) {
187+
throw new Error('Not authenticated')
188+
}
189+
190+
// Update user's interestedInHidingAds flag
191+
await ctx.db.patch(user.userId as Id<'users'>, {
192+
interestedInHidingAds: args.interested,
193+
})
194+
195+
return { success: true }
196+
},
197+
})

src/components/Doc.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { DocTitle } from '~/components/DocTitle'
1313
import { Markdown } from '~/components/Markdown'
1414
import { AdGate } from '~/contexts/AdsContext'
1515
import { CopyMarkdownButton } from './CopyMarkdownButton'
16-
import { GamLeader } from './Gam'
16+
import { GamHeader, GamLeader } from './Gam'
1717
import { Toc } from './Toc'
1818
import { TocMobile } from './TocMobile'
1919

@@ -110,8 +110,13 @@ export function Doc({
110110
}, [])
111111

112112
return (
113-
<React.Fragment>
113+
<div className="flex-1 min-h-0 flex flex-col">
114114
{shouldRenderToc ? <TocMobile headings={headings} /> : null}
115+
<AdGate>
116+
<div className="mb-2 xl:mb-4 max-w-full">
117+
<GamHeader />
118+
</div>
119+
</AdGate>
115120
<div
116121
className={twMerge(
117122
'w-full flex bg-white/70 dark:bg-black/40 mx-auto rounded-xl max-w-[936px]',
@@ -125,9 +130,6 @@ export function Doc({
125130
isTocVisible && 'pr-0!'
126131
)}
127132
>
128-
<AdGate>
129-
<GamLeader />
130-
</AdGate>
131133
{title ? (
132134
<div className="flex items-center justify-between gap-4 pr-2 lg:pr-4">
133135
<DocTitle>{title}</DocTitle>
@@ -192,6 +194,6 @@ export function Doc({
192194
</div>
193195
)}
194196
</div>
195-
</React.Fragment>
197+
</div>
196198
)
197199
}

src/components/DocsLayout.tsx

Lines changed: 13 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,15 @@
11
import * as React from 'react'
22
import { CgClose, CgMenuLeft } from 'react-icons/cg'
3-
import {
4-
FaArrowLeft,
5-
FaArrowRight,
6-
FaDiscord,
7-
FaGithub,
8-
FaTimes,
9-
} from 'react-icons/fa'
10-
import {
11-
Link,
12-
useMatches,
13-
useNavigate,
14-
useParams,
15-
} from '@tanstack/react-router'
16-
import { Select } from '~/components/Select'
3+
import { FaArrowLeft, FaArrowRight, FaDiscord, FaGithub } from 'react-icons/fa'
4+
import { Link, useMatches, useParams } from '@tanstack/react-router'
175
import { useLocalStorage } from '~/utils/useLocalStorage'
186
import { last } from '~/utils/utils'
19-
import type { SelectOption } from '~/components/Select'
207
import type { ConfigSchema, MenuItem } from '~/utils/config'
21-
import { create } from 'zustand'
22-
import { Framework, getFrameworkOptions } from '~/libraries'
8+
import { Framework } from '~/libraries'
239
import { DocsCalloutQueryGG } from '~/components/DocsCalloutQueryGG'
24-
import { DocsCalloutBytes } from '~/components/DocsCalloutBytes'
2510
import { twMerge } from 'tailwind-merge'
2611
import { partners, PartnerImage } from '~/utils/partners'
27-
import { GamFooter, GamLeftRailSquare, GamRightRailSquare } from './Gam'
12+
import { GamFooter, GamVrec1 } from './Gam'
2813
import { AdGate } from '~/contexts/AdsContext'
2914
import { SearchButton } from './SearchButton'
3015
import { FrameworkSelect, useCurrentFramework } from './FrameworkSelect'
@@ -365,8 +350,10 @@ export function DocsLayout({
365350
{children}
366351
</div>
367352
<AdGate>
368-
<div className="mb-8 !py-0! mx-auto max-w-full overflow-x-hidden">
369-
<GamFooter />
353+
<div className="px-2 xl:px-4">
354+
<div className="mb-8 !py-0! mx-auto max-w-full">
355+
<GamFooter popupPosition="top" />
356+
</div>
370357
</div>
371358
</AdGate>
372359
<div className="sticky flex items-center flex-wrap bottom-2 z-10 right-0 text-xs md:text-sm px-1 print:hidden">
@@ -407,11 +394,11 @@ export function DocsLayout({
407394
</div>
408395
</div>
409396
<div
410-
className="lg:-ml-2 lg:pl-2 w-full lg:w-[300px] [@media(min-width:1600px)]:w-[350px] [@media(min-width:1920px)]:w-[400px] shrink-0 lg:sticky
411-
lg:max-h-[calc(100dvh-var(--navbar-height))] lg:top-[var(--navbar-height)]
412-
lg:overflow-y-auto lg:overflow-x-hidden relative"
397+
className="lg:-ml-2 lg:pl-2 w-full lg:w-[300px] shrink-0 lg:sticky
398+
lg:top-[var(--navbar-height)]
399+
"
413400
>
414-
<div className="ml-auto flex flex-wrap flex-row justify-center lg:flex-col gap-2">
401+
<div className="lg:sticky lg:top-[var(--navbar-height)] ml-auto flex flex-wrap flex-row justify-center lg:flex-col gap-2">
415402
<div className="bg-white/70 dark:bg-black/40 border-gray-500/20 shadow-xl divide-y divide-gray-500/20 flex flex-col border border-r-0 border-t-0 rounded-bl-lg">
416403
<div className="px-2 w-full flex gap-2 justify-between">
417404
<Link
@@ -468,14 +455,7 @@ export function DocsLayout({
468455
</div>
469456
</div>
470457
<AdGate>
471-
<div className="bg-white/70 dark:bg-black/40 border-gray-500/20 shadow-xl flex flex-col border-t border-l border-b p-2 space-y-2 rounded-l-lg">
472-
<GamRightRailSquare />
473-
</div>
474-
</AdGate>
475-
<AdGate>
476-
<div className="bg-white/70 dark:bg-black/40 border-gray-500/20 shadow-xl flex flex-col border-t border-l border-b p-2 space-y-2 rounded-l-lg">
477-
<GamLeftRailSquare />
478-
</div>
458+
<GamVrec1 popupPosition="top" />
479459
</AdGate>
480460
{libraryId === 'query' ? (
481461
<div className="p-4 bg-white/70 dark:bg-black/40 border-b border-gray-500/20 shadow-xl divide-y divide-gray-500/20 flex flex-col border-t border-l rounded-l-lg">

src/components/Gam.tsx

Lines changed: 95 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
import { Link } from '@tanstack/react-router'
2-
import { ParentSize } from '@visx/responsive'
31
import React from 'react'
42
import { twMerge } from 'tailwind-merge'
5-
import { useResizeObserver } from '~/hooks/useResizeObserver'
6-
import { libraries } from '~/libraries'
3+
import { Link } from '@tanstack/react-router'
74

85
declare global {
96
interface Window {
@@ -71,51 +68,96 @@ const gamDivs = {
7168
incontent_footer: 'incontent_footer',
7269
mrec_1: 'mrec_1',
7370
mrec_2: 'mrec_2',
71+
vrec_1: 'vrec_1',
72+
header: 'header',
7473
} as const
7574

7675
function GamAd({
7776
name,
7877
children,
7978
adClassName,
79+
className,
80+
placeholderClassName,
81+
popupPosition = 'bottom',
82+
borderClassName,
83+
style,
8084
...props
8185
}: { name: keyof typeof gamDivs } & React.HTMLAttributes<HTMLDivElement> & {
8286
adClassName?: string
87+
placeholderClassName?: string
88+
popupPosition?: 'top' | 'bottom'
89+
borderClassName?: string
8390
}) {
8491
const gamId = gamDivs[name]
8592

86-
const ref = React.useRef<HTMLDivElement>(null!)
87-
const [size, setSize] = React.useState({
88-
width: 0,
89-
height: 0,
90-
})
93+
const popupClasses =
94+
popupPosition === 'top'
95+
? 'absolute bottom-full right-0 opacity-0 group-hover:opacity-100 transition-all duration-300 z-10'
96+
: 'absolute top-full right-0 opacity-0 group-hover:opacity-100 transition-all duration-300 z-10'
9197

92-
useResizeObserver({
93-
ref,
94-
selector: (el) => el?.querySelector('iframe'),
95-
onResize: (rect) => {
96-
if (rect.width === 0 || rect.height === 0) return
97-
setSize({
98-
width: rect.width,
99-
height: rect.height,
100-
})
101-
},
102-
})
98+
borderClassName = twMerge('rounded-xl overflow-hidden', borderClassName)
10399

104100
return (
105-
<div style={size}>
106-
<div {...props}>
107-
<div data-fuse={gamId} className={adClassName} ref={ref} />
101+
<div {...props} className={twMerge('relative group', className)}>
102+
<div
103+
className={twMerge(
104+
'absolute inset-0 bg-white/50 dark:bg-black/20 shadow-xl shadow-black/2',
105+
borderClassName,
106+
placeholderClassName
107+
)}
108+
>
109+
<div className="flex justify-center items-center h-full opacity-50 gap-[1px]">
110+
<span className="inline-block w-1 h-1 rounded-full bg-gray-500 animate-bounce [animation-delay:0ms]" />
111+
<span className="inline-block w-1 h-1 rounded-full bg-gray-500 animate-bounce [animation-delay:100ms]" />
112+
<span className="inline-block w-1 h-1 rounded-full bg-gray-500 animate-bounce [animation-delay:200ms]" />
113+
</div>
114+
<div
115+
className={twMerge(
116+
'absolute -top-px -left-px -right-px -bottom-px border-[2px] border-gray-200 dark:border-gray-900',
117+
borderClassName
118+
)}
119+
/>
120+
</div>
121+
<div className="relative overflow-hidden">
122+
<div className={twMerge('overflow-hidden', borderClassName)}>
123+
<div data-fuse={gamId} className={adClassName} />
124+
</div>
125+
<div
126+
className={twMerge(
127+
'absolute -top-px -left-px -right-px -bottom-px border-[2px] border-gray-200 dark:border-gray-900',
128+
borderClassName
129+
)}
130+
/>
131+
</div>
132+
<div className={twMerge('flex gap-1', popupClasses)}>
133+
<Link
134+
to="/ads"
135+
className="text-xs text-gray-500 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 px-2 py-1 bg-white/90 dark:bg-gray-700 rounded-lg shadow-lg whitespace-nowrap"
136+
>
137+
Learn about TanStack Ads
138+
</Link>
139+
<Link
140+
to="/ads"
141+
hash="hide-ads"
142+
className="text-xs text-gray-500 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 px-2 py-1 bg-white/90 dark:bg-gray-700 rounded-lg shadow-lg whitespace-nowrap"
143+
>
144+
Hide Ads
145+
</Link>
108146
</div>
109147
</div>
110148
)
111149
}
112150

113-
export function GamLeader() {
114-
return null
115-
}
116-
117-
export function GamFooter() {
118-
return <GamAd name="incontent_footer" style={{ maxWidth: '728px' }} />
151+
export function GamFooter(
152+
props: Omit<React.ComponentProps<typeof GamAd>, 'name'>
153+
) {
154+
return (
155+
<GamAd
156+
{...props}
157+
name="incontent_footer"
158+
style={{ maxWidth: '728px', ...props.style }}
159+
/>
160+
)
119161
}
120162

121163
export function GamRightRailSquare() {
@@ -166,3 +208,27 @@ export function GamMrec1() {
166208
export function GamMrec2() {
167209
return <GamAd name="mrec_2" />
168210
}
211+
212+
export function GamVrec1(
213+
props: Omit<React.ComponentProps<typeof GamAd>, 'name'>
214+
) {
215+
return <GamAd {...props} name="vrec_1" />
216+
}
217+
218+
export function GamHeader(
219+
props: React.HTMLAttributes<HTMLDivElement> & {
220+
adClassName?: string
221+
}
222+
) {
223+
return (
224+
<GamAd
225+
name="header"
226+
{...props}
227+
className={twMerge(
228+
'w-full max-w-[728px] flex mx-auto justify-center',
229+
props.className
230+
)}
231+
adClassName={twMerge('h-[90px]', props.adClassName)}
232+
/>
233+
)
234+
}

src/components/LandingPageGad.tsx

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,11 @@
1-
import { Link } from '@tanstack/react-router'
21
import { GamFooter } from './Gam'
32
import { AdGate } from '~/contexts/AdsContext'
43

54
export default function LandingPageGad() {
65
return (
76
<AdGate>
8-
<div className={`lg:max-[400px] px-4 mx-auto`}>
9-
<div className="flex flex-col gap-4 items-center">
10-
<div className="rounded-lg overflow-hidden mx-auto">
11-
<GamFooter />
12-
<div
13-
className="relative text-xs bg-white dark:bg-gray-800 py-2 px-4 rounded dark:text-gray-300
14-
dark:bg-opacity-20 self-center text-center max-w-[500px] space-y-2"
15-
>
16-
<div>
17-
<span className="font-medium italic">
18-
An ad on an open source project?
19-
</span>{' '}
20-
<span className="font-black">What is this, 1999?</span>
21-
</div>
22-
<div>
23-
<span className="font-medium italic">Please...</span> TanStack
24-
is 100% privately owned, with no paid products, venture capital,
25-
or acquisition plans. We're a small team dedicated to creating
26-
software used by millions daily. What did you expect?
27-
</div>
28-
<div>
29-
<Link
30-
to="/ethos"
31-
className="text-gray-600 dark:text-gray-200 font-bold underline"
32-
>
33-
Check out our ethos
34-
</Link>{' '}
35-
to learn more about how we plan on sticking around (and staying
36-
relevant) for the long-haul.
37-
</div>
38-
</div>
39-
</div>
40-
</div>
7+
<div className={`lg:max-[400px] px-4 mx-auto flex justify-center`}>
8+
<GamFooter />
419
</div>
4210
</AdGate>
4311
)

src/contexts/AdsContext.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
11
import * as React from 'react'
22
import { useAdsPreference } from '~/stores/userSettings'
33

4-
export function AdGate({
5-
children,
6-
deferUntilReady = false,
7-
}: {
8-
children: React.ReactNode
9-
deferUntilReady?: boolean
10-
}) {
11-
const { status, adsEnabled } = useAdsPreference()
12-
if (status === 'unknown') return deferUntilReady ? <>{children}</> : null
4+
export function AdGate({ children }: { children: React.ReactNode }) {
5+
const { adsEnabled } = useAdsPreference()
136
return adsEnabled ? <>{children}</> : null
147
}

0 commit comments

Comments
 (0)