feat: added keyboard accessibility
This commit is contained in:
parent
189ef9ef81
commit
731453588b
17 changed files with 598 additions and 152 deletions
BIN
client/public/assets/fonts/Poppins_latin-ext_500.woff2
Normal file
BIN
client/public/assets/fonts/Poppins_latin-ext_500.woff2
Normal file
Binary file not shown.
BIN
client/public/assets/fonts/Poppins_latin_500.woff2
Normal file
BIN
client/public/assets/fonts/Poppins_latin_500.woff2
Normal file
Binary file not shown.
BIN
client/public/assets/icons/question.png
Normal file
BIN
client/public/assets/icons/question.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1 KiB |
24
client/src/components/AlertToast/AlertToast.jsx
Normal file
24
client/src/components/AlertToast/AlertToast.jsx
Normal file
|
@ -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;
|
29
client/src/components/AlertToast/AlertToast.module.css
Normal file
29
client/src/components/AlertToast/AlertToast.module.css
Normal file
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
71
client/src/components/KeyboardModal/KeyboardModal.jsx
Normal file
71
client/src/components/KeyboardModal/KeyboardModal.jsx
Normal file
|
@ -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;
|
60
client/src/components/KeyboardModal/KeyboardModal.module.css
Normal file
60
client/src/components/KeyboardModal/KeyboardModal.module.css
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,54 +1,87 @@
|
|||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 100 800;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/JetBrains_Mono_latin-ext.woff2) format('woff2');
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 100 800;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/JetBrains_Mono_latin.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/Poppins_latin-ext_400.woff2) format('woff2');
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/Poppins_latin_400.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/Poppins_latin-ext_700.woff2) format('woff2');
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/Poppins_latin_700.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
font-style: normal;
|
||||
font-weight: 100 800;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/JetBrains_Mono_latin-ext.woff2) format("woff2");
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
|
||||
U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
font-style: normal;
|
||||
font-weight: 100 800;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/JetBrains_Mono_latin.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: "Poppins";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/Poppins_latin-ext_400.woff2) format("woff2");
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
|
||||
U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "Poppins";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/Poppins_latin_400.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: "Poppins";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/Poppins_latin-ext_500.woff2) format("woff2");
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
|
||||
U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "Poppins";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/Poppins_latin_500.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: "Poppins";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/Poppins_latin-ext_700.woff2) format("woff2");
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
|
||||
U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "Poppins";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/Poppins_latin_700.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { Routes, Route } from "react-router-dom";
|
||||
import styles from "./Home.module.css";
|
||||
import Header from "../../components/Header/Header";
|
||||
import Editor from "../../components/Editor/Editor";
|
||||
|
||||
const Home = () => {
|
||||
|
|
Loading…
Reference in a new issue