diff --git a/cspell.json b/cspell.json index 86c63589..e7ee57ab 100644 --- a/cspell.json +++ b/cspell.json @@ -11,6 +11,7 @@ "ignorePaths": [ "node_modules", "dist", - "public" + "public", + "src/consts/highlighter-styles.ts" ] } diff --git a/src/components/CodePreview.tsx b/src/components/CodePreview.tsx index 5e72711f..7b73b684 100644 --- a/src/components/CodePreview.tsx +++ b/src/components/CodePreview.tsx @@ -1,9 +1,6 @@ -import { useEffect, useState } from "react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; -import { - oneDark, - oneLight, -} from "react-syntax-highlighter/dist/esm/styles/prism"; + +import { useAppContext } from "@contexts/AppContext"; import CopyToClipboard from "./CopyToClipboard"; @@ -13,34 +10,22 @@ type Props = { }; const CodePreview = ({ language = "markdown", code }: Props) => { - const [theme, setTheme] = useState<"dark" | "light">("dark"); - - useEffect(() => { - const handleThemeChange = () => { - const newTheme = document.documentElement.getAttribute("data-theme") as - | "dark" - | "light"; - setTheme(newTheme || "dark"); - }; - - handleThemeChange(); - const observer = new MutationObserver(handleThemeChange); - observer.observe(document.documentElement, { - attributes: true, - attributeFilter: ["data-theme"], - }); - - return () => observer.disconnect(); - }, []); + const { highlighterStyle } = useAppContext(); return (
{code} diff --git a/src/components/HighlighterStyleSelector.tsx b/src/components/HighlighterStyleSelector.tsx new file mode 100644 index 00000000..f663b634 --- /dev/null +++ b/src/components/HighlighterStyleSelector.tsx @@ -0,0 +1,33 @@ +import { highlighterStyles } from "@consts/highlighter-styles"; +import { useAppContext } from "@contexts/AppContext"; +import { SelectorOption } from "@types"; + +import Selector from "./Selector"; + +const HighlighterStyleSelector = () => { + const { highlighterStyle, toggleHighlighterStyle } = useAppContext(); + + const options = highlighterStyles.map((style) => ({ + name: style.name, + })); + + const handleSelect = (option: SelectorOption) => { + const selected = highlighterStyles.find( + (style) => style.name === option.name + ); + if (!selected) { + return; + } + toggleHighlighterStyle(selected); + }; + + return ( + + ); +}; + +export default HighlighterStyleSelector; diff --git a/src/components/LanguageSelector.tsx b/src/components/LanguageSelector.tsx index ae30b12f..e273f5e1 100644 --- a/src/components/LanguageSelector.tsx +++ b/src/components/LanguageSelector.tsx @@ -1,115 +1,46 @@ -import { useRef, useEffect, useState } from "react"; +import { useMemo } from "react"; import { useAppContext } from "@contexts/AppContext"; -import { useKeyboardNavigation } from "@hooks/useKeyboardNavigation"; import { useLanguages } from "@hooks/useLanguages"; -import { LanguageType } from "@types"; +import { SelectorOption } from "@types"; -// Inspired by https://blog.logrocket.com/creating-custom-select-dropdown-css/ +import Selector from "./Selector"; const LanguageSelector = () => { - const { language, setLanguage } = useAppContext(); + const { language, toggleLanguage } = useAppContext(); const { fetchedLanguages, loading, error } = useLanguages(); - const dropdownRef = useRef(null); - const [isOpen, setIsOpen] = useState(false); - - const handleSelect = (selected: LanguageType) => { - setLanguage(selected); - setIsOpen(false); - }; - - const { focusedIndex, handleKeyDown, resetFocus, focusFirst } = - useKeyboardNavigation({ - items: fetchedLanguages, - isOpen, - onSelect: handleSelect, - onClose: () => setIsOpen(false), - }); - - const handleBlur = () => { - setTimeout(() => { - if ( - dropdownRef.current && - !dropdownRef.current.contains(document.activeElement) - ) { - setIsOpen(false); - } - }, 0); - }; - - const toggleDropdown = () => { - setIsOpen((prev) => { - if (!prev) setTimeout(focusFirst, 0); - return !prev; - }); - }; + const options = useMemo( + () => + fetchedLanguages.map((item) => ({ + name: item.lang, + icon: item.icon, + })), + [fetchedLanguages] + ); - useEffect(() => { - if (!isOpen) { - resetFocus(); + const handleSelect = (option: SelectorOption) => { + const selected = fetchedLanguages.find((lang) => lang.lang === option.name); + if (!selected) { + return; } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isOpen]); + toggleLanguage(selected); + }; - useEffect(() => { - if (isOpen && focusedIndex >= 0) { - const element = document.querySelector( - `.selector__item:nth-child(${focusedIndex + 1})` - ) as HTMLElement; - element?.focus(); - } - }, [isOpen, focusedIndex]); + if (loading) { + return

