Skip to content

Commit

Permalink
[docs] First page
Browse files Browse the repository at this point in the history
  • Loading branch information
oliviertassinari committed Aug 8, 2020
1 parent 1e17c08 commit 43286cd
Show file tree
Hide file tree
Showing 13 changed files with 630 additions and 138 deletions.
380 changes: 378 additions & 2 deletions docs/pages/_app.js
@@ -1,9 +1,385 @@
import MyApp from '@material-ui/monorepo/docs/pages/_app';
import { LicenseInfo } from '@material-ui/x-grid';

// Remove the license warning from demonstration purposes
LicenseInfo.setLicenseKey(
'0f94d8b65161817ca5d7f7af8ac2f042T1JERVI6TVVJLVN0b3J5Ym9vayxFWFBJUlk9MTY1NDg1ODc1MzU1MCxLRVlWRVJTSU9OPTE=',
);

export default MyApp;
import 'docs/src/modules/components/bootstrap';
// --- Post bootstrap -----
import React from 'react';
import find from 'lodash/find';
import { Provider as ReduxProvider, useDispatch, useSelector } from 'react-redux';
import { loadCSS } from 'fg-loadcss/src/loadCSS';
import NextHead from 'next/head';
import PropTypes from 'prop-types';
import acceptLanguage from 'accept-language';
import { create } from 'jss';
import rtl from 'jss-rtl';
import { useRouter } from 'next/router';
import { StylesProvider, jssPreset } from '@material-ui/styles';
import pages from 'docsx/src/pages';
import initRedux from 'docs/src/modules/redux/initRedux';
import PageContext from 'docs/src/modules/components/PageContext';
import GoogleAnalytics from 'docs/src/modules/components/GoogleAnalytics';
import loadScript from 'docs/src/modules/utils/loadScript';
import { ThemeProvider } from 'docs/src/modules/components/ThemeContext';
import { pathnameToLanguage, getCookie } from 'docs/src/modules/utils/helpers';
import { ACTION_TYPES, CODE_VARIANTS } from 'docs/src/modules/constants';

// Configure JSS
const jss = create({
plugins: [...jssPreset().plugins, rtl()],
insertionPoint: process.browser ? document.querySelector('#insertion-point-jss') : null,
});

function useFirstRender() {
const firstRenderRef = React.useRef(true);
React.useEffect(() => {
firstRenderRef.current = false;
}, []);

return firstRenderRef.current;
}

acceptLanguage.languages(['en', 'zh', 'pt', 'ru']);

