Skip to content

Commit 8d38d00

Browse files
authored
1 parent 4cf9cdd commit 8d38d00

File tree

16 files changed

+491
-22
lines changed

16 files changed

+491
-22
lines changed

.github/workflows/ci.yaml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
- name: Setup bun
2121
uses: oven-sh/setup-bun@v1
2222
with:
23-
bun-version: 1.0.33
23+
bun-version: 1.1.18
2424
- name: Install dependencies
2525
run: bun install --frozen-lockfile
2626
env:
@@ -82,7 +82,7 @@ jobs:
8282
- name: Setup bun
8383
uses: oven-sh/setup-bun@v1
8484
with:
85-
bun-version: 1.0.33
85+
bun-version: 1.1.18
8686
- name: Install dependencies
8787
run: bun install --frozen-lockfile
8888
- name: Setup Playwright
@@ -102,7 +102,7 @@ jobs:
102102
- name: Setup bun
103103
uses: oven-sh/setup-bun@v1
104104
with:
105-
bun-version: 1.0.33
105+
bun-version: 1.1.18
106106
- name: Install dependencies
107107
run: bun install --frozen-lockfile
108108
env:
@@ -121,7 +121,7 @@ jobs:
121121
- name: Setup bun
122122
uses: oven-sh/setup-bun@v1
123123
with:
124-
bun-version: 1.0.33
124+
bun-version: 1.1.18
125125
- name: Install dependencies
126126
run: bun install --frozen-lockfile
127127
env:
@@ -136,7 +136,7 @@ jobs:
136136
- name: Setup bun
137137
uses: oven-sh/setup-bun@v1
138138
with:
139-
bun-version: 1.0.33
139+
bun-version: 1.1.18
140140
- name: Install dependencies
141141
run: bun install --frozen-lockfile
142142
env:
@@ -151,7 +151,7 @@ jobs:
151151
- name: Setup bun
152152
uses: oven-sh/setup-bun@v1
153153
with:
154-
bun-version: 1.0.33
154+
bun-version: 1.1.18
155155
- name: Install dependencies
156156
run: bun install --frozen-lockfile
157157
env:
@@ -166,7 +166,7 @@ jobs:
166166
- name: Setup bun
167167
uses: oven-sh/setup-bun@v1
168168
with:
169-
bun-version: 1.0.33
169+
bun-version: 1.1.18
170170
- name: Install dependencies
171171
run: bun install --frozen-lockfile
172172
env:

bun.lockb

32 Bytes
Binary file not shown.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
],
2121
"dependencies": {
2222
"@geist-ui/icons": "^1.0.2",
23-
"@gitbook/api": "^0.52.0",
23+
"@gitbook/api": "^0.53.0",
2424
"@radix-ui/react-checkbox": "^1.0.4",
2525
"@radix-ui/react-popover": "^1.0.7",
2626
"@sentry/nextjs": "^7.94.1",

packages/react-openapi/src/OpenAPISchema.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,12 @@ export function OpenAPISchemaProperty(
4848
: getSchemaAlternatives(schema, new Set(circularRefs.keys()));
4949

5050
const shouldDisplayExample = (schema: OpenAPIV3.SchemaObject): boolean => {
51-
return (typeof schema.example === 'string' || typeof schema.example === 'number' || typeof schema.example === 'boolean')
52-
}
51+
return (
52+
typeof schema.example === 'string' ||
53+
typeof schema.example === 'number' ||
54+
typeof schema.example === 'boolean'
55+
);
56+
};
5357
return (
5458
<InteractiveSection
5559
id={id}
@@ -96,7 +100,9 @@ export function OpenAPISchemaProperty(
96100
/>
97101
) : null}
98102
{shouldDisplayExample(schema) ? (
99-
<span className="openapi-schema-example">Example: <code>{JSON.stringify(schema.example)}</code></span>
103+
<span className="openapi-schema-example">
104+
Example: <code>{JSON.stringify(schema.example)}</code>
105+
</span>
100106
) : null}
101107
</div>
102108
}

src/app/(global)/~gitbook/image/route.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { NextRequest } from 'next/server';
22