Loading languages...

; + } - if (loading) return

Loading languages...

; - if (error) return

Error fetching languages: {error}

; + if (error) { + return

Error fetching languages: {error}

; + } return ( -
- - {isOpen && ( -
    - {fetchedLanguages.map((lang, index) => ( -
  • handleSelect(lang)} - className={`selector__item ${ - language.lang === lang.lang ? "selected" : "" - } ${focusedIndex === index ? "focused" : ""}`} - aria-selected={language.lang === lang.lang} - > - -
  • - ))} -
- )} -
+ ); }; diff --git a/src/components/Selector.tsx b/src/components/Selector.tsx new file mode 100644 index 00000000..3fb8b8b9 --- /dev/null +++ b/src/components/Selector.tsx @@ -0,0 +1,116 @@ +/** + * Inspired by https://blog.logrocket.com/creating-custom-select-dropdown-css/ + */ + +import { FC, useEffect, useRef, useState } from "react"; + +import { useKeyboardNavigation } from "@hooks/useKeyboardNavigation"; +import { SelectorOption } from "@types"; + +interface SelectorProps { + options: Array; + selectedOption: SelectorOption; + handleSelect: (option: SelectorOption) => void; +} + +const Selector: FC = (props) => { + const { options, selectedOption, handleSelect } = props; + + const dropdownRef = useRef(null); + const [isOpen, setIsOpen] = useState(false); + + const { focusedIndex, handleKeyDown, resetFocus, focusFirst } = + useKeyboardNavigation({ + options, + isOpen, + onSelect: handleSelect, + onClose: () => setIsOpen(false), + }); + + const handleBlur = () => { + setTimeout(() => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(document.activeElement) + ) { + setIsOpen(false); + } + }, 0); + }; + + const toggleDropdown = () => { + setIsOpen((prev) => { + if (!prev) setTimeout(focusFirst, 0); + return !prev; + }); + }; + + useEffect(() => { + if (!isOpen) { + resetFocus(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOpen]); + + useEffect(() => { + if (isOpen && focusedIndex >= 0) { + const element = document.querySelector( + `.selector__item:nth-child(${focusedIndex + 1})` + ) as HTMLElement; + element?.focus(); + } + }, [isOpen, focusedIndex]); + + return ( +
+ + {isOpen && ( +
    + {options.map((item, index) => ( +
  • { + handleSelect(item); + setIsOpen(false); + }} + className={`selector__item ${ + selectedOption.name === item.name ? "selected" : "" + } ${focusedIndex === index ? "focused" : ""}`} + aria-selected={selectedOption.name === item.name} + > + +
  • + ))} +
+ )} +
+ ); +}; + +export default Selector; diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx index a8cdd66f..92be1fa1 100644 --- a/src/components/ThemeToggle.tsx +++ b/src/components/ThemeToggle.tsx @@ -1,29 +1,7 @@ -import { useState, useEffect } from "react"; +import { useAppContext } from "@contexts/AppContext"; const ThemeToggle = () => { - const [theme, setTheme] = useState("dark"); - - useEffect(() => { - // if the theme isn't set, use the user's system preference - const savedTheme = localStorage.getItem("theme"); - if (savedTheme) { - setTheme(savedTheme); - document.documentElement.setAttribute("data-theme", savedTheme); - } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) { - setTheme("dark"); - document.documentElement.setAttribute("data-theme", "dark"); - } else { - setTheme("light"); - document.documentElement.setAttribute("data-theme", "light"); - } - }, []); - - const toggleTheme = () => { - const newTheme = theme === "dark" ? "light" : "dark"; - setTheme(newTheme); - localStorage.setItem("theme", newTheme); - document.documentElement.setAttribute("data-theme", newTheme); - }; + const { theme, toggleTheme } = useAppContext(); return (