aboutsummaryrefslogtreecommitdiff
path: root/client/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/components')
-rw-r--r--client/src/components/AlertToast/AlertToast.jsx24
-rw-r--r--client/src/components/AlertToast/AlertToast.module.css29
-rw-r--r--client/src/components/CustomSelect/CustomSelect.jsx111
-rw-r--r--client/src/components/CustomSelect/CustomSelect.module.css49
-rw-r--r--client/src/components/Editor/Editor.jsx65
-rw-r--r--client/src/components/Editor/Editor.module.css64
-rw-r--r--client/src/components/Header/Header.jsx25
-rw-r--r--client/src/components/Header/Header.module.css34
-rw-r--r--client/src/components/KeyboardModal/KeyboardModal.jsx71
-rw-r--r--client/src/components/KeyboardModal/KeyboardModal.module.css60
-rw-r--r--client/src/components/Modal/Modal.jsx70
-rw-r--r--client/src/components/Modal/Modal.module.css6
12 files changed, 511 insertions, 97 deletions
diff --git a/client/src/components/AlertToast/AlertToast.jsx b/client/src/components/AlertToast/AlertToast.jsx
new file mode 100644
index 0000000..824b9e6
--- /dev/null
+++ b/client/src/components/AlertToast/AlertToast.jsx
@@ -0,0 +1,24 @@
+import React, { useEffect } from "react";
+import styles from "./AlertToast.module.css";
+
+const AlertToast = ({ openAlertToast, setOpenAlertToast }) => {
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setOpenAlertToast(false);
+ }, 3000);
+
+ return () => clearTimeout(timer);
+ }, [openAlertToast, setOpenAlertToast]);
+
+ return (
+ <div
+ className={`${styles.background} ${openAlertToast ? styles.active : ""}`}
+ >
+ <div className={styles.container}>
+ <p className={styles.container__title}>Please enter some text!</p>
+ </div>
+ </div>
+ );
+};
+
+export default AlertToast;
diff --git a/client/src/components/AlertToast/AlertToast.module.css b/client/src/components/AlertToast/AlertToast.module.css
new file mode 100644
index 0000000..c42ef90
--- /dev/null
+++ b/client/src/components/AlertToast/AlertToast.module.css
@@ -0,0 +1,29 @@
+.background {
+ position: fixed;
+ bottom: 40px;
+ left: 40px;
+ z-index: 1000;
+ transition: opacity 0.3s ease;
+ opacity: 0;
+}
+
+.background.active {
+ opacity: 1;
+}
+
+.active {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.container {
+ background-color: var(--color-light);
+ color: var(--color-dark);
+ padding: 10px 20px;
+ border-radius: 5px;
+}
+
+.container__title {
+ margin: 0;
+}
diff --git a/client/src/components/CustomSelect/CustomSelect.jsx b/client/src/components/CustomSelect/CustomSelect.jsx
index 3155700..4a906bd 100644
--- a/client/src/components/CustomSelect/CustomSelect.jsx
+++ b/client/src/components/CustomSelect/CustomSelect.jsx
@@ -1,35 +1,110 @@
import React, { useEffect, useState, useRef } from "react";
+import { SUPPORTED_LANGUAGES } from "../../utils/constants";
+
import styles from "./CustomSelect.module.css";
-const CustomSelect = ({ options, onSelect }) => {
+const CustomSelect = ({ onSelect, textAreaRef, isModalOpen }) => {
const [isOpen, setIsOpen] = useState(false);
+ const [options, setOptions] = useState(SUPPORTED_LANGUAGES);
const [selectedOption, setSelectedOption] = useState(
options.length > 0 ? options[0] : null,
);
+ const [focusedOptionIndex, setFocusedOptionIndex] = useState(0);
const selectRef = useRef(null);
+ const searchRef = useRef(null);
+ const optionsRef = useRef([]);
const toggleDropdown = () => {
setIsOpen(!isOpen);
};
- const handleOptionClick = (option) => {
+ const handleOptionClick = (option, index) => {
setSelectedOption(option);
onSelect(option.value);
+ setFocusedOptionIndex(index);
setIsOpen(false);
+ setOptions(SUPPORTED_LANGUAGES);
+ if (textAreaRef.current) {
+ textAreaRef.current.focus();
+ }
};
const handleClickOutside = (event) => {
if (selectRef.current && !selectRef.current.contains(event.target)) {
setIsOpen(false);
+ setOptions(SUPPORTED_LANGUAGES);
+
+ if (textAreaRef.current) {
+ textAreaRef.current.focus();
+ }
+ }
+ };
+
+ const handleChange = (e) => {
+ const searchVal = e.target.value;
+ const filteredOptions =
+ searchVal.length > 0
+ ? SUPPORTED_LANGUAGES.filter((option) =>
+ option.label.toLowerCase().includes(searchVal.toLowerCase()),
+ )
+ : SUPPORTED_LANGUAGES;
+
+ setOptions(filteredOptions);
+ setFocusedOptionIndex(0);
+ };
+
+ const handleKeyDown = (event) => {
+ if (isOpen) {
+ switch (event.key) {
+ case "ArrowDown":
+ setFocusedOptionIndex((prevIndex) => {
+ const newIndex =
+ prevIndex < options.length - 1 ? prevIndex + 1 : prevIndex;
+ optionsRef.current[newIndex].scrollIntoView({ block: "nearest" });
+ return newIndex;
+ });
+ break;
+ case "ArrowUp":
+ setFocusedOptionIndex((prevIndex) => {
+ const newIndex = prevIndex > 0 ? prevIndex - 1 : prevIndex;
+ optionsRef.current[newIndex].scrollIntoView({ block: "nearest" });
+ return newIndex;
+ });
+ break;
+ case "Enter":
+ event.preventDefault();
+ handleOptionClick(options[focusedOptionIndex], focusedOptionIndex);
+ break;
+ case "Escape":
+ setIsOpen(false);
+ setOptions(SUPPORTED_LANGUAGES);
+ if (textAreaRef.current) {
+ textAreaRef.current.focus();
+ }
+ break;
+ default:
+ break;
+ }
+ } else if (!isModalOpen && event.ctrlKey && event.key === "l") {
+ event.preventDefault();
+ setIsOpen(true);
}
};
useEffect(() => {
document.addEventListener("mousedown", handleClickOutside);
+ document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
+ document.removeEventListener("keydown", handleKeyDown);
};
- }, []);
+ }, [isOpen, options, focusedOptionIndex]);
+
+ useEffect(() => {
+ if (isOpen && searchRef.current) {
+ searchRef.current.focus();
+ }
+ }, [isOpen]);
return (
<div className={styles.select} ref={selectRef}>
@@ -45,16 +120,26 @@ const CustomSelect = ({ options, onSelect }) => {
)}
</div>
{isOpen && (
- <div className={styles.options}>
- {options.map((option) => (
- <div
- key={option.value}
- className={styles.option}
- onClick={() => handleOptionClick(option)}
- >
- {option.label}
- </div>
- ))}
+ <div className={styles.options__container}>
+ <input
+ onChange={handleChange}
+ className={styles.options__search}
+ ref={searchRef}
+ placeholder="Search..."
+ />
+ <div className={styles.options}>
+ {options.map((option, index) => (
+ <div
+ key={option.value}
+ className={`${styles.option} ${index === focusedOptionIndex ? styles.focused : ""}`}
+ onClick={() => handleOptionClick(option, index)}
+ tabIndex={0}
+ ref={(el) => (optionsRef.current[index] = el)}
+ >
+ {option.label}
+ </div>
+ ))}
+ </div>
</div>
)}
</div>
diff --git a/client/src/components/CustomSelect/CustomSelect.module.css b/client/src/components/CustomSelect/CustomSelect.module.css
index 4b5f1a4..f62a77f 100644
--- a/client/src/components/CustomSelect/CustomSelect.module.css
+++ b/client/src/components/CustomSelect/CustomSelect.module.css
@@ -10,39 +10,76 @@
.selected__option {
cursor: pointer;
+ font-weight: 500;
padding: 10px;
border: none;
display: flex;
justify-content: space-between;
}
-.options {
+.options__container {
position: absolute;
+ display: flex;
+ flex-direction: column;
+ padding: 0.5rem 0;
top: 140%;
left: 0;
- height: 400px;
- overflow-y: auto;
width: 100%;
+ height: 400px;
border: none;
border-radius: 10px;
background-color: var(--color-yellow);
z-index: 2;
+ align-items: center;
+}
+
+.options__search {
+ margin-top: 0.5rem;
+ padding: 6px;
+ width: 85%;
+ border: none;
+ border-radius: 6px;
+ background-color: #d79921;
+ color: var(--color-dark);
+ &:focus {
+ outline: none;
+ }
+ &::placeholder {
+ color: #504945;
+ opacity: 1;
+ }
+
+ &::-ms-input-placeholder {
+ color: #504945;
+ }
+}
+
+.options {
+ width: 100%;
+ height: 400px;
+ overflow-y: auto;
+ margin-top: 10px;
}
.option {
padding: 10px;
color: var(--color-dark);
- border-bottom: var(--color-dark) 1px solid;
+ border-bottom: 1px solid var(--color-dark);
cursor: pointer;
- transition: all 0.3s ease;
+ transition: transform 0.3s ease;
&:hover {
- scale: 0.9;
+ transform: scale(0.9);
}
&:last-child {
border-bottom: none;
}
}
+.option.focused {
+ background-color: #d79921;
+ outline: none;
+}
+
@media screen and (max-width: 480px) {
.select {
width: 8rem;
diff --git a/client/src/components/Editor/Editor.jsx b/client/src/components/Editor/Editor.jsx
index dac633c..976c1b2 100644
--- a/client/src/components/Editor/Editor.jsx
+++ b/client/src/components/Editor/Editor.jsx
@@ -5,13 +5,13 @@ import React, {
useCallback,
useMemo,
} from "react";
+import { Link } from "react-router-dom";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import Prism from "prismjs";
import styles from "./Editor.module.css";
import "../prism-themes/prism-gruvbox-dark.css";
import "../prism-themes/prism-line-numbers.css";
import { URL_REGEX } from "../../utils/constants";
-import Header from "../Header/Header";
import {
generateAESKey,
keyToString,
@@ -20,6 +20,9 @@ import {
decryptAES,
} from "../../utils/encryption";
import Modal from "../Modal/Modal";
+import AlertToast from "../AlertToast/AlertToast";
+import CustomSelect from "../CustomSelect/CustomSelect";
+import KeyboardModal from "../KeyboardModal/KeyboardModal";
const Editor = () => {
const { id } = useParams();
@@ -28,6 +31,8 @@ const Editor = () => {
const [text, setText] = useState("");
const [language, setLanguage] = useState("none");
const [openModal, setOpenModal] = useState(false);
+ const [openKeyboardModal, setOpenKeyboardModal] = useState(false);
+ const [openAlertToast, setOpenAlertToast] = useState(false);
const textareaRef = useRef(null);
const lineNumberRef = useRef(null);
const queryParams = useMemo(
@@ -48,7 +53,7 @@ const Editor = () => {
const handleSaveClick = useCallback(async () => {
if (!text) {
- alert("Please enter some text!");
+ setOpenAlertToast(true);
return;
}
if (URL_REGEX.test(text)) {
@@ -189,14 +194,67 @@ const Editor = () => {
});
}, []);
+ useEffect(() => {
+ const handleKeyDown = (event) => {
+ if (!id && event.ctrlKey && event.key.toLowerCase() === "s") {
+ event.preventDefault();
+ handleSaveClick();
+ }
+ };
+
+ document.addEventListener("keydown", handleKeyDown);
+
+ return () => {
+ document.removeEventListener("keydown", handleKeyDown);
+ };
+ }, [handleSaveClick]);
+
return (
<>
- <Header isSelectVisible={!id} onLanguageChange={handleLanguageChange} />
+ {!id && (
+ <>
+ <KeyboardModal
+ textAreaRef={textareaRef}
+ isOpen={openKeyboardModal}
+ setIsOpen={setOpenKeyboardModal}
+ isModalOpen={openModal}
+ />
+ <AlertToast
+ openAlertToast={openAlertToast}
+ setOpenAlertToast={setOpenAlertToast}
+ />
+ </>
+ )}
+ <div className={styles.header}>
+ <Link to="/">
+ <h1>
+ <span className={styles.header__mini}>mini</span>bin
+ </h1>
+ </Link>
+ {!id && (
+ <div className={styles.header__right}>
+ <CustomSelect
+ onSelect={handleLanguageChange}
+ textAreaRef={textareaRef}
+ isModalOpen={openModal}
+ />
+ <button className={styles.btn__help}>
+ <img
+ src="assets/icons/question.png"
+ className={styles.btn__help__icon}
+ alt="Help"
+ onClick={() => setOpenKeyboardModal(true)}
+ />
+ </button>
+ </div>
+ )}
+ </div>
<Modal
openModal={openModal}
setOpenModal={setOpenModal}
onSuccessClick={handleSuccessClick}
onCancelClick={handleCancelClick}
+ textAreaRef={textareaRef}
/>
<div className={styles.container}>
{!id && (
@@ -220,6 +278,7 @@ const Editor = () => {
ref={textareaRef}
placeholder="</> Paste, save, share! (Pasting just a URL will shorten it!)"
value={text}
+ disabled={openModal}
/>
<pre className="line-numbers" ref={lineNumberRef}>
<code
diff --git a/client/src/components/Editor/Editor.module.css b/client/src/components/Editor/Editor.module.css
index 62c6759..761e3ed 100644
--- a/client/src/components/Editor/Editor.module.css
+++ b/client/src/components/Editor/Editor.module.css
@@ -61,6 +61,25 @@
}
}
+.btn__help {
+ height: 2.75rem;
+ width: 2.75rem;
+ background-color: var(--color-yellow);
+ border: none;
+ border-radius: 50%;
+ cursor: pointer;
+ transition: 0.3s;
+ z-index: 1;
+ &:hover {
+ transform: scale(1.1);
+ }
+}
+
+.btn__help__icon {
+ height: 1.75rem;
+ width: 1.75rem;
+}
+
.btn__icon {
height: 3rem;
width: 3rem;
@@ -68,6 +87,47 @@
contrast(88%);
}
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin: 0.5rem 2rem;
+ color: var(--color-light);
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.header__right {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.header h1 {
+ margin: 0;
+}
+
+.header a {
+ text-decoration: none;
+ color: inherit;
+}
+
+.header__mini {
+ color: var(--color-yellow);
+}
+
+@media screen and (max-width: 480px) {
+ .header {
+ margin: 0.5rem 1rem;
+ }
+
+ .header h1 {
+ font-size: 2rem;
+ }
+}
+
@media screen and (max-width: 768px) {
.btn__save {
bottom: 2rem;
@@ -80,4 +140,8 @@
height: 2rem;
width: 2rem;
}
+
+ .btn__help {
+ display: none;
+ }
}
diff --git a/client/src/components/Header/Header.jsx b/client/src/components/Header/Header.jsx
deleted file mode 100644
index 9682104..0000000
--- a/client/src/components/Header/Header.jsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import React from "react";
-import { Link } from "react-router-dom";
-import { SUPPORTED_LANGUAGES } from "../../utils/constants";
-import styles from "./Header.module.css";
-import CustomSelect from "../CustomSelect/CustomSelect";
-
-const Header = ({ isSelectVisible, onLanguageChange }) => {
- return (
- <div className={styles.header}>
- <Link to="/">
- <h1>
- <span className={styles.header__mini}>mini</span>bin
- </h1>
- </Link>
- {isSelectVisible && (
- <CustomSelect
- options={SUPPORTED_LANGUAGES}
- onSelect={onLanguageChange}
- />
- )}
- </div>
- );
-};
-
-export default Header;
diff --git a/client/src/components/Header/Header.module.css b/client/src/components/Header/Header.module.css
deleted file mode 100644
index 9717e14..0000000
--- a/client/src/components/Header/Header.module.css
+++ /dev/null
@@ -1,34 +0,0 @@
-.header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin: 0.5rem 2rem;
- color: var(--color-light);
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-}
-
-.header h1 {
- margin: 0;
-}
-
-.header a {
- text-decoration: none;
- color: inherit;
-}
-
-.header__mini {
- color: var(--color-yellow);
-}
-
-@media screen and (max-width: 480px) {
- .header {
- margin: 0.5rem 1rem;
- }
-
- .header h1 {
- font-size: 2rem;
- }
-}
diff --git a/client/src/components/KeyboardModal/KeyboardModal.jsx b/client/src/components/KeyboardModal/KeyboardModal.jsx
new file mode 100644
index 0000000..6ba8536
--- /dev/null
+++ b/client/src/components/KeyboardModal/KeyboardModal.jsx
@@ -0,0 +1,71 @@
+import React, { useRef, useState, useEffect } from "react";
+import styles from "./KeyboardModal.module.css";
+
+const KeyboardModal = ({ textAreaRef, isOpen, setIsOpen, isModalOpen }) => {
+ const keyboardModalRef = useRef(null);
+
+ const handleClickOutside = (event) => {
+ if (
+ keyboardModalRef.current &&
+ !keyboardModalRef.current.contains(event.target)
+ ) {
+ setIsOpen(false);
+ }
+ };
+
+ const handleKeyDown = (event) => {
+ if (!isModalOpen && event.ctrlKey && event.key === "k") {
+ event.preventDefault();
+ if (textAreaRef.current) {
+ textAreaRef.current.blur();
+ }
+ setIsOpen(true);
+ } else if (isOpen && event.key === "Escape") {
+ setIsOpen(false);
+ if (textAreaRef.current) {
+ textAreaRef.current.focus();
+ }
+ }
+ };
+
+ useEffect(() => {
+ document.addEventListener("mousedown", handleClickOutside);
+ document.addEventListener("keydown", handleKeyDown);
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ document.removeEventListener("keydown", handleKeyDown);
+ };
+ }, [isOpen]);
+
+ return (
+ isOpen && (
+ <div className={`${styles.background} ${styles.active}`}>
+ <div ref={keyboardModalRef} className={styles.container}>
+ <button
+ className={styles.container__close}
+ onClick={() => setIsOpen(false)}
+ >
+ <span>&#10007;</span>
+ </button>
+ <p className={styles.container__title}>Keyboard Shortcuts</p>
+ <div className={styles.container__shortcuts}>
+ <p>
+ <span className={styles.container__shortcut__key}>Ctrl + K</span>
+ Open Keyboard Shortcuts
+ </p>
+ <p>
+ <span className={styles.container__shortcut__key}>Ctrl + L</span>
+ Open Language Selector
+ </p>
+ <p>
+ <span className={styles.container__shortcut__key}>Ctrl + S</span>
+ Save Bin
+ </p>
+ </div>
+ </div>
+ </div>
+ )
+ );
+};
+
+export default KeyboardModal;
diff --git a/client/src/components/KeyboardModal/KeyboardModal.module.css b/client/src/components/KeyboardModal/KeyboardModal.module.css
new file mode 100644
index 0000000..9daecca
--- /dev/null
+++ b/client/src/components/KeyboardModal/KeyboardModal.module.css
@@ -0,0 +1,60 @@
+.background {
+ display: none;
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+ background-color: #00000062;
+ z-index: 10;
+}
+
+.active {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.container {
+ top: 0;
+ left: 0;
+ background-color: var(--color-dark);
+ padding: 30px;
+ border-radius: 10px;
+ z-index: 11;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ color: var(--color-light);
+}
+
+.container__title {
+ margin: 0;
+ font-size: 1.5rem;
+}
+
+.container__shortcuts {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ margin-top: 10px;
+}
+
+.container__shortcut__key {
+ color: var(--color-yellow);
+ margin-right: 15px;
+}
+
+.container__close {
+ cursor: pointer;
+ background: none;
+ border: none;
+ margin-left: auto;
+ margin-bottom: 10px;
+ color: inherit;
+ font-size: 2rem;
+ &:hover {
+ transform: scale(0.9);
+ }
+}
diff --git a/client/src/components/Modal/Modal.jsx b/client/src/components/Modal/Modal.jsx
index bf0540b..4c68f53 100644
--- a/client/src/components/Modal/Modal.jsx
+++ b/client/src/components/Modal/Modal.jsx
@@ -1,39 +1,79 @@
import React, { useRef, useEffect } from "react";
import styles from "./Modal.module.css";
-const Modal = ({ openModal, setOpenModal, onSuccessClick, onCancelClick }) => {
+const Modal = ({
+ openModal,
+ setOpenModal,
+ onSuccessClick,
+ onCancelClick,
+ textAreaRef,
+}) => {
const modalRef = useRef(null);
useEffect(() => {
const handleClickOutside = (event) => {
if (modalRef.current && !modalRef.current.contains(event.target)) {
setOpenModal(false);
+ if (textAreaRef.current) {
+ setTimeout(() => {
+ textAreaRef.current.focus();
+ }, 0);
+ }
+ }
+ };
+
+ const handleKeyDown = (event) => {
+ if (event.key === "Escape") {
+ setOpenModal(false);
+ if (textAreaRef.current) {
+ setTimeout(() => {
+ textAreaRef.current.focus();
+ }, 0);
+ }
+ } else if (
+ (event.key.toLowerCase() === "y" ||
+ event.key.toLowerCase() === "enter") &&
+ openModal
+ ) {
+ onSuccessClick();
+ event.preventDefault();
+ setOpenModal(false);
+ } else if (event.key.toLowerCase() === "n" && openModal) {
+ onCancelClick();
+ setOpenModal(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
+ document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
+ document.removeEventListener("keydown", handleKeyDown);
};
- }, [setOpenModal]);
+ }, [openModal]);
return (
- <div className={`${styles.background} ${openModal && styles.active}`}>
- <div ref={modalRef} className={styles.container}>
- <button
- className={styles.container__close}
- onClick={() => setOpenModal(false)}
- >
- <span>&#10007;</span>
- </button>
- <p className={styles.container__title}>Encrypt content?</p>
- <div className={styles.container__actions}>
- <button onClick={onSuccessClick}>Yes</button>
- <button onClick={onCancelClick}>No</button>
+ openModal && (
+ <div className={`${styles.background} ${openModal && styles.active}`}>
+ <div ref={modalRef} className={styles.container}>
+ <button
+ className={styles.container__close}
+ onClick={() => setOpenModal(false)}
+ >
+ <span>&#10007;</span>
+ </button>
+ <p className={styles.container__title}>
+ Encrypt content?{" "}
+ <span className={styles.container__title__span}>[Y/n]</span>
+ </p>
+ <div className={styles.container__actions}>
+ <button onClick={onSuccessClick}>Yes</button>
+ <button onClick={onCancelClick}>No</button>
+ </div>
</div>
</div>
- </div>
+ )
);
};
diff --git a/client/src/components/Modal/Modal.module.css b/client/src/components/Modal/Modal.module.css
index ea4978b..3164a0f 100644
--- a/client/src/components/Modal/Modal.module.css
+++ b/client/src/components/Modal/Modal.module.css
@@ -42,6 +42,11 @@
}
}
+.container__title__span {
+ font-family: "JetBrains Mono", monospace;
+ color: var(--color-yellow);
+}
+
.container__actions {
margin-top: 20px;
display: flex;
@@ -65,4 +70,3 @@
transform: scale(1.1);
}
}
-