3-
import { verifyImageSignature, resizeImage, CloudflareImageOptions, checkIsSizableImageURL } from '@/lib/images';
3+
import {
4+
verifyImageSignature,
5+
resizeImage,
6+
CloudflareImageOptions,
7+
checkIsSizableImageURL,
8+
} from '@/lib/images';
49
import { parseImageAPIURL } from '@/lib/urls';
510

611
export const runtime = 'edge';

src/app/(space)/(content)/[[...pathname]]/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export default async function Page(props: { params: PagePathParams }) {
2727
content: contentPointer,
2828
contentTarget,
2929
space,
30+
parent,
3031
customization,
3132
pages,
3233
page,
@@ -84,6 +85,7 @@ export default async function Page(props: { params: PagePathParams }) {
8485
{page.layout.outline ? (
8586
<PageAside
8687
space={space}
88+
site={parent?.object === 'site' ? parent : undefined}
8789
customization={customization}
8890
page={page}
8991
document={document}

src/components/Ads/Ad.tsx

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
'use client';
2+
3+
import IconHeart from '@geist-ui/icons/heart';
4+
import * as React from 'react';
5+
6+
import { ClassValue, tcls } from '@/lib/tailwind';
7+
8+
import { AdClassicRendering } from './AdClassicRendering';
9+
import { AdCoverRendering } from './AdCoverRendering';
10+
import { AdItem, AdsResponse } from './types';
11+
12+
/**
13+
* Fetch and render the Ad placement.
14+
* https://docs.buysellads.com/ad-serving-api
15+
*/
16+
export function Ad({
17+
zoneId,
18+
spaceId,
19+
placement,
20+
ignore,
21+
style,
22+
mode = 'auto',
23+
}: {
24+
zoneId: string;
25+
spaceId: string;
26+
placement: string;
27+
ignore: boolean;
28+
style?: ClassValue;
29+
mode?: 'classic' | 'auto' | 'cover';
30+
}) {
31+
const containerRef = React.useRef<HTMLDivElement>(null);
32+
const [visible, setVisible] = React.useState(false);
33+
const [failed, setFailed] = React.useState(false);
34+
const [ad, setAd] = React.useState<AdItem | undefined>(undefined);
35+
36+
// Observe the container visibility
37+
React.useEffect(() => {
38+
if (!containerRef.current) {
39+
return;
40+
}
41+
42+
const observer = new IntersectionObserver(
43+
([entry]) => {
44+
if (entry.isIntersecting) {
45+
setVisible(true);
46+
}
47+
},
48+
{
49+
root: null,
50+
rootMargin: '0px',
51+
threshold: 0.1,
52+
},
53+
);
54+
55+
observer.observe(containerRef.current);
56+
57+
return () => {
58+
observer.disconnect();
59+
};
60+
}, []);
61+
62+
// When the container is visible,
63+
// track an impression on the ad and fetch it
64+
React.useEffect(() => {
65+
if (!visible) {
66+
return;
67+
}
68+
69+
let cancelled = false;
70+
71+
(async () => {
72+
const url = new URL(`https://srv.buysellads.com/ads/${zoneId}.json`);
73+
url.searchParams.set('segment', `placement:${placement}`);
74+
url.searchParams.set('v', 'true');
75+
if (ignore) {
76+
url.searchParams.set('ignore', 'true');
77+
}
78+
79+
try {
80+
const res = await fetch(url);
81+
const json: AdsResponse = await res.json();
82+
83+
if (cancelled) {
84+
return;
85+
}
86+
87+
const first = json.ads[0];
88+
if (first && 'active' in first) {
89+
setAd(first);
90+
}
91+
} catch (error) {
92+
console.error(
93+
'Failed to fetch ad, it might have been blocked by a ad-blocker',
94+
error,
95+
);
96+
setFailed(true);
97+
}
98+
})();
99+
100+
return () => {
101+
cancelled = true;
102+
};
103+
}, [visible, zoneId, ignore, placement]);
104+
105+
const viaUrl = new URL('https://www.gitbook.com');
106+
viaUrl.searchParams.set('utm_source', 'content');
107+
viaUrl.searchParams.set('utm_medium', 'ads');
108+
viaUrl.searchParams.set('utm_campaign', spaceId);
109+
110+
if (ad) {
111+
console.log('ad', ad);
112+
}
113+
114+
return (
115+
<div ref={containerRef} className={tcls(style)}>
116+
{ad ? (
117+
<>
118+
{mode === 'classic' || !('callToAction' in ad) ? (
119+
<AdClassicRendering ad={ad} />
120+
) : (
121+
<AdCoverRendering ad={ad} />
122+
)}
123+
{ad.pixel ? <AdPixels rawPixel={ad.pixel} /> : null}
124+
<p
125+
className={tcls(
126+
'mt-2',
127+
'mr-2',
128+
'text-xs',
129+
'text-right',
130+
'text-dark/5',
131+
'dark:text-light/5',
132+
)}
133+
>
134+
<a
135+
target="_blank"
136+
href={viaUrl.toString()}
137+
className={tcls('hover:underline')}
138+
>
139+
Ads via GitBook
140+
</a>
141+
</p>
142+
</>
143+
) : failed ? (
144+
<AdBlockerPlaceholder />
145+
) : null}
146+
</div>
147+
);
148+
}
149+
150+
/**
151+
* Render attribution or verification pixels.
152+
* https://docs.buysellads.com/ad-serving-api#pixels
153+
*/
154+
function AdPixels({ rawPixel }: { rawPixel: string }) {
155+
const pixels = rawPixel.split('||');
156+
const time = String(Math.round(Date.now() / 1e4) | 0);
157+
158+
return (
159+
<div className={tcls('hidden')}>
160+
{pixels.map((pixel, index) => {
161+
return (
162+
<img
163+
key={index}
164+
src={pixel.replace('[timestamp]', time)}
165+
width="1"
166+
height="1"
167+
style={{ display: 'none' }}
168+
alt="Ads tracking pixel"
169+
/>
170+
);
171+
})}
172+
</div>
173+
);
174+
}
175+
176+
/**
177+
* Placeholder when visitor has an ad-blocker.
178+
*/
179+
function AdBlockerPlaceholder() {
180+
return (
181+
<div
182+
className={tcls(
183+
'flex',
184+
'flex-col',
185+
'gap-3',
186+
'bg-light-2',
187+
'text-dark/7',
188+
'dark:bg-dark-2',
189+
'dark:text-light/7',
190+
'rounded-lg',
191+
'p-4',
192+
)}
193+
>
194+
<div className={tcls('flex', 'flex-row', 'gap-2', 'items-center')}>
195+
<IconHeart className={tcls('w-4', 'h-4', 'text-primary-500')} />
196+
<p className={tcls('text-xs', 'font-semibold')}>Ad disabled</p>
197+
</div>
198+
<p className={tcls('text-xs')}>
199+
{`It looks like you're using an adblocker. Whitelist this site to help support this
200+
project.`}
201+
</p>
202+
</div>
203+
);
204+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import * as React from 'react';
2+
3+
import { tcls } from '@/lib/tailwind';
4+
5+
import { AdItem } from './types';
6+
7+
/**
8+
* Classic rendering for an ad.
9+
*/
10+
export function AdClassicRendering({ ad }: { ad: AdItem }) {
11+
return (
12+
<a
13+
className={tcls(
14+
'flex',
15+
'flex-col',
16+
'gap-4',
17+
'bg-light-2',
18+
'text-dark/7',
19+
'dark:bg-dark-2',
20+
'dark:text-light/7',
21+
'hover:text-dark/9',
22+
'dark:hover:text-light/9',
23+
'rounded-lg',
24+
'p-4',
25+
)}
26+
href={ad.statlink}
27+
rel="sponsored noopener"
28+
target="_blank"
29+
>
30+
{'smallImage' in ad ? (
31+
<div>
32+
<img alt="Ads logo" className={tcls('rounded-md')} src={ad.smallImage} />
33+
</div>
34+
) : (
35+
<div
36+
className={tcls('px-6', 'py-4', 'rounded-md')}
37+
style={{ backgroundColor: ad.backgroundColor }}
38+
>
39+
<img alt="Ads logo" src={ad.logo} />
40+
</div>
41+
)}
42+
<div className={tcls('flex', 'flex-col')}>
43+
<div className={tcls('text-xs')}>{ad.description}</div>
44+
</div>
45+
</a>
46+
);
47+
}

0 commit comments

Comments
 (0)