aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-02-01 15:15:55 +0100
committerArmand Philippot <git@armandphilippot.com>2022-02-01 15:15:55 +0100
commit1e370817560c905a0a3520e245c317f308b6a5e7 (patch)
treef5a1e2c3b8ddbff2cb4d94eff82830388158e4c9 /src
parent8f8a3957b5d3b33bafaa0a6afe4187f75d6dd2b7 (diff)
chore: add a new settings to handle prism theme from toolbar
Diffstat (limited to 'src')
-rw-r--r--src/components/MDX/CodeBlock/CodeBlock.tsx10
-rw-r--r--src/components/Settings/PrismThemeToggle/PrismThemeToggle.tsx49
-rw-r--r--src/components/Settings/Settings.tsx2
-rw-r--r--src/i18n/en.json4
-rw-r--r--src/i18n/fr.json4
-rw-r--r--src/pages/_app.tsx5
-rw-r--r--src/pages/article/[slug].tsx10
-rw-r--r--src/utils/providers/prism.tsx162
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>
+ );
+};