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 (