diff --git a/src/components/Common/InputEditor/InputEditor.jsx b/src/components/Common/InputEditor/InputEditor.jsx index f2adcc50..030245bc 100644 --- a/src/components/Common/InputEditor/InputEditor.jsx +++ b/src/components/Common/InputEditor/InputEditor.jsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react"; -import { InputEditorContainer, InputEditorTheInput, InputEditorLabel, InputEditorPreview } from "./InputEditorElements"; +import { InputEditorContainer, InputEditorTheInput, InputEditorLabel } from "./InputEditorElements"; const InputEditor = ({ content, label, onCopyChanges }) => { const [value, setValue] = useState(""); @@ -16,7 +16,6 @@ const InputEditor = ({ content, label, onCopyChanges }) => { return ( {label} - {value} ); diff --git a/src/components/Common/InputEditor/InputEditorElements.jsx b/src/components/Common/InputEditor/InputEditorElements.jsx index 5a353c02..29e16049 100644 --- a/src/components/Common/InputEditor/InputEditorElements.jsx +++ b/src/components/Common/InputEditor/InputEditorElements.jsx @@ -5,29 +5,25 @@ export const InputEditorContainer = styled.div` border: 2px solid #4f4f4f; border-radius: 8px; box-shadow: 2px 2px #4f4f4f; + width: 100%; `; export const InputEditorTheInput = styled.input` - background-color: #0d1117; padding: 10px; - color: #b5b5b5; + color: white; width: 100%; border-radius: 8px; border: 1px solid #333342; outline: none; - font-size: 18px; - line-height: 24px; + line-height: 1; text-transform: capitalize; - margin-top: 20px; + font-size: 2em; + background-color: #0d1117; + text-transform: capitalize; + font-weight: 600; + font-family: Poppins, sans-serif; `; export const InputEditorLabel = styled.h2` - text-transform: uppercase; + text-transform: capitalize; text-align: center; color: #4f4f4f; - text-decoration-line: underline; -`; - -export const InputEditorPreview = styled.h1` - text-transform: capitalize; - background-color: #0d1117; - padding: 0 5px; `; diff --git a/src/components/Common/MarkdownEditor/CheckBoxClickable.jsx b/src/components/Common/MarkdownEditor/CheckBoxClickable.jsx new file mode 100644 index 00000000..dda081a3 --- /dev/null +++ b/src/components/Common/MarkdownEditor/CheckBoxClickable.jsx @@ -0,0 +1,45 @@ +import React from "react"; + +const compareStrings = (str1, str2) => { + for (let i = 0, j = 0; i < str1.length || j < str2.length; i++, j++) { + if (i >= str1.length) return false; + if (j >= str2.length) return false; + if (str1[i] !== str2[j]) return false; + } + return true; +}; +const CheckBoxClickable = ({ value, onChangeValue, disabled, ...props }) => { + if (disabled) return ; + + const handleCheckBoxChange = (e) => { + const textOfCheckBox = e.target.parentNode.textContent; + const valueListOfLines = value.split("\n"); + const findCheckedBoxLineIndex = valueListOfLines.findIndex((item) => + compareStrings(item?.replace(/- \[ \]|- \[[^]]+/, ""), textOfCheckBox.split("\n")[0]), + ); + valueListOfLines[findCheckedBoxLineIndex] = valueListOfLines[findCheckedBoxLineIndex].replace( + /- \[ \]|- \[[^]]+/, + (match) => (match === "- [ ]" ? "- [X]" : "- [ ]"), + ); + onChangeValue(valueListOfLines.join("\n")); + }; + + return ( + { + if (props.type === "checkbox") { + const isChecked = e.target.hasAttribute("checked"); + handleCheckBoxChange(e); + if (isChecked) { + e.target.removeAttribute("checked"); + } else { + e.target.setAttribute("checked", "checked"); + } + } + }} + /> + ); +}; +export default CheckBoxClickable; diff --git a/src/components/Common/MarkdownEditor/MarkdownEditor.css b/src/components/Common/MarkdownEditor/MarkdownEditor.css index 0d92b793..b73d2a96 100644 --- a/src/components/Common/MarkdownEditor/MarkdownEditor.css +++ b/src/components/Common/MarkdownEditor/MarkdownEditor.css @@ -3,3 +3,10 @@ font-size: 18px !important; line-height: 24px !important; } +.image { + max-height: 500px; +} +.preview { + max-height: calc(100vh - 550px - 3rem); + overflow-y: auto; +} diff --git a/src/components/Common/MarkdownEditor/MarkdownEditor.jsx b/src/components/Common/MarkdownEditor/MarkdownEditor.jsx index 23132445..219f8e1f 100644 --- a/src/components/Common/MarkdownEditor/MarkdownEditor.jsx +++ b/src/components/Common/MarkdownEditor/MarkdownEditor.jsx @@ -8,17 +8,19 @@ import { } from "./MarkdownEditorElements"; import rehypeSanitize from "rehype-sanitize"; import "./MarkdownEditor.css"; +import CheckBoxClickable from "./CheckBoxClickable"; +import useImageUploadEvents from "./useImageUploadEvents"; -const compareStrings = (str1, str2) => { - for (let i = 0, j = 0; i < str1.length || j < str2.length; i++, j++) { - if (i >= str1.length) return false; - if (j >= str2.length) return false; - if (str1[i] !== str2[j]) return false; - } - return true; -}; -const MarkdownEditor = ({ content, label, previewModeOnly, onCopyChanges }) => { +const MarkdownEditor = ({ content, label, previewModeOnly, onCopyChanges, pageName }) => { const [value, setValue] = useState(""); + + const handleChange = (value) => { + setValue(value); + onCopyChanges(label, value); + }; + + const { onPasteImage, onDragOverImage, onDropImage } = useImageUploadEvents(value, handleChange, pageName); + useEffect(() => { setValue(content); }, [content, label]); @@ -30,29 +32,16 @@ const MarkdownEditor = ({ content, label, previewModeOnly, onCopyChanges }) => { style={{ whiteSpace: "normal", backgroundColor: "#000" }} components={{ input: (props) => { - return ; + return ; + }, + img: (props) => { + return ; }, }} /> ); } - const handleChange = (value) => { - setValue(value); - onCopyChanges(label, value); - }; - const handleCheckBoxChange = (e) => { - const textOfCheckBox = e.target.parentNode.textContent; - const valueListOfLines = value.split("\n"); - const findCheckedBoxLineIndex = valueListOfLines.findIndex((item) => - compareStrings(item?.replace(/- \[ \]|- \[[^]]+/, ""), textOfCheckBox.split("\n")[0]), - ); - valueListOfLines[findCheckedBoxLineIndex] = valueListOfLines[findCheckedBoxLineIndex].replace( - /- \[ \]|- \[[^]]+/, - (match) => (match === "- [ ]" ? "- [X]" : "- [ ]"), - ); - handleChange(valueListOfLines.join("\n")); - }; return ( {label} @@ -64,26 +53,21 @@ const MarkdownEditor = ({ content, label, previewModeOnly, onCopyChanges }) => { paddingLeft: "5px", paddingRight: "5px", }} + className="preview" components={{ input: (props) => { return ( - { - if (props.type === "checkbox") { - const isChecked = e.target.hasAttribute("checked"); - handleCheckBoxChange(e); - if (isChecked) { - e.target.removeAttribute("checked"); - } else { - e.target.setAttribute("checked", "checked"); - } - } - }} + value={value} + onChangeValue={handleChange} /> ); }, + img: (props) => { + return ; + }, }} /> @@ -96,6 +80,9 @@ const MarkdownEditor = ({ content, label, previewModeOnly, onCopyChanges }) => { }} preview="edit" visibleDragbar={false} + onDrop={onDropImage} + onDragOver={onDragOverImage} + onPaste={onPasteImage} /> diff --git a/src/components/Common/MarkdownEditor/MarkdownEditorElements.jsx b/src/components/Common/MarkdownEditor/MarkdownEditorElements.jsx index af71191b..1395bdd8 100644 --- a/src/components/Common/MarkdownEditor/MarkdownEditorElements.jsx +++ b/src/components/Common/MarkdownEditor/MarkdownEditorElements.jsx @@ -17,8 +17,7 @@ export const MarkdownEditorPreviewContainer = styled.div` export const MarkdownEditorContainer = styled.div``; export const MarkdownLabel = styled.h2` - text-transform: uppercase; + text-transform: capitalize; text-align: center; color: #4f4f4f; - text-decoration-line: underline; `; diff --git a/src/components/Common/MarkdownEditor/useImageUploadEvents.jsx b/src/components/Common/MarkdownEditor/useImageUploadEvents.jsx new file mode 100644 index 00000000..fef60410 --- /dev/null +++ b/src/components/Common/MarkdownEditor/useImageUploadEvents.jsx @@ -0,0 +1,99 @@ +import { useState } from "react"; +import axios from "axios"; +import { cdnContentImagesUrl, getApiUrl } from "../../../features/apiUrl"; +import { toast } from "react-toastify"; + +const useImageUploadEvents = (prevContent, setContent, pageName) => { + const [errorMessage, setErrorMessage] = useState(""); + + const handleUploadAndDisplayImage = async (file) => { + const fileName = `${pageName}-${Date.now()}.${file && file.type.split("/")[1]}`; + const reader = new FileReader(); + reader.onloadend = async () => { + const newFile = new File([reader.result], fileName, { type: file && file.type }); + const formData = new FormData(); + formData.append("image", newFile); + console.log(newFile); + const API_URL = getApiUrl("api/upload"); + await axios.post(API_URL, formData); + const newImageUrl = cdnContentImagesUrl(`/${pageName}/${fileName.split("-")[1]}`); + setContent(prevContent + `\n![PLEASE_ADD_A_NAME_FOR_THIS_IMAGE_HERE](${newImageUrl})`); + }; + reader.readAsArrayBuffer(file); + }; + + const handleDrop = async (e) => { + e.preventDefault(); + const file = e.dataTransfer.files[0]; + if (!file) return; + if (!file.type.startsWith("image/")) { + toast.error("Invalid file type. Only images are allowed."); + return; + } + const allowedTypes = ["image/png", "image/jpeg", "image/jpg"]; + + if (!allowedTypes.includes(file.type)) { + toast.error("Invalid file type. Only png and jpg are allowed."); + return; + } + const maxFileSize = 1000000; // 1000KB + if (file.size > maxFileSize) { + toast.error(`File size should be less than ${maxFileSize / 1000}KB.`); + return; + } + try { + handleUploadAndDisplayImage(file); + } catch (err) { + if (err.message === "Request failed with status code 429") { + setErrorMessage("You are uploading images too fast. Please wait a few seconds and try again."); + errorMessage && toast.error(errorMessage); + if (errorMessage === "") { + toast("You are uploading images too fast. Please wait a few seconds and try again."); + } + } + } + }; + const handlePaste = async (e) => { + const items = (e.clipboardData || e.originalEvent.clipboardData).items; + let file = null; + + // Check if the paste event contains an image + for (const item of items) { + if (item.type.startsWith("image")) { + file = item.getAsFile(); + break; + } + } + if (!file) return; + if (!file.type.startsWith("image/")) { + toast.error("Invalid file type. Only images are allowed."); + return; + } + if (file.type !== ("image/png" || "image/jpeg" || "image/jpg")) { + toast.error("Invalid file type. Only png and jpg are allowed."); + return; + } + const maxFileSize = 1000000; // 1000KB + if (file.size > maxFileSize) { + toast.error(`File size should be less than ${maxFileSize / 1000}KB.`); + return; + } + try { + handleUploadAndDisplayImage(file); + } catch (err) { + if (err.message === "Request failed with status code 429") { + setErrorMessage("You are uploading images too fast. Please wait a few seconds and try again."); + errorMessage && toast.error(errorMessage); + if (errorMessage === "") { + toast("You are uploading images too fast. Please wait a few seconds and try again."); + } + } + } + }; + const handleDragOver = (e) => { + e.preventDefault(); + }; + + return { onDropImage: handleDrop, onDragOverImage: handleDragOver, onPasteImage: handlePaste }; +}; +export default useImageUploadEvents; diff --git a/src/components/Dashboard/Notetaker/NoteApp.jsx b/src/components/Dashboard/Notetaker/NoteApp.jsx index 2bb38679..3e6925e8 100644 --- a/src/components/Dashboard/Notetaker/NoteApp.jsx +++ b/src/components/Dashboard/Notetaker/NoteApp.jsx @@ -48,7 +48,13 @@ const NoteApp = () => { const handlePickNote = (noteId) => { const pickedNote = notes.find((note) => note._id === noteId); setNeedToAdd(false); - setPickedNote(pickedNote !== -1 ? pickedNote : {}); + setPickedNote( + pickedNote === -1 + ? {} + : pickedNote.title.includes("UntitledNote") + ? { ...pickedNote, title: "" } + : pickedNote, + ); }; const handlePinNote = (noteId) => { const pinnedNote = notes.find((note) => note._id === noteId); diff --git a/src/components/Dashboard/Notetaker/NoteDescription.jsx b/src/components/Dashboard/Notetaker/NoteDescription.jsx index 216e4f80..7056eece 100644 --- a/src/components/Dashboard/Notetaker/NoteDescription.jsx +++ b/src/components/Dashboard/Notetaker/NoteDescription.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { DescriptionContent, DescriptionDisplayTitle, @@ -39,7 +39,7 @@ const NoteDescription = ({ children, onPin, needToAdd, onCloseAddMode, onChangeP onCloseAddMode(false); setShowNote({}); }; - const handleCopyNoteData = (label, content) => { + const handleCopyNoteData = useCallback((label, content) => { setShowNote((prevCopyNote) => { if (label === "description") label = "content"; return { @@ -47,7 +47,7 @@ const NoteDescription = ({ children, onPin, needToAdd, onCloseAddMode, onChangeP [label]: content, }; }); - }; + }); const handleSaveNote = (newNote) => { if (!newNote.title && !newNote.content) { dispatch(deleteNote(newNote._id)); @@ -102,7 +102,7 @@ const NoteDescription = ({ children, onPin, needToAdd, onCloseAddMode, onChangeP /> ) : ( - {showNote.title || (showNote._id ? `UntitledNote #${showNote._id.substr(-10)}` : "")} + {showNote.title || (showNote._id ? `Untitled Note` : "")} )} @@ -112,12 +112,10 @@ const NoteDescription = ({ children, onPin, needToAdd, onCloseAddMode, onChangeP content={needToEdit && showNote.content ? showNote.content : ""} label="description" onCopyChanges={handleCopyNoteData} + pageName="notes" /> ) : ( - + )} diff --git a/src/components/Dashboard/Notetaker/NoteElements.jsx b/src/components/Dashboard/Notetaker/NoteElements.jsx index 2eb6ef27..6fe912c8 100644 --- a/src/components/Dashboard/Notetaker/NoteElements.jsx +++ b/src/components/Dashboard/Notetaker/NoteElements.jsx @@ -95,6 +95,10 @@ export const NoteItemShortTitle = styled.h4` export const NoteItemShortDescription = styled.p` font-family: "Roboto", sans-serif; font-weight: 100; + width: 100%; + overflow-wrap: break-word; + font-style: ${(props) => (props.empty ? "italic" : "")}; + opacity: ${(props) => (props.empty ? "0.7" : "")}; `; export const NotesDescriptionContainer = styled.div` @@ -125,17 +129,23 @@ export const NotesDescription = styled.div` top: 3rem; left: 0; width: 100%; + max-height: calc(100vh - 3rem); display: flex; flex-direction: column; padding: 0 10px; color: #f5f5f5; padding: 20px; + overflow-y: auto; `; -export const DescriptionTitle = styled.div``; +export const DescriptionTitle = styled.div` + width: 100%; + overflow-wrap: break-word; +`; export const DescriptionContent = styled.div``; export const DescriptionDisplayTitle = styled.h1` text-transform: capitalize; + width: 100%; `; diff --git a/src/components/Dashboard/Notetaker/NoteItem.jsx b/src/components/Dashboard/Notetaker/NoteItem.jsx index 97b40226..838cd82b 100644 --- a/src/components/Dashboard/Notetaker/NoteItem.jsx +++ b/src/components/Dashboard/Notetaker/NoteItem.jsx @@ -8,9 +8,11 @@ import { } from "./NoteElements"; import NotePinning from "./NotePinning"; +const cleanFromTags = (text) => { + return text?.replace(/<[^>]+>|-|\[[^]]+|#/g, "").replace(/!\[(.*?)\]\([^)]*\)/g, "$1", ""); +}; const shortText = (text, letters) => { - const textCleanFromTags = text?.replace(/<[^>]+>|-|\[[^]]+|#/g, ""); - return textCleanFromTags?.length > letters ? `${textCleanFromTags.slice(0, letters)}...` : textCleanFromTags; + return text?.length > letters ? `${text.slice(0, letters)}...` : text; }; const NoteItem = ({ _id, title, content, pinned, onPick, onPin }) => { @@ -18,15 +20,19 @@ const NoteItem = ({ _id, title, content, pinned, onPick, onPin }) => { const [shortDescr, setShortDescr] = useState(""); useEffect(() => { - setShortTitle(() => (title ? shortText(title, 30) : `UntitledNote #${_id.substr(-10)}`)); - setShortDescr(() => (content ? shortText(content, 60) : "undescribedNote")); + setShortTitle(() => title && shortText(title, 30)); + setShortDescr(() => { + if (!content) return "(Empty)"; + const cleanContent = cleanFromTags(content); + return shortText(cleanContent, 60); + }); }, [title, content]); return ( onPick(_id)}> {shortTitle} - {shortDescr} + {shortDescr} diff --git a/src/features/notes/notesSlice.js b/src/features/notes/notesSlice.js index 8df3237d..41450091 100644 --- a/src/features/notes/notesSlice.js +++ b/src/features/notes/notesSlice.js @@ -7,6 +7,7 @@ const initialState = { isNoteSuccess: false, isNoteLoading: false, noteMessage: "", + countUntitled: 0, }; // Create new note @@ -84,7 +85,11 @@ export const noteSlice = createSlice({ state.isNoteSuccess = true; state.isNoteLoading = false; state.isNoteError = false; - state.notes.push(action.payload); + state.notes.push( + action.payload.title + ? action.payload + : { ...action.payload, title: `UntitledNote #${++state.countUntitled}` }, + ); }) .addCase(createNote.rejected, (state, action) => { state.isNoteLoading = false; @@ -99,7 +104,15 @@ export const noteSlice = createSlice({ state.isNoteLoading = false; state.isNoteSuccess = true; state.notes = state.notes.map((note) => - note._id === action.payload._id ? { ...note, ...action.payload } : note, + note._id === action.payload._id + ? { + ...note, + ...action.payload, + title: action.payload.title + ? action.payload.title + : `UntitledNote #${++state.countUntitled}`, + } + : note, ); }) .addCase(updateNote.rejected, (state, action) => { @@ -134,7 +147,10 @@ export const noteSlice = createSlice({ .addCase(getNotes.fulfilled, (state, action) => { state.isNoteLoading = false; state.isNoteSuccess = true; - state.notes = action.payload; + state.notes = action.payload.map((item) => { + if (!item.title) return { ...item, title: `UntitledNote #${++state.countUntitled}` }; + return item; + }); }) .addCase(getNotes.rejected, (state, action) => { state.isNoteLoading = false; diff --git a/src/pages/ForgotPassword.jsx b/src/pages/ForgotPassword.jsx index a0805c24..4a4a3a26 100644 --- a/src/pages/ForgotPassword.jsx +++ b/src/pages/ForgotPassword.jsx @@ -152,6 +152,7 @@ const SendEmail = ({ email, onChange, onSubmitSendEmail, isLoading }) => ( placeholder="email" onChange={onChange} aria-label="email" + autoComplete="off" /> @@ -183,6 +184,7 @@ const VerifyCode = ({ code, onChange, onSubmitVerifyCode, isUserLoading }) => ( placeholder={"Code"} onChange={onChange} aria-label={"Code"} + autoComplete="off" /> {/* Resend */} @@ -216,7 +218,7 @@ const ResetPassword = ({ password, confirmPassword, onChange, onSubmitPassword, placeholder="Password" onChange={onChange} aria-label="Password" - autoComplete={null} + autoComplete="off" /> @@ -231,7 +233,7 @@ const ResetPassword = ({ password, confirmPassword, onChange, onSubmitPassword, placeholder="Confirm Password" onChange={onChange} aria-label="Password" - autoComplete={null} + autoComplete="off" /> {/* Resend */} diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx index 6bac030d..b53c655e 100644 --- a/src/pages/Login.jsx +++ b/src/pages/Login.jsx @@ -91,7 +91,7 @@ const Login = () => { placeholder="Username" onChange={onChange} aria-label="Username" - autoComplete={false} + autoComplete="off" /> @@ -106,7 +106,7 @@ const Login = () => { placeholder="Password" onChange={onChange} aria-label="Password" - autoComplete={false} + autoComplete="off" /> diff --git a/src/pages/Register.jsx b/src/pages/Register.jsx index 3d37b2dd..73513fd4 100644 --- a/src/pages/Register.jsx +++ b/src/pages/Register.jsx @@ -187,6 +187,7 @@ const RegisterEmail = ({ placeholder={"Email"} onChange={onChange} aria-label={"Email"} + autoComplete="off" />
@@ -198,6 +199,7 @@ const RegisterEmail = ({ name={"termsAndConditions"} onChange={(e) => setFormData({ ...formData, termsAndConditions: e.target.checked })} value={termsAndConditions} + autoComplete="off" />
I agree to all statements included in @@ -236,6 +238,7 @@ const VerifyCode = ({ code, onChange, onSubmitVerifyCode, isUserLoading }) => ( placeholder={"Code"} onChange={onChange} aria-label={"Code"} + autoComplete="off" /> {!isUserLoading ? ( @@ -265,6 +268,7 @@ const AddUserData = ({ name, username, password, password2, onChange, onSubmitUs placeholder="Full Name" onChange={onChange} aria-label="name" + autoComplete="off" /> @@ -279,6 +283,7 @@ const AddUserData = ({ name, username, password, password2, onChange, onSubmitUs placeholder="Username" onChange={onChange} aria-label="Username" + autoComplete="off" /> @@ -293,6 +298,7 @@ const AddUserData = ({ name, username, password, password2, onChange, onSubmitUs placeholder={"Password"} onChange={onChange} aria-label={"Password"} + autoComplete="off" /> @@ -307,6 +313,7 @@ const AddUserData = ({ name, username, password, password2, onChange, onSubmitUs placeholder={"Confirm Password"} onChange={onChange} aria-label={"Confirm Password"} + autoComplete="off" /> diff --git a/src/pages/ResetPassword.jsx b/src/pages/ResetPassword.jsx index 4e4e4a1c..ab145373 100644 --- a/src/pages/ResetPassword.jsx +++ b/src/pages/ResetPassword.jsx @@ -102,7 +102,7 @@ const ResetPassword = () => { placeholder="Password" onChange={onChange} aria-label="Password" - autoComplete={null} + autoComplete="off" /> @@ -117,7 +117,7 @@ const ResetPassword = () => { placeholder="Confirm Password" onChange={onChange} aria-label="Password" - autoComplete={null} + autoComplete="off" />