-
+
+
{
+ const newValue = e.target.value;
+ if (!newValue) {
+ clearSearch();
+ return;
+ }
+ setSearchText(newValue);
+ }}
/>
+ {!searchText && (
+
+ )}
);
};
diff --git a/src/components/SnippetList.tsx b/src/components/SnippetList.tsx
index 0b9a2025..f4a9c643 100644
--- a/src/components/SnippetList.tsx
+++ b/src/components/SnippetList.tsx
@@ -1,43 +1,75 @@
import { motion, AnimatePresence, useReducedMotion } from "motion/react";
-import { useState } from "react";
+import { useEffect, useState } from "react";
+import { useSearchParams } from "react-router-dom";
import { useAppContext } from "@contexts/AppContext";
import { useSnippets } from "@hooks/useSnippets";
import { SnippetType } from "@types";
+import { QueryParams } from "@utils/enums";
+import {
+ getLanguageDisplayLogo,
+ getLanguageDisplayName,
+} from "@utils/languageUtils";
+import { slugify } from "@utils/slugify";
import { LeftAngleArrowIcon } from "./Icons";
import SnippetModal from "./SnippetModal";
const SnippetList = () => {
- const { language, snippet, setSnippet } = useAppContext();
- const { fetchedSnippets } = useSnippets();
- const [isModalOpen, setIsModalOpen] = useState(false);
-
+ const [searchParams, setSearchParams] = useSearchParams();
const shouldReduceMotion = useReducedMotion();
- if (!fetchedSnippets)
- return (
-
-
-
- );
+ const { language, subLanguage, snippet, setSnippet } = useAppContext();
+ const { fetchedSnippets } = useSnippets();
+
+ const [isModalOpen, setIsModalOpen] = useState
(false);
- const handleOpenModal = (activeSnippet: SnippetType) => {
+ const handleOpenModal = (selected: SnippetType) => () => {
setIsModalOpen(true);
- setSnippet(activeSnippet);
+ setSnippet(selected);
+ searchParams.set(QueryParams.SNIPPET, slugify(selected.title));
+ setSearchParams(searchParams);
};
const handleCloseModal = () => {
setIsModalOpen(false);
setSnippet(null);
+ searchParams.delete(QueryParams.SNIPPET);
+ setSearchParams(searchParams);
};
+ /**
+ * open the relevant modal if the snippet is in the search params
+ */
+ useEffect(() => {
+ const snippetSlug = searchParams.get(QueryParams.SNIPPET);
+ if (!snippetSlug) {
+ return;
+ }
+
+ const selectedSnippet = (fetchedSnippets ?? []).find(
+ (item) => slugify(item.title) === snippetSlug
+ );
+ if (selectedSnippet) {
+ handleOpenModal(selectedSnippet)();
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [fetchedSnippets, searchParams]);
+
+ if (!fetchedSnippets) {
+ return (
+
+
+
+ );
+ }
+
return (
<>
{fetchedSnippets.map((snippet, idx) => {
- const uniqueId = `${language.name}-${snippet.title}`;
+ const uniqueId = `${language.name}-${snippet.title}-${idx}`;
return (
{
opacity: 1,
y: 0,
transition: {
- delay: shouldReduceMotion ? 0 : 0.09 + idx * 0.05,
duration: shouldReduceMotion ? 0 : 0.2,
},
}}
@@ -55,7 +86,6 @@ const SnippetList = () => {
opacity: 0,
y: -20,
transition: {
- delay: idx * 0.01,
duration: shouldReduceMotion ? 0 : 0.09,
},
}}
@@ -67,12 +97,15 @@ const SnippetList = () => {
handleOpenModal(snippet)}
+ onClick={handleOpenModal(snippet)}
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.98 }}
>
-

+
{snippet.title}
diff --git a/src/components/SubLanguageSelector.tsx b/src/components/SubLanguageSelector.tsx
index 0b88210e..7b0d271c 100644
--- a/src/components/SubLanguageSelector.tsx
+++ b/src/components/SubLanguageSelector.tsx
@@ -1,42 +1,68 @@
+import { useNavigate } from "react-router-dom";
+
import { useAppContext } from "@contexts/AppContext";
import { LanguageType } from "@types";
+import { configureUserSelection } from "@utils/configureUserSelection";
+import { defaultSlugifiedSubLanguageName } from "@utils/consts";
+import { slugify } from "@utils/slugify";
type SubLanguageSelectorProps = {
- mainLanguage: LanguageType;
- afterSelect: () => void;
- onDropdownToggle: (openedLang: LanguageType) => void;
opened: boolean;
+ parentLanguage: LanguageType;
+ onDropdownToggle: (_: LanguageType["name"]) => void;
+ handleParentSelect: (_: LanguageType) => void;
+ afterSelect: () => void;
};
const SubLanguageSelector = ({
- mainLanguage,
+ opened,
+ parentLanguage,
+ handleParentSelect,
afterSelect,
onDropdownToggle,
- opened,
}: SubLanguageSelectorProps) => {
- const { language, setLanguage } = useAppContext();
+ const navigate = useNavigate();
- const handleSelect = (selected: LanguageType) => {
- setLanguage(selected);
- onDropdownToggle(mainLanguage);
- afterSelect();
- };
+ const { language, subLanguage, setSearchText } = useAppContext();
+
+ const handleSubLanguageSelect =
+ (selected: LanguageType["subLanguages"][number]) => async () => {
+ const {
+ language: newLanguage,
+ subLanguage: newSubLanguage,
+ category: newCategory,
+ } = await configureUserSelection({
+ languageName: parentLanguage.name,
+ subLanguageName: selected.name,
+ });
+
+ setSearchText("");
+ navigate(
+ `/${slugify(newLanguage.name)}/${slugify(newSubLanguage)}/${slugify(newCategory)}`
+ );
+ afterSelect();
+ };
return (
<>
setLanguage(mainLanguage)}
+ aria-selected={
+ subLanguage === defaultSlugifiedSubLanguageName &&
+ language.name === parentLanguage.name
+ }
+ onClick={() => handleParentSelect(parentLanguage)}
>
- {opened && (
- <>
- {mainLanguage.subLanguages.map((subLanguage) => (
- {
- handleSelect({
- ...subLanguage,
- mainLanguage: mainLanguage,
- subLanguages: [],
- });
- }}
- >
-
-
- ))}
- >
- )}
+ {opened &&
+ parentLanguage.subLanguages.map((sl) => (
+
+
+
+ ))}
>
);
};
diff --git a/src/contexts/AppContext.tsx b/src/contexts/AppContext.tsx
index ac7b51ce..4ebd3bea 100644
--- a/src/contexts/AppContext.tsx
+++ b/src/contexts/AppContext.tsx
@@ -1,43 +1,74 @@
-import { createContext, FC, useContext, useState } from "react";
+import { createContext, FC, useContext, useEffect, useState } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import { useLanguages } from "@hooks/useLanguages";
import { AppState, LanguageType, SnippetType } from "@types";
-
-// tokens
-const defaultLanguage: LanguageType = {
- name: "JAVASCRIPT",
- icon: "/icons/javascript.svg",
- subLanguages: [],
-};
-
-// TODO: add custom loading and error handling
-const defaultState: AppState = {
- language: defaultLanguage,
- setLanguage: () => {},
- category: "",
- setCategory: () => {},
- snippet: null,
- setSnippet: () => {},
-};
+import { configureUserSelection } from "@utils/configureUserSelection";
+import { defaultLanguage, defaultState } from "@utils/consts";
+import { slugify } from "@utils/slugify";
const AppContext = createContext(defaultState);
export const AppProvider: FC<{ children: React.ReactNode }> = ({
children,
}) => {
- const [language, setLanguage] = useState(defaultLanguage);
- const [category, setCategory] = useState("");
+ const navigate = useNavigate();
+ const { languageName, subLanguageName, categoryName } = useParams();
+
+ const { fetchedLanguages } = useLanguages();
+
+ const [language, setLanguage] = useState(null);
+ const [subLanguage, setSubLanguage] = useState(
+ null
+ );
+ const [category, setCategory] = useState(null);
const [snippet, setSnippet] = useState(null);
+ const [searchText, setSearchText] = useState("");
+
+ const configure = async () => {
+ const { language, subLanguage, category } = await configureUserSelection({
+ languageName,
+ subLanguageName,
+ categoryName,
+ });
+
+ setLanguage(language);
+ setSubLanguage(subLanguage);
+ setCategory(category);
+ };
+
+ useEffect(() => {
+ configure();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [fetchedLanguages, languageName, subLanguageName, categoryName]);
+
+ /**
+ * Set the default language if the language is not found in the URL.
+ */
+ useEffect(() => {
+ if (languageName === undefined) {
+ navigate(`/${slugify(defaultLanguage.name)}`, { replace: true });
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ if (language === null || category === null) {
+ return Loading...
;
+ }
return (
{children}
diff --git a/src/hooks/useCategories.ts b/src/hooks/useCategories.ts
index d1d4a631..65639acd 100644
--- a/src/hooks/useCategories.ts
+++ b/src/hooks/useCategories.ts
@@ -2,16 +2,20 @@ import { useMemo } from "react";
import { useAppContext } from "@contexts/AppContext";
import { CategoryType } from "@types";
-import { slugify } from "@utils/slugify";
+import { getLanguageFileName } from "@utils/languageUtils";
import { useFetch } from "./useFetch";
export const useCategories = () => {
- const { language } = useAppContext();
- const { data, loading, error } = useFetch(
- `/consolidated/${language.mainLanguage ? `${slugify(language.mainLanguage.name)}--${slugify(language.name)}` : slugify(language.name)}.json`
+ const { language, subLanguage } = useAppContext();
+
+ const fileName = useMemo(
+ () => getLanguageFileName(language.name, subLanguage),
+ [language.name, subLanguage]
);
+ const { data, loading, error } = useFetch(fileName);
+
const fetchedCategories = useMemo(() => {
return data ? data.map((item) => item.name) : [];
}, [data]);
diff --git a/src/hooks/useKeyboardNavigation.ts b/src/hooks/useKeyboardNavigation.ts
index 401ead2b..2082b1ac 100644
--- a/src/hooks/useKeyboardNavigation.ts
+++ b/src/hooks/useKeyboardNavigation.ts
@@ -1,14 +1,11 @@
import { useState } from "react";
-import { LanguageType } from "@types";
-
interface UseKeyboardNavigationProps {
- items: LanguageType[];
+ items: { languageName: string; subLanguageName?: string }[];
isOpen: boolean;
- onSelect: (item: LanguageType) => void;
+ toggleDropdown: (languageName: string) => void;
+ onSelect: (languageName: string, subLanguageName?: string) => void;
onClose: () => void;
- toggleDropdown: (openedLang: LanguageType) => void;
- openedLanguages: LanguageType[];
}
const keyboardEventKeys = {
@@ -25,15 +22,16 @@ type KeyboardEventKeys =
export const useKeyboardNavigation = ({
items,
isOpen,
- openedLanguages,
+ toggleDropdown,
onSelect,
onClose,
- toggleDropdown,
}: UseKeyboardNavigationProps) => {
const [focusedIndex, setFocusedIndex] = useState(-1);
const handleKeyDown = (event: React.KeyboardEvent) => {
- if (!isOpen) return;
+ if (!isOpen) {
+ return;
+ }
const key = event.key as KeyboardEventKeys;
@@ -49,26 +47,14 @@ export const useKeyboardNavigation = ({
break;
case "ArrowRight":
if (focusedIndex >= 0) {
- const selectedItem = items.filter(
- (item) =>
- !item.mainLanguage ||
- openedLanguages.includes(item.mainLanguage)
- )[focusedIndex];
-
- if (selectedItem.subLanguages.length > 0) {
- toggleDropdown(selectedItem);
- }
+ const selectedItem = items[focusedIndex];
+ toggleDropdown(selectedItem.languageName);
}
break;
case "Enter":
if (focusedIndex >= 0) {
- onSelect(
- items.filter(
- (item) =>
- !item.mainLanguage ||
- openedLanguages.includes(item.mainLanguage)
- )[focusedIndex]
- );
+ const selectedItem = items[focusedIndex];
+ onSelect(selectedItem.languageName, selectedItem.subLanguageName);
}
break;
case "Escape":
diff --git a/src/hooks/useSnippets.ts b/src/hooks/useSnippets.ts
index f0d30800..5cf4498c 100644
--- a/src/hooks/useSnippets.ts
+++ b/src/hooks/useSnippets.ts
@@ -1,18 +1,53 @@
+import { useMemo } from "react";
+import { useSearchParams } from "react-router-dom";
+
import { useAppContext } from "@contexts/AppContext";
import { CategoryType } from "@types";
+import { defaultCategoryName } from "@utils/consts";
+import { QueryParams } from "@utils/enums";
+import { getLanguageFileName } from "@utils/languageUtils";
import { slugify } from "@utils/slugify";
import { useFetch } from "./useFetch";
export const useSnippets = () => {
- const { language, category } = useAppContext();
- const { data, loading, error } = useFetch(
- `/consolidated/${language.mainLanguage ? `${slugify(language.mainLanguage.name)}--${slugify(language.name)}` : slugify(language.name)}.json`
+ const [searchParams] = useSearchParams();
+
+ const { language, subLanguage, category } = useAppContext();
+
+ const fileName = useMemo(
+ () => getLanguageFileName(language.name, subLanguage),
+ [language.name, subLanguage]
);
- const fetchedSnippets = data
- ? data.find((item) => item.name === category)?.snippets
- : [];
+ const { data, loading, error } = useFetch(fileName);
+
+ const fetchedSnippets = useMemo(() => {
+ if (!data) {
+ return [];
+ }
+
+ // If the category is the default category, return all snippets for the given language.
+ const snippets =
+ slugify(category) === slugify(defaultCategoryName)
+ ? data.flatMap((item) => item.snippets)
+ : (data.find((item) => item.name === category)?.snippets ?? []);
+
+ if (!searchParams.has(QueryParams.SEARCH)) {
+ return snippets;
+ }
+
+ return snippets.filter((item) => {
+ const searchTerm = (
+ searchParams.get(QueryParams.SEARCH) || ""
+ ).toLowerCase();
+ return (
+ item.title.toLowerCase().includes(searchTerm) ||
+ item.description.toLowerCase().includes(searchTerm) ||
+ item.tags.some((tag) => tag.toLowerCase().includes(searchTerm))
+ );
+ });
+ }, [category, data, searchParams]);
return { fetchedSnippets, loading, error };
};
diff --git a/src/main.tsx b/src/main.tsx
index 1a01bb18..957d266f 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -2,15 +2,14 @@ import "@styles/main.css";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
+import { BrowserRouter } from "react-router-dom";
-import { AppProvider } from "@contexts/AppContext";
-
-import App from "./App";
+import AppRouter from "@AppRouter";
createRoot(document.getElementById("root")!).render(
-
-
-
+
+
+
);
diff --git a/src/styles/main.css b/src/styles/main.css
index 5ffc06ef..d0f2a08f 100644
--- a/src/styles/main.css
+++ b/src/styles/main.css
@@ -76,6 +76,7 @@
--fw-normal: 400;
/* Border radius */
+ --br-sm: 0.25rem;
--br-md: 0.5rem;
--br-lg: 0.75rem;
}
@@ -301,12 +302,38 @@ abbr {
border: 1px solid var(--clr-border-primary);
border-radius: var(--br-md);
padding: 0.75em 1.125em;
+ position: relative;
&:is(:hover, :focus-within) {
border-color: var(--clr-accent);
}
}
+.search-field label {
+ position: absolute;
+ margin-left: 2.25em;
+}
+
+.search-field:hover, .search-field:hover * {
+ cursor: pointer;
+}
+
+/* hide the label when the search field input element is focused */
+.search-field input:focus + label {
+ display: none;
+}
+
+.search-field label kbd {
+ background-color: var(--clr-bg-secondary);
+ border: 1px solid var(--clr-border-primary);
+ border-radius: var(--br-sm);
+ padding: 0.25em 0.5em;
+ margin: 0 0.25em;
+ font-family: var(--ff-mono);
+ font-weight: var(--fw-bold);
+ color: var(--clr-text-primary);
+}
+
.search-field > input {
background-color: transparent;
border: none;
diff --git a/src/types/index.ts b/src/types/index.ts
index 8e489f44..a8001394 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -1,7 +1,6 @@
export type LanguageType = {
name: string;
icon: string;
- mainLanguage?: LanguageType;
subLanguages: {
name: string;
icon: string;
@@ -35,9 +34,10 @@ export type RawSnippetType = {
export type AppState = {
language: LanguageType;
- setLanguage: React.Dispatch>;
+ subLanguage: LanguageType["name"];
category: string;
- setCategory: React.Dispatch>;
snippet: SnippetType | null;
setSnippet: React.Dispatch>;
+ searchText: string;
+ setSearchText: React.Dispatch>;
};
diff --git a/src/utils/configureUserSelection.ts b/src/utils/configureUserSelection.ts
new file mode 100644
index 00000000..345f9376
--- /dev/null
+++ b/src/utils/configureUserSelection.ts
@@ -0,0 +1,78 @@
+import { CategoryType, LanguageType } from "@types";
+
+import {
+ defaultCategoryName,
+ defaultLanguage,
+ defaultSlugifiedSubLanguageName,
+} from "./consts";
+import { slugify } from "./slugify";
+
+export async function configureUserSelection({
+ languageName,
+ subLanguageName,
+ categoryName,
+}: {
+ languageName: string | undefined;
+ subLanguageName?: string | undefined;
+ categoryName?: string | undefined;
+}): Promise<{
+ language: LanguageType;
+ subLanguage: LanguageType["name"];
+ category: CategoryType["name"];
+}> {
+ const slugifiedLanguageName = languageName
+ ? slugify(languageName)
+ : undefined;
+ const slugifiedSubLanguageName = subLanguageName
+ ? slugify(subLanguageName)
+ : undefined;
+ const slugifiedCategoryName = categoryName
+ ? slugify(categoryName)
+ : undefined;
+
+ const fetchedLanguages: LanguageType[] = await fetch(
+ "/consolidated/_index.json"
+ ).then((res) => res.json());
+
+ const language =
+ fetchedLanguages.find(
+ (lang) => slugify(lang.name) === slugifiedLanguageName
+ ) ?? defaultLanguage;
+
+ const subLanguage = language.subLanguages.find(
+ (sl) => slugify(sl.name) === slugifiedSubLanguageName
+ );
+ const matchedSubLanguage =
+ subLanguage === undefined
+ ? defaultSlugifiedSubLanguageName
+ : slugify(subLanguage.name);
+
+ let category: CategoryType | undefined;
+ try {
+ const fetchedCategories: CategoryType[] = await fetch(
+ `/consolidated/${slugify(language.name)}.json`
+ ).then((res) => res.json());
+ category = fetchedCategories.find(
+ (item) => slugify(item.name) === slugifiedCategoryName
+ );
+
+ if (category === undefined) {
+ category = {
+ name: defaultCategoryName,
+ snippets: fetchedCategories.flatMap((item) => item.snippets),
+ };
+ }
+ } catch (_error) {
+ // This state should not be reached in the normal flow.
+ category = {
+ name: defaultCategoryName,
+ snippets: [],
+ };
+ }
+
+ return {
+ language,
+ subLanguage: matchedSubLanguage,
+ category: category.name,
+ };
+}
diff --git a/src/utils/consts.ts b/src/utils/consts.ts
new file mode 100644
index 00000000..fced7edd
--- /dev/null
+++ b/src/utils/consts.ts
@@ -0,0 +1,24 @@
+import { AppState, CategoryType, LanguageType } from "@types";
+
+import { slugify } from "./slugify";
+
+export const defaultLanguage: LanguageType = {
+ name: "JAVASCRIPT",
+ icon: "/icons/javascript.svg",
+ subLanguages: [],
+};
+
+export const defaultSlugifiedSubLanguageName = slugify("All Sub Languages");
+
+export const defaultCategoryName: CategoryType["name"] = "All Snippets";
+
+// TODO: add custom loading and error handling
+export const defaultState: AppState = {
+ language: defaultLanguage,
+ subLanguage: defaultSlugifiedSubLanguageName,
+ category: defaultCategoryName,
+ snippet: null,
+ setSnippet: () => {},
+ searchText: "",
+ setSearchText: () => {},
+};
diff --git a/src/utils/enums.ts b/src/utils/enums.ts
new file mode 100644
index 00000000..61de7575
--- /dev/null
+++ b/src/utils/enums.ts
@@ -0,0 +1,4 @@
+export enum QueryParams {
+ SEARCH = "q",
+ SNIPPET = "snippet",
+}
diff --git a/src/utils/languageUtils.ts b/src/utils/languageUtils.ts
new file mode 100644
index 00000000..f77e9822
--- /dev/null
+++ b/src/utils/languageUtils.ts
@@ -0,0 +1,31 @@
+import { LanguageType } from "@types";
+
+import { defaultSlugifiedSubLanguageName } from "./consts";
+import { reverseSlugify, slugify } from "./slugify";
+
+export function getLanguageDisplayName(
+ language: LanguageType["name"],
+ subLanguage: LanguageType["subLanguages"][number]["name"]
+) {
+ return slugify(subLanguage) !== defaultSlugifiedSubLanguageName
+ ? reverseSlugify(subLanguage).toLocaleUpperCase()
+ : language;
+}
+
+export function getLanguageDisplayLogo(
+ language: LanguageType["name"],
+ subLanguage: LanguageType["subLanguages"][number]["name"]
+) {
+ return slugify(subLanguage) !== defaultSlugifiedSubLanguageName
+ ? `/icons/${slugify(language)}--${slugify(subLanguage)}.svg`
+ : `/icons/${slugify(language)}.svg`;
+}
+
+export function getLanguageFileName(
+ language: LanguageType["name"],
+ subLanguage: LanguageType["subLanguages"][number]["name"]
+) {
+ return slugify(subLanguage) !== defaultSlugifiedSubLanguageName
+ ? `/consolidated/${slugify(language)}--${slugify(subLanguage)}.json`
+ : `/consolidated/${slugify(language)}.json`;
+}
diff --git a/tests/configureUserSelection.test.ts b/tests/configureUserSelection.test.ts
new file mode 100644
index 00000000..2829dcb9
--- /dev/null
+++ b/tests/configureUserSelection.test.ts
@@ -0,0 +1,173 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+
+import { CategoryType, LanguageType } from "../src/types";
+import { configureUserSelection } from "../src/utils/configureUserSelection";
+import { defaultCategoryName, defaultLanguage } from "../src/utils/consts";
+import { slugify } from "../src/utils/slugify";
+
+vi.mock("../src/utils/slugify");
+
+describe("configureUserSelection", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ const mockFetch = (urlResponses: Record) => {
+ global.fetch = vi.fn(async (url) => {
+ const response = urlResponses[url as string];
+ if (response instanceof Error) {
+ throw response;
+ }
+ return {
+ json: async () => response,
+ };
+ }) as unknown as typeof fetch;
+ };
+
+ it("should return default language and category if no arguments are provided", async () => {
+ mockFetch({
+ "/consolidated/_index.json": [],
+ });
+
+ const result = await configureUserSelection({
+ languageName: undefined,
+ categoryName: undefined,
+ });
+
+ expect(result).toEqual({
+ language: defaultLanguage,
+ category: defaultCategoryName,
+ });
+
+ expect(fetch).toHaveBeenCalledWith("/consolidated/_index.json");
+ });
+
+ it("should match the language and default to the first category if categoryName is undefined", async () => {
+ const mockLanguages: LanguageType[] = [
+ {
+ name: "JavaScript",
+ icon: "js-icon",
+ subLanguages: [],
+ },
+ {
+ name: "Python",
+ icon: "python-icon",
+ subLanguages: [],
+ },
+ ];
+ const mockCategories: CategoryType[] = [
+ {
+ name: "Basics",
+ snippets: [],
+ },
+ {
+ name: "Advanced",
+ snippets: [],
+ },
+ ];
+
+ mockFetch({
+ "/consolidated/_index.json": mockLanguages,
+ "/consolidated/javascript.json": mockCategories,
+ });
+
+ vi.mocked(slugify).mockImplementation((str) => str.toLowerCase());
+
+ const result = await configureUserSelection({
+ languageName: "JavaScript",
+ categoryName: undefined,
+ });
+
+ expect(result).toEqual({
+ language: mockLanguages[0],
+ category: defaultCategoryName,
+ });
+
+ expect(slugify).toHaveBeenCalledWith("JavaScript");
+ expect(fetch).toHaveBeenCalledWith("/consolidated/_index.json");
+ expect(fetch).toHaveBeenCalledWith("/consolidated/javascript.json");
+ });
+
+ it("should match the language and specific category if both arguments are provided", async () => {
+ const mockLanguages: LanguageType[] = [
+ {
+ name: "JavaScript",
+ icon: "js-icon",
+ subLanguages: [],
+ },
+ {
+ name: "Python",
+ icon: "python-icon",
+ subLanguages: [],
+ },
+ ];
+ const mockCategories: CategoryType[] = [
+ {
+ name: "Basics",
+ snippets: [],
+ },
+ {
+ name: "Advanced",
+ snippets: [],
+ },
+ ];
+
+ mockFetch({
+ "/consolidated/_index.json": mockLanguages,
+ "/consolidated/javascript.json": mockCategories,
+ });
+
+ vi.mocked(slugify).mockImplementation((str) => str.toLowerCase());
+
+ const result = await configureUserSelection({
+ languageName: "JavaScript",
+ categoryName: "Advanced",
+ });
+
+ expect(result).toEqual({
+ language: mockLanguages[0],
+ category: mockCategories[1].name,
+ });
+
+ expect(slugify).toHaveBeenCalledWith("JavaScript");
+ expect(slugify).toHaveBeenCalledWith("Advanced");
+ expect(fetch).toHaveBeenCalledWith("/consolidated/_index.json");
+ expect(fetch).toHaveBeenCalledWith("/consolidated/javascript.json");
+ });
+
+ it("should return default category if category fetch fails", async () => {
+ const mockLanguages: LanguageType[] = [
+ {
+ name: "JavaScript",
+ icon: "js-icon",
+ subLanguages: [],
+ },
+ {
+ name: "Python",
+ icon: "python-icon",
+ subLanguages: [],
+ },
+ ];
+
+ mockFetch({
+ "/consolidated/_index.json": mockLanguages,
+ "/consolidated/javascript.json": new Error("Network error"),
+ });
+
+ vi.mocked(slugify).mockImplementation((str) => str.toLowerCase());
+
+ const result = await configureUserSelection({
+ languageName: "JavaScript",
+ categoryName: undefined,
+ });
+
+ expect(result).toEqual({
+ language: mockLanguages[0],
+ category: defaultCategoryName,
+ });
+
+ expect(slugify).toHaveBeenCalledWith("JavaScript");
+ expect(fetch).toHaveBeenCalledWith("/consolidated/_index.json");
+ expect(fetch).toHaveBeenCalledWith("/consolidated/javascript.json");
+ });
+});
diff --git a/tests/languageUtils.test.ts b/tests/languageUtils.test.ts
new file mode 100644
index 00000000..d6b8f383
--- /dev/null
+++ b/tests/languageUtils.test.ts
@@ -0,0 +1,53 @@
+import { describe, it, expect } from "vitest";
+
+import { defaultSlugifiedSubLanguageName } from "../src/utils/consts";
+import {
+ getLanguageDisplayName,
+ getLanguageDisplayLogo,
+ getLanguageFileName,
+} from "../src/utils/languageUtils";
+
+describe(getLanguageDisplayName.name, () => {
+ it("should return the upper cased subLanguage if it is not the default", () => {
+ const result = getLanguageDisplayName("JAVASCRIPT", "React");
+ expect(result).toBe("REACT");
+ });
+
+ it("should return the language name if subLanguage is the default", () => {
+ const result = getLanguageDisplayName(
+ "JAVASCRIPT",
+ defaultSlugifiedSubLanguageName
+ );
+ expect(result).toBe("JAVASCRIPT");
+ });
+});
+
+describe(getLanguageDisplayLogo.name, () => {
+ it("should return a concatenation of the language and subLanguage if subLanguage is not the default", () => {
+ const result = getLanguageDisplayLogo("JAVASCRIPT", "React");
+ expect(result).toBe("/icons/javascript--react.svg");
+ });
+
+ it("should return the language name only if subLanguage is the default", () => {
+ const result = getLanguageDisplayLogo(
+ "JAVASCRIPT",
+ defaultSlugifiedSubLanguageName
+ );
+ expect(result).toBe("/icons/javascript.svg");
+ });
+});
+
+describe(getLanguageFileName.name, () => {
+ it("should return a concatenation of the language and subLanguage if subLanguage is not the default", () => {
+ const result = getLanguageFileName("JAVASCRIPT", "React");
+ expect(result).toBe("/consolidated/javascript--react.json");
+ });
+
+ it("should return the language name only if subLanguage is the default", () => {
+ const result = getLanguageFileName(
+ "JAVASCRIPT",
+ defaultSlugifiedSubLanguageName
+ );
+ expect(result).toBe("/consolidated/javascript.json");
+ });
+});
pFad - Phonifier reborn
Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy