Skip to content

Commit

Permalink
feat: notes | Func of Paste, Drop Image In editor (#541)
Browse files Browse the repository at this point in the history
* [Feat] Create new CheckBoxClickable Component

why: make the MarkdownEditor Component cleaner
how: move relevant code to CheckBoxClickable Component file

* [Feat] Add Func of Paste, Drop Image In editor

why: so user could paste and drop images in md editor
how: integrate the code from CreateBlog

* [Feat] Manage the Image Path to be Correct

why: so we could also display the Image
how: as was done in Feeds page

* [Bug] Not Related, Fix AutoComplete Warning In Login Page

why: it appers non stop
how: set "off" instead of false or null

* [Feat] Update The Description Component State When Add Images

why: so when the user save the note the images will be added to Description context
how: pass the handleChange to useImageUploadEvents and update it

* [Feat] Some Tweeks

What: 1. change "undescribedNote" to "(Empty)"
2. remove id from "untitledNote" to "untitledNote #number"
3. remove all the address of the image and leave only the name of image
how: 1. change when needed
2. make count in redux
3. with regexp
why: 1. looks better
2. to not show the id of note to user
3. looks better

* [Feat] Some Style Tweaks

what: 1. change the title editor , dont need to preview what we input
2. make description and description preview in edit mode scrollable
3. handle text overflow when needed
why: better ux
how: 1. change styles
2. overflow-y:auto and limit width
3. overflow-wrap:break-word;
  • Loading branch information
ArkadiK94 authored Dec 16, 2023
1 parent 4b0105c commit 0f4056e
Show file tree
Hide file tree
Showing 16 changed files with 256 additions and 79 deletions.
3 changes: 1 addition & 2 deletions src/components/Common/InputEditor/InputEditor.jsx
Original file line number Diff line number Diff line change
@@ -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("");
Expand All @@ -16,7 +16,6 @@ const InputEditor = ({ content, label, onCopyChanges }) => {
return (
<InputEditorContainer>
<InputEditorLabel>{label}</InputEditorLabel>
<InputEditorPreview>{value}</InputEditorPreview>
<InputEditorTheInput type="text" onChange={handleChange} value={value} />
</InputEditorContainer>
);
Expand Down
22 changes: 9 additions & 13 deletions src/components/Common/InputEditor/InputEditorElements.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
`;
45 changes: 45 additions & 0 deletions src/components/Common/MarkdownEditor/CheckBoxClickable.jsx
Original file line number Diff line number Diff line change
@@ -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 <input {...props} disabled={true} />;

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 (
<input
{...props}
disabled={false}
onChange={(e) => {
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;
7 changes: 7 additions & 0 deletions src/components/Common/MarkdownEditor/MarkdownEditor.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
63 changes: 25 additions & 38 deletions src/components/Common/MarkdownEditor/MarkdownEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand All @@ -30,29 +32,16 @@ const MarkdownEditor = ({ content, label, previewModeOnly, onCopyChanges }) => {
style={{ whiteSpace: "normal", backgroundColor: "#000" }}
components={{
input: (props) => {
return <input {...props} disabled={true} />;
return <CheckBoxClickable disabled={true} {...props} />;
},
img: (props) => {
return <img {...props} className="image" />;
},
}}
/>
);
}
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 (
<MarkdownContainer>
<MarkdownLabel>{label}</MarkdownLabel>
Expand All @@ -64,26 +53,21 @@ const MarkdownEditor = ({ content, label, previewModeOnly, onCopyChanges }) => {
paddingLeft: "5px",
paddingRight: "5px",
}}
className="preview"
components={{
input: (props) => {
return (
<input
<CheckBoxClickable
{...props}
disabled={false}
onChange={(e) => {
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 <img {...props} className="image" />;
},
}}
/>
</MarkdownEditorPreviewContainer>
Expand All @@ -96,6 +80,9 @@ const MarkdownEditor = ({ content, label, previewModeOnly, onCopyChanges }) => {
}}
preview="edit"
visibleDragbar={false}
onDrop={onDropImage}
onDragOver={onDragOverImage}
onPaste={onPasteImage}
/>
</MarkdownEditorContainer>
</MarkdownContainer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
`;
99 changes: 99 additions & 0 deletions src/components/Common/MarkdownEditor/useImageUploadEvents.jsx
Original file line number Diff line number Diff line change
@@ -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;
8 changes: 7 additions & 1 deletion src/components/Dashboard/Notetaker/NoteApp.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
14 changes: 6 additions & 8 deletions src/components/Dashboard/Notetaker/NoteDescription.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import {
DescriptionContent,
DescriptionDisplayTitle,
Expand Down Expand Up @@ -39,15 +39,15 @@ 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 {
...prevCopyNote,
[label]: content,
};
});
};
});
const handleSaveNote = (newNote) => {
if (!newNote.title && !newNote.content) {
dispatch(deleteNote(newNote._id));
Expand Down Expand Up @@ -102,7 +102,7 @@ const NoteDescription = ({ children, onPin, needToAdd, onCloseAddMode, onChangeP
/>
) : (
<DescriptionDisplayTitle>
{showNote.title || (showNote._id ? `UntitledNote #${showNote._id.substr(-10)}` : "")}
{showNote.title || (showNote._id ? `Untitled Note` : "")}
</DescriptionDisplayTitle>
)}
</DescriptionTitle>
Expand All @@ -112,12 +112,10 @@ const NoteDescription = ({ children, onPin, needToAdd, onCloseAddMode, onChangeP
content={needToEdit && showNote.content ? showNote.content : ""}
label="description"
onCopyChanges={handleCopyNoteData}
pageName="notes"
/>
) : (
<MarkdownEditor
content={showNote.content || (showNote._id ? `undescribedNote` : "")}
previewModeOnly
/>
<MarkdownEditor content={showNote.content || ""} previewModeOnly pageName="notes" />
)}
</DescriptionContent>
</NotesDescription>
Expand Down
Loading

0 comments on commit 0f4056e

Please sign in to comment.