function LanguageNegotiation() {
const dispatch = useDispatch();
const router = useRouter();
const userLanguage = useSelector((state) => state.options.userLanguage);

React.useEffect(() => {
const { userLanguage: userLanguageUrl, canonical } = pathnameToLanguage(router.asPath);
const preferedLanguage =
getCookie('userLanguage') !== 'noDefault' && userLanguage === 'en'
? acceptLanguage.get(navigator.language)
: userLanguage;

if (preferedLanguage !== userLanguage) {
window.location = preferedLanguage === 'en' ? canonical : `/${preferedLanguage}${canonical}`;
} else if (userLanguageUrl !== userLanguage) {
dispatch({ type: ACTION_TYPES.OPTIONS_CHANGE, payload: { userLanguage: userLanguageUrl } });
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps

return null;
}

/**
* Priority: on first render: navigated value, persisted value; otherwise initial value, 'JS'
*
* @returns {string} - The persisted variant if the initial value is undefined
*/
function usePersistCodeVariant() {
const dispatch = useDispatch();
const { codeVariant: initialCodeVariant = CODE_VARIANTS.JS } = useSelector(
(state) => state.options,
);

const isFirstRender = useFirstRender();

const navigatedCodeVariant = React.useMemo(() => {
const navigatedCodeVariantMatch =
typeof window !== 'undefined' ? window.location.hash.match(/\.(js|tsx)$/) : null;

if (navigatedCodeVariantMatch === null) {
return undefined;
}

return navigatedCodeVariantMatch[1] === 'tsx' ? CODE_VARIANTS.TS : CODE_VARIANTS.JS;
}, []);

const persistedCodeVariant = React.useMemo(() => {
if (typeof window === 'undefined') {
return undefined;
}
return getCookie('codeVariant');
}, []);

/**
* we initialize from navigation or cookies. on subsequent renders the store is the
* truth
*/
const codeVariant =
isFirstRender === true
? navigatedCodeVariant || persistedCodeVariant || initialCodeVariant
: initialCodeVariant;

React.useEffect(() => {
if (codeVariant !== initialCodeVariant) {
dispatch({ type: ACTION_TYPES.OPTIONS_CHANGE, payload: { codeVariant } });
}
});

React.useEffect(() => {
document.cookie = `codeVariant=${codeVariant};path=/;max-age=31536000`;
}, [codeVariant]);

return codeVariant;
}

/**
* basically just a `useAnalytics` hook.
* However, it needs the redux store which is created
* in the same component this "hook" is used.
*/
function Analytics() {
React.useEffect(() => {
loadScript('https://www.google-analytics.com/analytics.js', document.querySelector('head'));
}, []);

const options = useSelector((state) => state.options);

const codeVariant = usePersistCodeVariant();
React.useEffect(() => {
window.ga('set', 'dimension1', codeVariant);
}, [codeVariant]);

React.useEffect(() => {
window.ga('set', 'dimension2', options.userLanguage);
}, [options.userLanguage]);

React.useEffect(() => {
/**
* @type {null | MediaQueryList}
*/
let matchMedia = null;

/**
* Based on https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#Monitoring_screen_resolution_or_zoom_level_changes
* Adjusted to track 3 or more different ratios
*/
function trackDevicePixelRation() {
window.ga('set', 'dimension3', Math.round(window.devicePixelRatio * 10) / 10);

matchMedia = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
// Need to setup again.
// Otherwise we track only changes from the initial ratio to another.
// It would not track 3 or more different monitors/zoom stages
matchMedia.addListener(trackDevicePixelRation);
}

trackDevicePixelRation();

return () => {
matchMedia = null;
};
}, []);

return null;
}

// Inspired by
// https://developers.google.com/web/tools/workbox/guides/advanced-recipes#offer_a_page_reload_for_users
function forcePageReload(registration) {
// console.log('already controlled?', Boolean(navigator.serviceWorker.controller));

if (!navigator.serviceWorker.controller) {
// The window client isn't currently controlled so it's a new service
// worker that will activate immediately.
return;
}

// console.log('registration waiting?', Boolean(registration.waiting));
if (registration.waiting) {
// SW is waiting to activate. Can occur if multiple clients open and
// one of the clients is refreshed.
registration.waiting.postMessage('skipWaiting');
return;
}

function listenInstalledStateChange() {
registration.installing.addEventListener('statechange', (event) => {
// console.log('statechange', event.target.state);
if (event.target.state === 'installed' && registration.waiting) {
// A new service worker is available, inform the user
registration.waiting.postMessage('skipWaiting');
} else if (event.target.state === 'activated') {
// Force the control of the page by the activated service worker.
window.location.reload();
}
});
}

if (registration.installing) {
listenInstalledStateChange();
return;
}

// We are currently controlled so a new SW may be found...
// Add a listener in case a new SW is found,
registration.addEventListener('updatefound', listenInstalledStateChange);
}

async function registerServiceWorker() {
if (
'serviceWorker' in navigator &&
process.env.NODE_ENV === 'production' &&
window.location.host.indexOf('material-ui.com') <= 0
) {
// register() automatically attempts to refresh the sw.js.
const registration = await navigator.serviceWorker.register('/sw.js');
// Force the page reload for users.
forcePageReload(registration);
}
}

let dependenciesLoaded = false;

function loadDependencies() {
if (dependenciesLoaded) {
return;
}

dependenciesLoaded = true;

loadCSS(
'https://fonts.googleapis.com/icon?family=Material+Icons',
document.querySelector('#material-icon-font'),
);
}

if (process.browser && process.env.NODE_ENV === 'production') {
// eslint-disable-next-line no-console
console.log(
`%c
███╗ ███╗ █████╗ ████████╗███████╗██████╗ ██╗ █████╗ ██╗ ██╗ ██╗██╗
████╗ ████║██╔══██╗╚══██╔══╝██╔════╝██╔══██╗██║██╔══██╗██║ ██║ ██║██║
██╔████╔██║███████║ ██║ █████╗ ██████╔╝██║███████║██║█████╗██║ ██║██║
██║╚██╔╝██║██╔══██║ ██║ ██╔══╝ ██╔══██╗██║██╔══██║██║╚════╝██║ ██║██║
██║ ╚═╝ ██║██║ ██║ ██║ ███████╗██║ ██║██║██║ ██║███████╗ ╚██████╔╝██║
╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝
Tip: you can access the documentation \`theme\` object directly in the console.
`,
'font-family:monospace;color:#1976d2;font-size:12px;',
);
}

function findActivePage(currentPages, pathname) {
const activePage = find(currentPages, (page) => {
if (page.children) {
if (pathname.indexOf(`${page.pathname}/`) === 0) {
// Check if one of the children matches (for /components)
return findActivePage(page.children, pathname);
}
}

// Should be an exact match if no children
return pathname === page.pathname;
});

if (!activePage) {
return null;
}

// We need to drill down
if (activePage.pathname !== pathname) {
return findActivePage(activePage.children, pathname);
}

return activePage;
}

function AppWrapper(props) {
const { children, pageProps } = props;

const router = useRouter();
const [redux] = React.useState(() =>
initRedux({ options: { userLanguage: pageProps.userLanguage } }),
);

React.useEffect(() => {
loadDependencies();
registerServiceWorker();

// Remove the server-side injected CSS.
const jssStyles = document.querySelector('#jss-server-side');
if (jssStyles) {
jssStyles.parentElement.removeChild(jssStyles);
}
}, []);

const activePage = findActivePage(pages, router.pathname);

let fonts = ['https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap'];
if (router.pathname.match(/onepirate/)) {
fonts = [
'https://fonts.googleapis.com/css?family=Roboto+Condensed:700|Work+Sans:300,400&display=swap',
];
}

return (
<React.Fragment>
<NextHead>
{fonts.map((font) => (
<link rel="stylesheet" href={font} key={font} />
))}
</NextHead>
<ReduxProvider store={redux}>
<PageContext.Provider value={{ activePage, pages, versions: pageProps.versions }}>
<StylesProvider jss={jss}>
<ThemeProvider>{children}</ThemeProvider>
</StylesProvider>
</PageContext.Provider>
<LanguageNegotiation />
<Analytics />
</ReduxProvider>
<GoogleAnalytics key={router.route} />
</React.Fragment>
);
}

AppWrapper.propTypes = {
children: PropTypes.node.isRequired,
pageProps: PropTypes.object.isRequired,
};

export default function MyApp(props) {
const { Component, pageProps } = props;

return (
<AppWrapper pageProps={pageProps}>
<Component {...pageProps} />
</AppWrapper>
);
}

MyApp.propTypes = {
Component: PropTypes.elementType.isRequired,
pageProps: PropTypes.object.isRequired,
};

MyApp.getInitialProps = async ({ ctx, Component }) => {
let pageProps = {};

if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}

return {
pageProps: {
userLanguage: ctx.query.userLanguage || 'en',
...pageProps,
},
};
};

// Track fraction of actual events to prevent exceeding event quota.
// Filter sessions instead of individual events so that we can track multiple metrics per device.
const disableWebVitalsReporting = Math.random() > 0.0001;
export function reportWebVitals({ id, name, label, value }) {
if (disableWebVitalsReporting) {
return;
}

window.ga('send', 'event', {
eventCategory: label === 'web-vital' ? 'Web Vitals' : 'Next.js custom metric',
eventAction: name,
eventValue: Math.round(name === 'CLS' ? value * 1000 : value), // values must be integers
eventLabel: id, // id unique to current page load
nonInteraction: true, // avoids affecting bounce rate.
});
}

0 comments on commit 43286cd

Please sign in to comment.