diff --git a/docs/nextConfigDocsInfra.js b/docs/nextConfigDocsInfra.js index b5e7a45f170b9d..38a637a11855a6 100644 --- a/docs/nextConfigDocsInfra.js +++ b/docs/nextConfigDocsInfra.js @@ -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, }, experimental: { scrollRestoration: true, diff --git a/docs/package.json b/docs/package.json index ec33c08dd4a06c..4b12de54da7d32 100644 --- a/docs/package.json +++ b/docs/package.json @@ -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", diff --git a/docs/src/modules/components/Ad.js b/docs/src/modules/components/Ad.tsx similarity index 92% rename from docs/src/modules/components/Ad.js rename to docs/src/modules/components/Ad.tsx index 42a5ba21e9bf72..b94043f4cfdf69 100644 --- a/docs/src/modules/components/Ad.js +++ b/docs/src/modules/components/Ad.tsx @@ -5,11 +5,10 @@ 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 ( @@ -17,7 +16,6 @@ function PleaseDisableAdblock(props) { component="span" elevation={0} sx={{ display: 'block', p: 1.5, border: '2px solid', borderColor: 'primary.main' }} - {...props} > {t('likeMui')} @@ -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, @@ -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); + const [carbonOut, setCarbonOut] = React.useState(null); const { current: randomAdblock } = React.useRef(Math.random()); const { current: randomInHouse } = React.useRef(Math.random()); @@ -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(); const checkAdblock = React.useCallback( (attempt = 1) => { @@ -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); @@ -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) { return undefined; } @@ -231,10 +232,6 @@ export default function Ad() { display: 'flex', alignItems: 'flex-end', }), - ...(adShape === 'inline2' && { - display: 'flex', - alignItems: 'flex-end', - }), })} data-ga-event-category="ad" data-ga-event-action="click" diff --git a/docs/src/modules/components/AdCarbon.js b/docs/src/modules/components/AdCarbon.tsx similarity index 89% rename from docs/src/modules/components/AdCarbon.js rename to docs/src/modules/components/AdCarbon.tsx index bd62cf8bfe3871..cf39fdc5b7c431 100644 --- a/docs/src/modules/components/AdCarbon.js +++ b/docs/src/modules/components/AdCarbon.tsx @@ -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); @@ -18,7 +27,6 @@ const CarbonRoot = styled('span')(({ theme }) => { display: 'none', }, '& #carbonads': { - display: 'block', ...styles.root, '& .carbon-img': styles.imgWrapper, '& img': styles.img, @@ -54,8 +62,8 @@ function AdCarbonImage() { return ; } -export function AdCarbonInline(props) { - const [ad, setAd] = React.useState(null); +export function AdCarbonInline() { + const [ad, setAd] = React.useState(null); React.useEffect(() => { let active = true; @@ -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) { @@ -117,7 +125,6 @@ export function AdCarbonInline(props) { /> ))} ) : ( -
+
); } diff --git a/docs/src/modules/components/AdDisplay.js b/docs/src/modules/components/AdDisplay.tsx similarity index 74% rename from docs/src/modules/components/AdDisplay.js rename to docs/src/modules/components/AdDisplay.tsx index 3b67cec3e5e529..21eddc2246e4ac 100644 --- a/docs/src/modules/components/AdDisplay.js +++ b/docs/src/modules/components/AdDisplay.tsx @@ -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); @@ -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; } @@ -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 ( - ad by {ad.poweredby} + + {t('adPublisher').replace('{{publisher}}', ad.poweredby)} + ); - /* eslint-enable material-ui/no-hardcoded-labels, react/no-danger */ + /* eslint-enable react/no-danger */ } AdDisplay.propTypes = { diff --git a/docs/src/modules/components/AdGuest.js b/docs/src/modules/components/AdGuest.tsx similarity index 59% rename from docs/src/modules/components/AdGuest.js rename to docs/src/modules/components/AdGuest.tsx index e7cd035a4c7995..e97f5440952d55 100644 --- a/docs/src/modules/components/AdGuest.js +++ b/docs/src/modules/components/AdGuest.tsx @@ -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); @@ -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; diff --git a/docs/src/modules/components/AdInHouse.js b/docs/src/modules/components/AdInHouse.tsx similarity index 61% rename from docs/src/modules/components/AdInHouse.js rename to docs/src/modules/components/AdInHouse.tsx index 8c3c89861217e0..e158a53adaa8a7 100644 --- a/docs/src/modules/components/AdInHouse.js +++ b/docs/src/modules/components/AdInHouse.tsx @@ -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 }) { const { ad } = props; return ; diff --git a/docs/src/modules/components/AdManager.js b/docs/src/modules/components/AdManager.js deleted file mode 100644 index baf1154215c1a1..00000000000000 --- a/docs/src/modules/components/AdManager.js +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from 'react'; -import PropTypes from 'prop-types'; -import { unstable_useEnhancedEffect as useEnhancedEffect } from '@mui/material/utils'; - -export const AdContext = React.createContext(); - -// 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', ...props }) { - const [portal, setPortal] = React.useState({}); - - useEnhancedEffect(() => { - const description = document.querySelector(classSelector); - setPortal({ placement: 'body-top', element: description }); - }, [classSelector]); - - return {props.children}; -} - -AdManager.propTypes = { - children: PropTypes.node, - classSelector: PropTypes.string, -}; diff --git a/docs/src/modules/components/AdManager.tsx b/docs/src/modules/components/AdManager.tsx new file mode 100644 index 00000000000000..3fccd1c7ff5594 --- /dev/null +++ b/docs/src/modules/components/AdManager.tsx @@ -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({ 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({ placement: 'body-top', element: null }); + + useEnhancedEffect(() => { + const container = document.querySelector(classSelector); + setPortal({ placement: 'body-top', element: container }); + }, [classSelector]); + + return {children}; +} + +AdManager.propTypes = { + children: PropTypes.node, + classSelector: PropTypes.string, +}; diff --git a/docs/src/modules/components/ad.styles.js b/docs/src/modules/components/ad.styles.ts similarity index 93% rename from docs/src/modules/components/ad.styles.js rename to docs/src/modules/components/ad.styles.ts index 37a698eaaea99a..dffa082eb2f157 100644 --- a/docs/src/modules/components/ad.styles.js +++ b/docs/src/modules/components/ad.styles.ts @@ -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', @@ -44,7 +44,7 @@ const adBodyImageStyles = (theme) => ({ }, }); -const adBodyInlineStyles = (theme) => { +const adBodyInlineStyles = (theme: Theme) => { const baseline = adBodyImageStyles(theme); return { diff --git a/docs/src/modules/constants.js b/docs/src/modules/constants.js index 527946179861bd..7e5a49edcd5a32 100644 --- a/docs/src/modules/constants.js +++ b/docs/src/modules/constants.js @@ -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, }; diff --git a/docs/types/ga.d.ts b/docs/types/ga.d.ts new file mode 100644 index 00000000000000..77c372691d0f4d --- /dev/null +++ b/docs/types/ga.d.ts @@ -0,0 +1,7 @@ +import Gtag from '@types/gtag.js'; + +declare global { + interface Window { + gtag: Gtag.Gtag; + } +} diff --git a/packages/mui-docs/src/translations/translations.json b/packages/mui-docs/src/translations/translations.json index be8c78215ef28d..20ad001d2ecf91 100644 --- a/packages/mui-docs/src/translations/translations.json +++ b/packages/mui-docs/src/translations/translations.json @@ -1,5 +1,6 @@ { "adblock": "If you don't mind tech-related ads (no tracking or remarketing), and want to keep us running, please whitelist us in your blocker.", + "adPublisher": "ad by {{publisher}}", "api-docs": { "componentName": "Component name", "componentsApi": "Components API", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aee5cd1f547d22..ec1ba98c428954 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -842,6 +842,9 @@ importers: '@types/css-mediaquery': specifier: ^0.1.4 version: 0.1.4 + '@types/gtag.js': + specifier: ^0.0.20 + version: 0.0.20 '@types/json2mq': specifier: ^0.2.2 version: 0.2.2 @@ -5257,6 +5260,9 @@ packages: '@types/fs-extra@11.0.4': resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + '@types/gtag.js@0.0.20': + resolution: {integrity: sha512-wwAbk3SA2QeU67unN7zPxjEHmPmlXwZXZvQEpbEUQuMCRGgKyE1m6XDuTUA9b6pCGb/GqJmdfMOY5LuDjJSbbg==} + '@types/hoist-non-react-statics@3.3.5': resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==} @@ -16120,6 +16126,8 @@ snapshots: '@types/jsonfile': 6.1.1 '@types/node': 18.19.39 + '@types/gtag.js@0.0.20': {} + '@types/hoist-non-react-statics@3.3.5': dependencies: '@types/react': 18.3.3