Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[docs-infra] Move Ads component to TypeScript #42842

Merged
merged 2 commits into from
Jul 15, 2024
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
2 changes: 2 additions & 0 deletions docs/nextConfigDocsInfra.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ function withDocsInfra(nextConfig) {
NETLIFY_DEPLOY_URL: process.env.DEPLOY_URL,
// Name of the site, its Netlify subdomain; for example, material-ui-docs
NETLIFY_SITE_NAME: process.env.SITE_NAME,
// The ratio of ads display reported to Google Analytics. Used to avoid an exceed on the Google Analytics quotas.
GA_ADS_DISPLAY_RATIO: 0.1,
},
Comment on lines +74 to 76
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure it's the best place to move the constants

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo, ideally we move _app to the docs infra package, add the ads display ratio as a prop and provide it in a context.
For now I guess the approach works, though I don't like to depend on process.env in libraries, it's implicit API and bundlers may not always handle it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we revert this?

SCR-20240722-bvgz

Also related to vercel/next.js#57755 (comment)

experimental: {
scrollRestoration: true,
Expand Down
1 change: 1 addition & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
"@types/react-transition-group": "^4.4.10",
"@types/react-window": "^1.8.8",
"@types/stylis": "^4.2.0",
"@types/gtag.js": "^0.0.20",
"chai": "^4.4.1",
"cross-fetch": "^4.0.0",
"gm": "^1.25.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,17 @@ import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';
import AdCarbon from 'docs/src/modules/components/AdCarbon';
import AdInHouse from 'docs/src/modules/components/AdInHouse';
import { GA_ADS_DISPLAY_RATIO } from 'docs/src/modules/constants';
import { AdContext, adShape } from 'docs/src/modules/components/AdManager';
import { useTranslate } from '@mui/docs/i18n';

function PleaseDisableAdblock(props) {
function PleaseDisableAdblock() {
const t = useTranslate();

return (
<Paper
component="span"
elevation={0}
sx={{ display: 'block', p: 1.5, border: '2px solid', borderColor: 'primary.main' }}
{...props}
>
<Typography variant="body2" component="span" gutterBottom sx={{ display: 'block' }}>
{t('likeMui')}
Expand Down Expand Up @@ -74,7 +72,10 @@ const inHouseAds = [
},
];

class AdErrorBoundary extends React.Component {
class AdErrorBoundary extends React.Component<{
eventLabel: string | null;
children?: React.ReactNode | undefined;
}> {
static propTypes = {
children: PropTypes.node.isRequired,
eventLabel: PropTypes.string,
Expand Down Expand Up @@ -120,8 +121,8 @@ function isBot() {
}

export default function Ad() {
const [adblock, setAdblock] = React.useState(null);
const [carbonOut, setCarbonOut] = React.useState(null);
const [adblock, setAdblock] = React.useState<null | boolean>(null);
const [carbonOut, setCarbonOut] = React.useState<null | boolean>(null);

const { current: randomAdblock } = React.useRef(Math.random());
const { current: randomInHouse } = React.useRef(Math.random());
Expand Down Expand Up @@ -150,7 +151,7 @@ export default function Ad() {
const ad = React.useContext(AdContext);
const eventLabel = label ? `${label}-${ad.placement}-${adShape}` : null;

const timerAdblock = React.useRef();
const timerAdblock = React.useRef<NodeJS.Timeout>();
Copy link
Member

@oliviertassinari oliviertassinari Jul 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The codebase uses:

Suggested change
const timerAdblock = React.useRef<NodeJS.Timeout>();
const timerAdblock = React.useRef<ReturnType<typeof setTimeout>>();

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modification done in #42944


const checkAdblock = React.useCallback(
(attempt = 1) => {
Expand All @@ -162,7 +163,7 @@ export default function Ad() {
) {
if (
document.querySelector('#carbonads a') &&
document.querySelector('#carbonads a').getAttribute('href') ===
document.querySelector('#carbonads a')?.getAttribute('href') ===
'https://material-ui-next.com/discover-more/backers'
) {
setCarbonOut(true);
Expand Down Expand Up @@ -198,7 +199,7 @@ export default function Ad() {

React.useEffect(() => {
// Avoid an exceed on the Google Analytics quotas.
if (Math.random() > GA_ADS_DISPLAY_RATIO || !eventLabel) {
if (Math.random() > ((process.env.GA_ADS_DISPLAY_RATIO ?? 0.1) as number) || !eventLabel) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing to note as well is that Next.js actually puts a number in process.env.GA_ADS_DISPLAY_RATIO while in Node.js this would always be a string, so the types will always be incorrect.
Personally, I'd never pass anything to a Next.js env that is not a string and always parse it from a string on the client side. This would avoid the as number which circumvents the type checking on this line.

return undefined;
}

Expand Down Expand Up @@ -231,10 +232,6 @@ export default function Ad() {
display: 'flex',
alignItems: 'flex-end',
}),
...(adShape === 'inline2' && {
display: 'flex',
alignItems: 'flex-end',
}),
})}
Comment on lines 234 to 235
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not found usage of 'inline2' in material-ui, mui-x, or toolpad repo

data-ga-event-category="ad"
data-ga-event-action="click"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ import loadScript from 'docs/src/modules/utils/loadScript';
import AdDisplay from 'docs/src/modules/components/AdDisplay';
import { adStylesObject } from 'docs/src/modules/components/ad.styles';

type CarbonAd = {
pixel: string;
timestamp: string;
statimp: string;
statlink: string;
image: string;
company: string;
description: string;
};
const CarbonRoot = styled('span')(({ theme }) => {
const styles = adStylesObject['body-image'](theme);

Expand All @@ -18,7 +27,6 @@ const CarbonRoot = styled('span')(({ theme }) => {
display: 'none',
},
'& #carbonads': {
display: 'block',
...styles.root,
'& .carbon-img': styles.imgWrapper,
'& img': styles.img,
Expand Down Expand Up @@ -54,8 +62,8 @@ function AdCarbonImage() {
return <CarbonRoot ref={ref} />;
}

export function AdCarbonInline(props) {
const [ad, setAd] = React.useState(null);
export function AdCarbonInline() {
const [ad, setAd] = React.useState<CarbonAd | null>(null);

React.useEffect(() => {
let active = true;
Expand All @@ -79,8 +87,8 @@ export function AdCarbonInline(props) {
const data = await response.json();
// Inspired by https://github.com/Semantic-Org/Semantic-UI-React/blob/2c7134128925dd831de85011e3eb0ec382ba7f73/docs/src/components/CarbonAd/CarbonAdNative.js#L9
const sanitizedAd = data.ads
.filter((item) => Object.keys(item).length > 0)
.filter((item) => item.statlink)
.filter((item: any) => Object.keys(item).length > 0)
.filter((item: any) => item.statlink)
.filter(Boolean)[0];

if (!sanitizedAd) {
Expand Down Expand Up @@ -117,7 +125,6 @@ export function AdCarbonInline(props) {
/>
))}
<AdDisplay
{...props}
className="carbonads"
shape="inline"
ad={{
Expand All @@ -131,7 +138,7 @@ export function AdCarbonInline(props) {
/>
</React.Fragment>
) : (
<div {...props} style={{ minHeight: 52 }} />
<div style={{ minHeight: 52 }} />
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import * as React from 'react';
import PropTypes from 'prop-types';
import { styled } from '@mui/material/styles';
import { adShape } from 'docs/src/modules/components/AdManager';
import { GA_ADS_DISPLAY_RATIO } from 'docs/src/modules/constants';
import { adStylesObject } from 'docs/src/modules/components/ad.styles';
import { useTranslate } from '@mui/docs/i18n';

const InlineShape = styled('span')(({ theme }) => {
const styles = adStylesObject['body-inline'](theme);
Expand Down Expand Up @@ -31,12 +31,27 @@ const ImageShape = styled('span')(({ theme }) => {
};
});

export default function AdDisplay(props) {
export interface Ad {
name: string;
link: string;
img?: string;
description: string;
poweredby: string;
label: string;
}
interface AdDisplayProps {
ad: Ad;
className?: string;
shape?: 'auto' | 'inline' | 'image';
}

export default function AdDisplay(props: AdDisplayProps) {
const { ad, className, shape: shapeProp = 'auto' } = props;
const t = useTranslate();

React.useEffect(() => {
// Avoid an exceed on the Google Analytics quotas.
if (Math.random() > GA_ADS_DISPLAY_RATIO || !ad.label) {
if (Math.random() > ((process.env.GA_ADS_DISPLAY_RATIO ?? 0.1) as number) || !ad.label) {
return;
}

Expand All @@ -48,15 +63,9 @@ export default function AdDisplay(props) {

const shape = shapeProp === 'auto' ? adShape : shapeProp;

let Root;
if (shape === 'inline') {
Root = InlineShape;
}
if (shape === 'image') {
Root = ImageShape;
}
const Root = shape === 'image' ? ImageShape : InlineShape;

/* eslint-disable material-ui/no-hardcoded-labels, react/no-danger */
/* eslint-disable react/no-danger */
return (
<Root className={className}>
<a
Expand All @@ -79,10 +88,12 @@ export default function AdDisplay(props) {
dangerouslySetInnerHTML={{ __html: ad.description }}
/>
</a>
<span className="AdDisplay-poweredby">ad by {ad.poweredby}</span>
<span className="AdDisplay-poweredby">
{t('adPublisher').replace('{{publisher}}', ad.poweredby)}
</span>
</Root>
);
/* eslint-enable material-ui/no-hardcoded-labels, react/no-danger */
/* eslint-enable react/no-danger */
}

AdDisplay.propTypes = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@ import PropTypes from 'prop-types';
import Portal from '@mui/material/Portal';
import { AdContext } from 'docs/src/modules/components/AdManager';

export default function AdGuest(props) {
interface AdGuestProps {
/**
* The querySelector use to target the element which will include the ad.
*/
classSelector?: string;
children?: React.ReactNode | undefined;
}

export default function AdGuest(props: AdGuestProps) {
const { classSelector = '.description', children } = props;
const ad = React.useContext(AdContext);

Expand All @@ -16,10 +24,12 @@ export default function AdGuest(props) {
container={() => {
const element = document.querySelector(classSelector);

if (ad.element === element) {
element.classList.add('ad');
} else {
element.classList.remove('ad');
if (element) {
if (ad.element === element) {
element.classList.add('ad');
} else {
element.classList.remove('ad');
}
}

return ad.element;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import AdDisplay from 'docs/src/modules/components/AdDisplay';
import AdDisplay, { Ad } from 'docs/src/modules/components/AdDisplay';

export default function AdInHouse(props) {
export default function AdInHouse(props: { ad: Omit<Ad, 'poweredby' | 'label'> }) {
const { ad } = props;

return <AdDisplay ad={{ poweredby: 'MUI', label: `in-house-${ad.name}`, ...ad }} />;
Expand Down
30 changes: 0 additions & 30 deletions docs/src/modules/components/AdManager.js

This file was deleted.

43 changes: 43 additions & 0 deletions docs/src/modules/components/AdManager.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { unstable_useEnhancedEffect as useEnhancedEffect } from '@mui/material/utils';

type AdPortal = {
placement: 'body-top';
element: Element | null;
};

interface AdManagerProps {
/**
* The querySelector use to target the element which will include the ad.
*/
classSelector?: string;
children?: React.ReactNode | undefined;
}

export const AdContext = React.createContext<AdPortal>({ placement: 'body-top', element: null });

// Persisted for the whole session.
// The state is used to use different ad placements.
const randomSession = Math.random();

// Distribution profile:
// 20% body-inline
// 80% body-image
export const adShape = randomSession < 0.2 ? 'inline' : 'image';

export default function AdManager({ classSelector = '.description', children }: AdManagerProps) {
const [portal, setPortal] = React.useState<AdPortal>({ placement: 'body-top', element: null });

useEnhancedEffect(() => {
const container = document.querySelector(classSelector);
setPortal({ placement: 'body-top', element: container });
}, [classSelector]);

return <AdContext.Provider value={portal}>{children}</AdContext.Provider>;
}

AdManager.propTypes = {
children: PropTypes.node,
classSelector: PropTypes.string,
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { alpha } from '@mui/material/styles';
import { alpha, Theme } from '@mui/material/styles';
import { adShape } from 'docs/src/modules/components/AdManager';

const adBodyImageStyles = (theme) => ({
const adBodyImageStyles = (theme: Theme) => ({
root: {
display: 'block',
overflow: 'hidden',
Expand Down Expand Up @@ -44,7 +44,7 @@ const adBodyImageStyles = (theme) => ({
},
});

const adBodyInlineStyles = (theme) => {
const adBodyInlineStyles = (theme: Theme) => {
const baseline = adBodyImageStyles(theme);

return {
Expand Down
4 changes: 0 additions & 4 deletions docs/src/modules/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,8 @@ const LANGUAGES_LABEL = [
},
];

// The ratio of ads display sending event to Google Analytics
const GA_ADS_DISPLAY_RATIO = 0.1;

module.exports = {
CODE_VARIANTS,
LANGUAGES_LABEL,
CODE_STYLING,
GA_ADS_DISPLAY_RATIO,
};
7 changes: 7 additions & 0 deletions docs/types/ga.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Gtag from '@types/gtag.js';

declare global {
interface Window {
gtag: Gtag.Gtag;
}
}
Loading
Loading