Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: notes | Func of Paste, Drop Image In editor #541

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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