diff options
Diffstat (limited to 'client/src/components')
-rw-r--r-- | client/src/components/AlertToast/AlertToast.jsx | 24 | ||||
-rw-r--r-- | client/src/components/AlertToast/AlertToast.module.css | 29 | ||||
-rw-r--r-- | client/src/components/CustomSelect/CustomSelect.jsx | 111 | ||||
-rw-r--r-- | client/src/components/CustomSelect/CustomSelect.module.css | 49 | ||||
-rw-r--r-- | client/src/components/Editor/Editor.jsx | 65 | ||||
-rw-r--r-- | client/src/components/Editor/Editor.module.css | 64 | ||||
-rw-r--r-- | client/src/components/Header/Header.jsx | 25 | ||||
-rw-r--r-- | client/src/components/Header/Header.module.css | 34 | ||||
-rw-r--r-- | client/src/components/KeyboardModal/KeyboardModal.jsx | 71 | ||||
-rw-r--r-- | client/src/components/KeyboardModal/KeyboardModal.module.css | 60 | ||||
-rw-r--r-- | client/src/components/Modal/Modal.jsx | 70 | ||||
-rw-r--r-- | client/src/components/Modal/Modal.module.css | 6 |
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>✗</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>✗</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>✗</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); } } - |