diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-02-01 15:15:55 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-02-01 15:15:55 +0100 |
| commit | 1e370817560c905a0a3520e245c317f308b6a5e7 (patch) | |
| tree | f5a1e2c3b8ddbff2cb4d94eff82830388158e4c9 /src | |
| parent | 8f8a3957b5d3b33bafaa0a6afe4187f75d6dd2b7 (diff) | |
chore: add a new settings to handle prism theme from toolbar
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/MDX/CodeBlock/CodeBlock.tsx | 10 | ||||
| -rw-r--r-- | src/components/Settings/PrismThemeToggle/PrismThemeToggle.tsx | 49 | ||||
| -rw-r--r-- | src/components/Settings/Settings.tsx | 2 | ||||
| -rw-r--r-- | src/i18n/en.json | 4 | ||||
| -rw-r--r-- | src/i18n/fr.json | 4 | ||||
| -rw-r--r-- | src/pages/_app.tsx | 5 | ||||
| -rw-r--r-- | src/pages/article/[slug].tsx | 10 | ||||
| -rw-r--r-- | src/utils/providers/prism.tsx | 162 |
8 files changed, 245 insertions, 1 deletions
diff --git a/src/components/MDX/CodeBlock/CodeBlock.tsx b/src/components/MDX/CodeBlock/CodeBlock.tsx index a822744..45a6176 100644 --- a/src/components/MDX/CodeBlock/CodeBlock.tsx +++ b/src/components/MDX/CodeBlock/CodeBlock.tsx @@ -5,6 +5,7 @@ import Prism from 'prismjs'; import { ReactChildren, useEffect } from 'react'; import { useIntl } from 'react-intl'; import '@utils/plugins/prism-color-scheme'; +import { usePrismTheme } from '@utils/providers/prism'; const CodeBlock = ({ className, @@ -29,6 +30,15 @@ const CodeBlock = ({ translateCopyButton(locale, intl); }, [intl, locale]); + const { setCodeBlocks } = usePrismTheme(); + + useEffect(() => { + const allPre: NodeListOf<HTMLPreElement> = document.querySelectorAll( + 'pre[data-prismjs-color-scheme' + ); + setCodeBlocks(allPre); + }, [setCodeBlocks, router.asPath]); + return ( <div> <pre className={classNames.join(' ')}> diff --git a/src/components/Settings/PrismThemeToggle/PrismThemeToggle.tsx b/src/components/Settings/PrismThemeToggle/PrismThemeToggle.tsx new file mode 100644 index 0000000..06f7ac8 --- /dev/null +++ b/src/components/Settings/PrismThemeToggle/PrismThemeToggle.tsx @@ -0,0 +1,49 @@ +import { Toggle } from '@components/Form'; +import { MoonIcon, SunIcon } from '@components/Icons'; +import Spinner from '@components/Spinner/Spinner'; +import { usePrismTheme } from '@utils/providers/prism'; +import { useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; + +const PrismThemeToggle = () => { + const intl = useIntl(); + const [isMounted, setIsMounted] = useState<boolean>(false); + + useEffect(() => { + setIsMounted(true); + }, []); + + const { theme, setTheme, resolvedTheme } = usePrismTheme(); + const [isDarkTheme, setIsDarkTheme] = useState<boolean>(theme === 'dark'); + + useEffect(() => { + if (theme === 'system') { + setIsDarkTheme(resolvedTheme === 'dark'); + } else { + setIsDarkTheme(theme === 'dark'); + } + }, [theme, resolvedTheme]); + + const updateTheme = () => { + isDarkTheme ? setTheme('light') : setTheme('dark'); + setIsDarkTheme(!isDarkTheme); + }; + + if (!isMounted) return <Spinner />; + + return ( + <Toggle + id="prism-theme" + label={intl.formatMessage({ + defaultMessage: 'Code blocks:', + description: 'PrismThemeToggle: toggle label', + })} + leftChoice={<SunIcon />} + rightChoice={<MoonIcon />} + value={isDarkTheme} + changeHandler={updateTheme} + /> + ); +}; + +export default PrismThemeToggle; diff --git a/src/components/Settings/Settings.tsx b/src/components/Settings/Settings.tsx index 80eb0c3..9f38ecb 100644 --- a/src/components/Settings/Settings.tsx +++ b/src/components/Settings/Settings.tsx @@ -1,6 +1,7 @@ import { CogIcon } from '@components/Icons'; import ThemeToggle from '@components/Settings/ThemeToggle/ThemeToggle'; import { useIntl } from 'react-intl'; +import PrismThemeToggle from './PrismThemeToggle/PrismThemeToggle'; import ReduceMotion from './ReduceMotion/ReduceMotion'; import styles from './Settings.module.scss'; @@ -18,6 +19,7 @@ const Settings = () => { </div> <ThemeToggle /> <ReduceMotion /> + <PrismThemeToggle /> </> ); }; diff --git a/src/i18n/en.json b/src/i18n/en.json index 3fa93ca..d1d48e5 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -583,6 +583,10 @@ "defaultMessage": "{topicsCount, plural, =0 {Related topics} one {Related topic} other {Related topics}}", "description": "RelatedTopics: widget title" }, + "w0UfY0": { + "defaultMessage": "Code blocks:", + "description": "PrismThemeToggle: toggle label" + }, "w1nIrj": { "defaultMessage": "Off", "description": "ReduceMotion: toggle off label" diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 2ac3ff3..85cfb3d 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -583,6 +583,10 @@ "defaultMessage": "{topicsCount, plural, =0 {Sujets liés} one {Sujet lié} other {Sujets liés}}", "description": "RelatedTopics: widget title" }, + "w0UfY0": { + "defaultMessage": "Blocs de code :", + "description": "PrismThemeToggle: toggle label" + }, "w1nIrj": { "defaultMessage": "Arrêt", "description": "ReduceMotion: toggle off label" diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 913861e..6df1a1d 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -2,6 +2,7 @@ import { MatomoProvider } from '@datapunt/matomo-tracker-react'; import { AppPropsWithLayout } from '@ts/types/app'; import { settings } from '@utils/config'; import { instance } from '@utils/helpers/matomo'; +import { PrismThemeProvider } from '@utils/providers/prism'; import { ThemeProvider } from 'next-themes'; import { useRouter } from 'next/router'; import { IntlProvider } from 'react-intl'; @@ -24,7 +25,9 @@ const MyApp = ({ Component, pageProps }: AppPropsWithLayout) => { enableColorScheme={true} enableSystem={true} > - {getLayout(<Component {...pageProps} />)} + <PrismThemeProvider> + {getLayout(<Component {...pageProps} />)} + </PrismThemeProvider> </ThemeProvider> </IntlProvider> </MatomoProvider> diff --git a/src/pages/article/[slug].tsx b/src/pages/article/[slug].tsx index 1799fb0..d0ea68a 100644 --- a/src/pages/article/[slug].tsx +++ b/src/pages/article/[slug].tsx @@ -21,6 +21,7 @@ import { useEffect } from 'react'; import { useIntl } from 'react-intl'; import { Blog, BlogPosting, Graph, WebPage } from 'schema-dts'; import '@utils/plugins/prism-color-scheme'; +import { usePrismTheme } from '@utils/providers/prism'; const SingleArticle: NextPageWithLayout<ArticleProps> = ({ post }) => { const { @@ -61,6 +62,15 @@ const SingleArticle: NextPageWithLayout<ArticleProps> = ({ post }) => { translateCopyButton(locale, intl); }, [intl, locale]); + const { setCodeBlocks } = usePrismTheme(); + + useEffect(() => { + const allPre: NodeListOf<HTMLPreElement> = document.querySelectorAll( + 'pre[data-prismjs-color-scheme' + ); + setCodeBlocks(allPre); + }, [setCodeBlocks, router.asPath]); + const webpageSchema: WebPage = { '@id': `${articleUrl}`, '@type': 'WebPage', diff --git a/src/utils/providers/prism.tsx b/src/utils/providers/prism.tsx new file mode 100644 index 0000000..840f9d8 --- /dev/null +++ b/src/utils/providers/prism.tsx @@ -0,0 +1,162 @@ +import { LocalStorage } from '@services/local-storage'; +import { + createContext, + FC, + useCallback, + useContext, + useEffect, + useState, +} from 'react'; + +export type PrismTheme = 'dark' | 'light' | 'system'; +export type ResolvedPrismTheme = 'dark' | 'light'; + +export type UsePrismThemeProps = { + themes: PrismTheme[]; + theme?: PrismTheme; + setTheme: (theme: PrismTheme) => void; + resolvedTheme?: ResolvedPrismTheme; + codeBlocks?: NodeListOf<HTMLPreElement>; + setCodeBlocks: (codeBlocks: NodeListOf<HTMLPreElement>) => void; +}; + +export type PrismThemeProviderProps = { + attribute?: string; + storageKey?: string; + themes?: PrismTheme[]; +}; + +export const PrismThemeContext = createContext<UsePrismThemeProps>({ + themes: ['dark', 'light', 'system'], + setTheme: (_) => {}, + setCodeBlocks: (_) => {}, +}); + +export const usePrismTheme = () => useContext(PrismThemeContext); + +const prefersDarkScheme = () => { + if (typeof window === 'undefined') return; + + return ( + window.matchMedia && + window.matchMedia('(prefers-color-scheme: dark)').matches + ); +}; + +const isValidTheme = (theme: string): boolean => { + return theme === 'dark' || theme === 'light' || theme === 'system'; +}; + +const getTheme = (key: string): PrismTheme | undefined => { + if (typeof window === 'undefined') return undefined; + const storageValue = LocalStorage.get(key); + + return storageValue && isValidTheme(storageValue) + ? (storageValue as PrismTheme) + : undefined; +}; + +export const PrismThemeProvider: FC<PrismThemeProviderProps> = ({ + attribute = 'data-prismjs-color-scheme', + storageKey = 'prismjs-color-scheme', + themes = ['dark', 'light', 'system'], + children, +}) => { + const getThemeFromSystem = useCallback(() => { + return prefersDarkScheme() ? 'dark' : 'light'; + }, []); + + const [prismTheme, setPrismTheme] = useState<PrismTheme>( + getTheme(storageKey) || 'system' + ); + + const updateTheme = (theme: PrismTheme) => { + setPrismTheme(theme); + }; + + useEffect(() => { + LocalStorage.set(storageKey, prismTheme); + }, [prismTheme, storageKey]); + + const [resolvedTheme, setResolvedTheme] = useState<ResolvedPrismTheme>(); + + useEffect(() => { + if (prismTheme === 'dark' || prismTheme === 'light') { + setResolvedTheme(prismTheme); + } else { + setResolvedTheme(getThemeFromSystem()); + } + }, [prismTheme, getThemeFromSystem]); + + const updateResolvedTheme = useCallback(() => { + setResolvedTheme(getThemeFromSystem()); + }, [getThemeFromSystem]); + + useEffect(() => { + window + .matchMedia('(prefers-color-scheme: dark)') + .addEventListener('change', updateResolvedTheme); + + return () => + window + .matchMedia('(prefers-color-scheme: dark)') + .removeEventListener('change', updateResolvedTheme); + }, [updateResolvedTheme]); + + const [preTags, setPreTags] = useState<NodeListOf<HTMLPreElement>>(); + + const updatePreTags = useCallback((tags: NodeListOf<HTMLPreElement>) => { + setPreTags(tags); + }, []); + + const updatePreTagsAttribute = useCallback(() => { + preTags?.forEach((pre) => { + pre.setAttribute(attribute, prismTheme); + }); + }, [attribute, preTags, prismTheme]); + + useEffect(() => { + updatePreTagsAttribute(); + }, [updatePreTagsAttribute, prismTheme]); + + const listenAttributeChange = useCallback( + (pre: HTMLPreElement) => { + var observer = new MutationObserver(function (mutations) { + mutations.forEach((record) => { + var mutatedPre = record.target as HTMLPreElement; + var newTheme = mutatedPre.getAttribute(attribute) as PrismTheme; + console.log('here'); + setPrismTheme(newTheme); + }); + }); + observer.observe(pre, { + attributes: true, + attributeFilter: [attribute], + }); + }, + [attribute] + ); + + useEffect(() => { + if (!preTags) return; + + preTags.forEach((pre) => { + listenAttributeChange(pre); + }); + }, [preTags, listenAttributeChange]); + + return ( + <PrismThemeContext.Provider + value={{ + themes, + theme: prismTheme, + setTheme: updateTheme, + codeBlocks: preTags, + setCodeBlocks: updatePreTags, + resolvedTheme, + }} + > + {children} + </PrismThemeContext.Provider> + ); +}; |
