Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cspell-dict.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
quicksnip
slugified
slugifyed
sublanguage
fastapi
21 changes: 21 additions & 0 deletions snippets/javascript/[react]/basics/hello-world.md
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<h1>Hello, World!</h1>
</div>
);
};

ReactDOM.render(<App />, document.getElementById('root'));
```
9 changes: 9 additions & 0 deletions snippets/javascript/[react]/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions snippets/python/[fastapi]/basics/hello-world.md
Original file line number Diff line number Diff line change
@@ -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!"}
```
1 change: 1 addition & 0 deletions snippets/python/[fastapi]/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
93 changes: 74 additions & 19 deletions src/components/LanguageSelector.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(null);
const [isOpen, setIsOpen] = useState(false);
const [openedLanguages, setOpenedLanguages] = useState<LanguageType[]>([]);

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),
});
Expand All @@ -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);
Expand All @@ -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(
Expand Down Expand Up @@ -90,23 +133,35 @@ const LanguageSelector = () => {
onKeyDown={handleKeyDown}
tabIndex={-1}
>
{fetchedLanguages.map((lang, index) => (
<li
key={lang.name}
role="option"
tabIndex={-1}
onClick={() => handleSelect(lang)}
className={`selector__item ${
language.name === lang.name ? "selected" : ""
} ${focusedIndex === index ? "focused" : ""}`}
aria-selected={language.name === lang.name}
>
<label>
<img src={lang.icon} alt="" />
<span>{lang.name}</span>
</label>
</li>
))}
{fetchedLanguages.map((lang, index) =>
lang.subLanguages.length > 0 ? (
<SubLanguageSelector
key={index}
mainLanguage={lang}
afterSelect={() => {
setIsOpen(false);
}}
opened={openedLanguages.includes(lang)}
onDropdownToggle={handleToggleSublanguage}
/>
) : (
<li
key={lang.name}
role="option"
tabIndex={-1}
onClick={() => handleSelect(lang)}
className={`selector__item ${
language.name === lang.name ? "selected" : ""
} ${focusedIndex === index ? "focused" : ""}`}
aria-selected={language.name === lang.name}
>
<label>
<img src={lang.icon} alt="" />
<span>{lang.name}</span>
</label>
</li>
)
)}
</ul>
)}
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/SnippetList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ const SnippetList = () => {
<SnippetModal
snippet={snippet}
handleCloseModal={handleCloseModal}
language={language.name}
language={snippet.extension}
/>
)}
</AnimatePresence>
Expand Down
86 changes: 86 additions & 0 deletions src/components/SubLanguageSelector.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<li
key={mainLanguage.name}
role="option"
tabIndex={-1}
className={`selector__item ${
language.name === mainLanguage.name ? "selected" : ""
}`}
aria-selected={language.name === mainLanguage.name}
onClick={() => setLanguage(mainLanguage)}
>
<label>
<img src={mainLanguage.icon} alt={mainLanguage.name} />
<span>{mainLanguage.name}</span>
<button
className="sublanguage__button"
tabIndex={-1}
aria-expanded={opened}
aria-haspopup="listbox"
onClick={(e) => {
e.stopPropagation();
onDropdownToggle(mainLanguage);
}}
>
<span className="sublanguage__arrow" />
</button>
</label>
</li>

{opened && (
<>
{mainLanguage.subLanguages.map((subLanguage) => (
<li
key={subLanguage.name}
role="option"
tabIndex={-1}
className={`selector__item sublanguage__item ${
language.name === subLanguage.name ? "selected" : ""
}`}
aria-selected={language.name === subLanguage.name}
onClick={() => {
handleSelect({
...subLanguage,
mainLanguage: mainLanguage,
subLanguages: [],
});
}}
>
<label>
<img src={subLanguage.icon} alt={subLanguage.name} />
<span>{subLanguage.name}</span>
</label>
</li>
))}
</>
)}
</>
);
};

export default SubLanguageSelector;
2 changes: 1 addition & 1 deletion src/contexts/AppContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useCategories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useFetch } from "./useFetch";
export const useCategories = () => {
const { language } = useAppContext();
const { data, loading, error } = useFetch<CategoryType[]>(
`/consolidated/${slugify(language.name)}.json`
`/consolidated/${language.mainLanguage ? `${slugify(language.mainLanguage.name)}--${slugify(language.name)}` : slugify(language.name)}.json`
);

const fetchedCategories = useMemo(() => {
Expand Down
26 changes: 25 additions & 1 deletion src/hooks/useKeyboardNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,8 +25,10 @@ type KeyboardEventKeys =
export const useKeyboardNavigation = ({
items,
isOpen,
openedLanguages,
onSelect,
onClose,
toggleDropdown,
}: UseKeyboardNavigationProps) => {
const [focusedIndex, setFocusedIndex] = useState<number>(-1);

Expand All @@ -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":
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useSnippets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useFetch } from "./useFetch";
export const useSnippets = () => {
const { language, category } = useAppContext();
const { data, loading, error } = useFetch<CategoryType[]>(
`/consolidated/${slugify(language.name)}.json`
`/consolidated/${language.mainLanguage ? `${slugify(language.mainLanguage.name)}--${slugify(language.name)}` : slugify(language.name)}.json`
);

const fetchedSnippets = data
Expand Down
Loading
Loading