diff --git a/cspell-dict.txt b/cspell-dict.txt
index 8218957c..5211f2a5 100644
--- a/cspell-dict.txt
+++ b/cspell-dict.txt
@@ -1,3 +1,5 @@
quicksnip
slugified
slugifyed
+sublanguage
+fastapi
\ No newline at end of file
diff --git a/snippets/javascript/[react]/basics/hello-world.md b/snippets/javascript/[react]/basics/hello-world.md
new file mode 100644
index 00000000..5cdaba09
--- /dev/null
+++ b/snippets/javascript/[react]/basics/hello-world.md
@@ -0,0 +1,21 @@
+---
+title: Hello, World!
+description: Show Hello World on the page.
+author: ACR1209
+tags: printing,hello-world
+---
+
+```tsx
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+const App = () => {
+ return (
+
+
Hello, World!
+
+ );
+};
+
+ReactDOM.render(, document.getElementById('root'));
+```
diff --git a/snippets/javascript/[react]/icon.svg b/snippets/javascript/[react]/icon.svg
new file mode 100644
index 00000000..b9025712
--- /dev/null
+++ b/snippets/javascript/[react]/icon.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/snippets/python/[fastapi]/basics/hello-world.md b/snippets/python/[fastapi]/basics/hello-world.md
new file mode 100644
index 00000000..69afb9cf
--- /dev/null
+++ b/snippets/python/[fastapi]/basics/hello-world.md
@@ -0,0 +1,21 @@
+---
+title: Hello, World!
+description: Returns Hello, World! when it recives a GET request made to the root endpoint.
+author: ACR1209
+tags: printing,hello-world,web,api
+---
+
+```py
+from typing import Union
+from fastapi import FastAPI
+
+app = FastAPI()
+
+
+@app.get("/")
+def read_root():
+ return {"msg": "Hello, World!"}
+
+# Usage:
+# -> Go to http://127.0.0.1:8000/ and you'll see {"msg", "Hello, World!"}
+```
diff --git a/snippets/python/[fastapi]/icon.svg b/snippets/python/[fastapi]/icon.svg
new file mode 100644
index 00000000..a7be660d
--- /dev/null
+++ b/snippets/python/[fastapi]/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/LanguageSelector.tsx b/src/components/LanguageSelector.tsx
index d8e208fe..8a62d651 100644
--- a/src/components/LanguageSelector.tsx
+++ b/src/components/LanguageSelector.tsx
@@ -1,28 +1,50 @@
-import { useRef, useEffect, useState } from "react";
+import { useRef, useEffect, useState, useMemo } from "react";
import { useAppContext } from "@contexts/AppContext";
import { useKeyboardNavigation } from "@hooks/useKeyboardNavigation";
import { useLanguages } from "@hooks/useLanguages";
import { LanguageType } from "@types";
+import SubLanguageSelector from "./SubLanguageSelector";
+
// Inspired by https://blog.logrocket.com/creating-custom-select-dropdown-css/
const LanguageSelector = () => {
const { language, setLanguage } = useAppContext();
const { fetchedLanguages, loading, error } = useLanguages();
+ const allLanguages = useMemo(
+ () =>
+ fetchedLanguages.flatMap((lang) =>
+ lang.subLanguages.length > 0
+ ? [
+ lang,
+ ...lang.subLanguages.map((subLang) => ({
+ ...subLang,
+ mainLanguage: lang,
+ subLanguages: [],
+ })),
+ ]
+ : [lang]
+ ),
+ [fetchedLanguages]
+ );
const dropdownRef = useRef(null);
const [isOpen, setIsOpen] = useState(false);
+ const [openedLanguages, setOpenedLanguages] = useState([]);
const handleSelect = (selected: LanguageType) => {
setLanguage(selected);
setIsOpen(false);
+ setOpenedLanguages([]);
};
const { focusedIndex, handleKeyDown, resetFocus, focusFirst } =
useKeyboardNavigation({
- items: fetchedLanguages,
+ items: allLanguages,
isOpen,
+ openedLanguages,
+ toggleDropdown: (openedLang) => handleToggleSublanguage(openedLang),
onSelect: handleSelect,
onClose: () => setIsOpen(false),
});
@@ -38,6 +60,20 @@ const LanguageSelector = () => {
}, 0);
};
+ const handleToggleSublanguage = (openedLang: LanguageType) => {
+ const isAlreadyOpened = openedLanguages.some(
+ (lang) => lang.name === openedLang.name
+ );
+
+ if (!isAlreadyOpened) {
+ setOpenedLanguages((prev) => [...prev, openedLang]);
+ } else {
+ setOpenedLanguages((prev) =>
+ prev.filter((lang) => lang.name !== openedLang.name)
+ );
+ }
+ };
+
const toggleDropdown = () => {
setIsOpen((prev) => {
if (!prev) setTimeout(focusFirst, 0);
@@ -52,6 +88,13 @@ const LanguageSelector = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);
+ useEffect(() => {
+ if (language.mainLanguage) {
+ handleToggleSublanguage(language.mainLanguage);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [language]);
+
useEffect(() => {
if (isOpen && focusedIndex >= 0) {
const element = document.querySelector(
@@ -90,23 +133,35 @@ const LanguageSelector = () => {
onKeyDown={handleKeyDown}
tabIndex={-1}
>
- {fetchedLanguages.map((lang, index) => (
- handleSelect(lang)}
- className={`selector__item ${
- language.name === lang.name ? "selected" : ""
- } ${focusedIndex === index ? "focused" : ""}`}
- aria-selected={language.name === lang.name}
- >
-
-
- ))}
+ {fetchedLanguages.map((lang, index) =>
+ lang.subLanguages.length > 0 ? (
+ {
+ setIsOpen(false);
+ }}
+ opened={openedLanguages.includes(lang)}
+ onDropdownToggle={handleToggleSublanguage}
+ />
+ ) : (
+ handleSelect(lang)}
+ className={`selector__item ${
+ language.name === lang.name ? "selected" : ""
+ } ${focusedIndex === index ? "focused" : ""}`}
+ aria-selected={language.name === lang.name}
+ >
+
+
+ )
+ )}
)}
diff --git a/src/components/SnippetList.tsx b/src/components/SnippetList.tsx
index 8868b9f1..0b9a2025 100644
--- a/src/components/SnippetList.tsx
+++ b/src/components/SnippetList.tsx
@@ -87,7 +87,7 @@ const SnippetList = () => {
)}
diff --git a/src/components/SubLanguageSelector.tsx b/src/components/SubLanguageSelector.tsx
new file mode 100644
index 00000000..0b88210e
--- /dev/null
+++ b/src/components/SubLanguageSelector.tsx
@@ -0,0 +1,86 @@
+import { useAppContext } from "@contexts/AppContext";
+import { LanguageType } from "@types";
+
+type SubLanguageSelectorProps = {
+ mainLanguage: LanguageType;
+ afterSelect: () => void;
+ onDropdownToggle: (openedLang: LanguageType) => void;
+ opened: boolean;
+};
+
+const SubLanguageSelector = ({
+ mainLanguage,
+ afterSelect,
+ onDropdownToggle,
+ opened,
+}: SubLanguageSelectorProps) => {
+ const { language, setLanguage } = useAppContext();
+
+ const handleSelect = (selected: LanguageType) => {
+ setLanguage(selected);
+ onDropdownToggle(mainLanguage);
+ afterSelect();
+ };
+
+ return (
+ <>
+ setLanguage(mainLanguage)}
+ >
+
+
+
+ {opened && (
+ <>
+ {mainLanguage.subLanguages.map((subLanguage) => (
+ {
+ handleSelect({
+ ...subLanguage,
+ mainLanguage: mainLanguage,
+ subLanguages: [],
+ });
+ }}
+ >
+
+
+ ))}
+ >
+ )}
+ >
+ );
+};
+
+export default SubLanguageSelector;
diff --git a/src/contexts/AppContext.tsx b/src/contexts/AppContext.tsx
index 30dd366e..ac7b51ce 100644
--- a/src/contexts/AppContext.tsx
+++ b/src/contexts/AppContext.tsx
@@ -6,7 +6,7 @@ import { AppState, LanguageType, SnippetType } from "@types";
const defaultLanguage: LanguageType = {
name: "JAVASCRIPT",
icon: "/icons/javascript.svg",
- subIndexes: [],
+ subLanguages: [],
};
// TODO: add custom loading and error handling
diff --git a/src/hooks/useCategories.ts b/src/hooks/useCategories.ts
index 4bd00685..d1d4a631 100644
--- a/src/hooks/useCategories.ts
+++ b/src/hooks/useCategories.ts
@@ -9,7 +9,7 @@ import { useFetch } from "./useFetch";
export const useCategories = () => {
const { language } = useAppContext();
const { data, loading, error } = useFetch(
- `/consolidated/${slugify(language.name)}.json`
+ `/consolidated/${language.mainLanguage ? `${slugify(language.mainLanguage.name)}--${slugify(language.name)}` : slugify(language.name)}.json`
);
const fetchedCategories = useMemo(() => {
diff --git a/src/hooks/useKeyboardNavigation.ts b/src/hooks/useKeyboardNavigation.ts
index 46bda82c..401ead2b 100644
--- a/src/hooks/useKeyboardNavigation.ts
+++ b/src/hooks/useKeyboardNavigation.ts
@@ -7,11 +7,14 @@ interface UseKeyboardNavigationProps {
isOpen: boolean;
onSelect: (item: LanguageType) => void;
onClose: () => void;
+ toggleDropdown: (openedLang: LanguageType) => void;
+ openedLanguages: LanguageType[];
}
const keyboardEventKeys = {
arrowDown: "ArrowDown",
arrowUp: "ArrowUp",
+ arrowRight: "ArrowRight",
enter: "Enter",
escape: "Escape",
} as const;
@@ -22,8 +25,10 @@ type KeyboardEventKeys =
export const useKeyboardNavigation = ({
items,
isOpen,
+ openedLanguages,
onSelect,
onClose,
+ toggleDropdown,
}: UseKeyboardNavigationProps) => {
const [focusedIndex, setFocusedIndex] = useState(-1);
@@ -42,9 +47,28 @@ export const useKeyboardNavigation = ({
case "ArrowUp":
setFocusedIndex((prev) => (prev > 0 ? prev - 1 : items.length - 1));
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);
+ }
+ }
+ break;
case "Enter":
if (focusedIndex >= 0) {
- onSelect(items[focusedIndex]);
+ onSelect(
+ items.filter(
+ (item) =>
+ !item.mainLanguage ||
+ openedLanguages.includes(item.mainLanguage)
+ )[focusedIndex]
+ );
}
break;
case "Escape":
diff --git a/src/hooks/useSnippets.ts b/src/hooks/useSnippets.ts
index a9d85499..f0d30800 100644
--- a/src/hooks/useSnippets.ts
+++ b/src/hooks/useSnippets.ts
@@ -7,7 +7,7 @@ import { useFetch } from "./useFetch";
export const useSnippets = () => {
const { language, category } = useAppContext();
const { data, loading, error } = useFetch(
- `/consolidated/${slugify(language.name)}.json`
+ `/consolidated/${language.mainLanguage ? `${slugify(language.mainLanguage.name)}--${slugify(language.name)}` : slugify(language.name)}.json`
);
const fetchedSnippets = data
diff --git a/src/styles/main.css b/src/styles/main.css
index 9f8436bb..5ffc06ef 100644
--- a/src/styles/main.css
+++ b/src/styles/main.css
@@ -403,6 +403,37 @@ abbr {
border-radius: var(--br-md);
}
+.sublanguage__item {
+ margin-left: 1.5rem;
+}
+
+.sublanguage__button{
+ margin-left: auto;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 1rem 0.5rem;
+ border: 0;
+ background-color: transparent;
+}
+
+.sublanguage__arrow {
+ border-left: 7px solid transparent;
+ border-right: 7px solid transparent;
+ border-top: 7px solid var(--clr-text-primary); /* [1] */
+ transform: rotate(-90deg);
+ transition: transform 100ms ease;
+ cursor: pointer;
+}
+
+[aria-expanded="true"] .sublanguage__arrow {
+ transform: rotate(0deg);
+}
+
+.selector__item.selected .sublanguage__arrow {
+ border-top-color: var(--clr-text-tertiary);
+}
+
.selector__item label {
width: 100%;
padding: 0.25em 0.75em;
diff --git a/src/types/index.ts b/src/types/index.ts
index da4ade07..8e489f44 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -1,7 +1,8 @@
export type LanguageType = {
name: string;
icon: string;
- subIndexes: {
+ mainLanguage?: LanguageType;
+ subLanguages: {
name: string;
icon: string;
}[];
@@ -19,6 +20,7 @@ export type SnippetType = {
code: string;
tags: string[];
contributors: string[];
+ extension: string;
};
export type RawSnippetType = {
@@ -28,6 +30,7 @@ export type RawSnippetType = {
code: string;
tags: string;
contributors?: string;
+ extension: string;
};
export type AppState = {
diff --git a/utils/consolidateSnippets.ts b/utils/consolidateSnippets.ts
index d8d47911..280278ee 100644
--- a/utils/consolidateSnippets.ts
+++ b/utils/consolidateSnippets.ts
@@ -23,7 +23,7 @@ const index: LanguageType[] = [];
for (const language of languages) {
copyFileSync(language.icon, join(iconPath, `${slugify(language.name)}.svg`));
- const subIndexes: LanguageType["subIndexes"] = [];
+ const subLanguages: LanguageType["subLanguages"] = [];
for (const subLanguage of language.subLanguages) {
const joinedName = `${slugify(language.name)}--${slugify(subLanguage.name)}`;
@@ -31,7 +31,7 @@ for (const language of languages) {
const subLanguageFilePath = join(dataPath, `${joinedName}.json`);
copyFileSync(subLanguage.icon, join(iconPath, iconName));
- subIndexes.push({
+ subLanguages.push({
name: subLanguage.name.toUpperCase(),
icon: `/icons/${iconName}`,
});
@@ -45,7 +45,7 @@ for (const language of languages) {
index.push({
name: language.name.toUpperCase(),
icon: `/icons/${slugify(language.name)}.svg`,
- subIndexes,
+ subLanguages,
});
const languageFilePath = join(dataPath, `${slugify(language.name)}.json`);
diff --git a/utils/snippetParser.ts b/utils/snippetParser.ts
index c64f2988..4b4a4d7d 100644
--- a/utils/snippetParser.ts
+++ b/utils/snippetParser.ts
@@ -73,6 +73,7 @@ function parseSnippet(
}
cursor += match[0].length;
+ const extension = match[0].replace(/[\r\n`-]/g, "");
match = codeRegex.exec(fromCursor());
if (match === null) {
@@ -94,6 +95,7 @@ function parseSnippet(
.map((contributor) => contributor.trim())
.filter((contributor) => contributor),
code: code.replace(/\r\n/g, "\n"),
+ extension,
};
}