Compare commits

..

10 commits

Author SHA1 Message Date
bc0c9c3f02 doc: update changelog 2023-04-29 10:17:08 +03:00
3963e435e8 feat: publish local notes 2023-04-29 10:16:04 +03:00
f8365ce2ee doc: add commit 2023-04-29 09:51:25 +03:00
32f1d78a71 doc: update changelog and readme 2023-04-29 09:45:40 +03:00
6d17fca352 fix: save edited note 2023-04-29 09:44:42 +03:00
3f705bf8c4 refactor: change printDate to timestamp2text 2023-04-29 09:37:22 +03:00
80858f53d9 fix: text updating in edit 2023-04-29 09:35:15 +03:00
b8c7c69012 fix: ai completion 2023-04-29 09:19:14 +03:00
0a0f0f950a feat: noteEditing 2023-04-29 08:58:57 +03:00
ff9eccc887 refactor: note inputs 2023-04-29 08:44:44 +03:00
15 changed files with 239 additions and 122 deletions

View file

@ -26,6 +26,7 @@ Running on: <https://anopaper.artegoser.ru/>
## Features ## Features
- Save notes locally - Save notes locally
- Edit local notes
- Publish one-time notes (when read, the note is deleted from the server and saved locally) - Publish one-time notes (when read, the note is deleted from the server and saved locally)
- Use OpenAI API to complete notes (with your own api key) - Use OpenAI API to complete notes (with your own api key)
- Collaborate with other users on notes - Collaborate with other users on notes

View file

@ -17,8 +17,8 @@
# Anopaper v1.1.0 (coming) # Anopaper v1.1.0 (coming)
- [ ] Local notes editing - [x] Local notes editing (0a0f0f950ae95afb78d3fe71815b351f77f01eb9)
- [ ] Publish local notes - [x] Publish local notes (3963e435e8faca9b93b2e481ac799d78ed863f8c)
- [x] Migration notes storage to mongodb (#3) - [x] Migration notes storage to mongodb (#3)
- [ ] Settings for publish notes, such as: delete after reading, number of reads before deleting, adding your own data (name, picture, status in the settings) to the note. - [ ] Settings for publish notes, such as: delete after reading, number of reads before deleting, adding your own data (name, picture, status in the settings) to the note.
- [ ] Api for upload photos - [ ] Api for upload photos

View file

@ -16,7 +16,7 @@
*/ */
import RenderMarkdown from "../components/markdown"; import RenderMarkdown from "../components/markdown";
import { printDate } from "./utils"; import { timestamp2text } from "./utils";
function Note({ note }) { function Note({ note }) {
return ( return (
@ -26,7 +26,7 @@ function Note({ note }) {
{note.name} {note.name}
</h2> </h2>
<div className="justify-self-center lg:justify-self-end"> <div className="justify-self-center lg:justify-self-end">
{`${printDate(note.time)} ${ {`${timestamp2text(note.time)} ${
note.pub ? `| ${locals.PublicNote}` : `| ${locals.LocalNote}` note.pub ? `| ${locals.PublicNote}` : `| ${locals.LocalNote}`
}`} }`}
</div> </div>

View file

@ -17,10 +17,10 @@
import { Configuration, OpenAIApi } from "openai"; import { Configuration, OpenAIApi } from "openai";
async function Complete(setText, textUpdate) { async function Complete(text) {
document.body.style.cursor = "wait"; document.body.style.cursor = "wait";
let initText = localStorage.getItem("NoteText"); let initText = text;
const configuration = new Configuration({ const configuration = new Configuration({
apiKey: settings.openAiKey, apiKey: settings.openAiKey,
@ -37,16 +37,9 @@ async function Complete(setText, textUpdate) {
logprobs: null, logprobs: null,
}); });
let totalText = initText + response.data.choices[0].text;
localStorage.setItem("NoteText", totalText);
setText(totalText);
if (settings.CollabEdit === true) {
textUpdate(totalText, true);
}
document.body.style.cursor = "default"; document.body.style.cursor = "default";
return initText + response.data.choices[0].text;
} }
export { Complete }; export { Complete };

View file

@ -14,9 +14,12 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
/* eslint-disable react-refresh/only-export-components */
import { CheckBox } from "./checkbox"; import { CheckBox } from "./checkbox";
import { inputStyle, settingsAddInput } from "./styles"; import { inputStyle, settingsAddInput } from "./styles";
import { ButtonWithIcon } from "./button";
import { Complete } from "../components/openai";
import { DocumentTextIcon } from "@heroicons/react/24/outline";
function SettingsCheckBox({ label, title, className, settingName, onClick }) { function SettingsCheckBox({ label, title, className, settingName, onClick }) {
return ( return (
@ -109,10 +112,80 @@ function SettingsSection({ name, children }) {
); );
} }
function NoteNameInput({ value, onChange, preview = false }) {
return (
<input
type="text"
className={`mb-2 md:w-1/6 w-full ${inputStyle} ${
preview ? "hidden" : ""
}`}
placeholder={locals.NoteName}
maxLength={64}
defaultValue={value}
onChange={onChange}
/>
);
}
function NoteTextArea({ value, onChange, preview = false }) {
return (
<textarea
className={`
${inputStyle}
w-full
${preview ? "hidden" : ""}
`}
rows="10"
placeholder={locals.NotePlaceholder}
maxLength={5000}
onChange={onChange}
defaultValue={value}
id="noteTextArea"
></textarea>
);
}
function NotesAdditionalSettings({
noteText = localStorage.getItem("NoteText"),
onClick,
}) {
return (
<>
{settings.additionalFeatures && (
<div className="justify-self-start lg:justify-self-start">
<SettingsSection name={locals.AdditionalFeatures}>
{!!settings.openAiKey && (
<ButtonWithIcon
icon={DocumentTextIcon}
text={locals.AIComplete}
className="m-1"
w="w-full"
onClick={async () => {
let text = await Complete(noteText);
document.getElementById("noteTextArea").value = text;
onClick(text);
}}
/>
)}
<SettingsCheckBox
label={locals.CollabEdit}
settingName="CollabEdit"
/>
</SettingsSection>
</div>
)}
</>
);
}
export { export {
SettingsCheckBox, SettingsCheckBox,
SettingsTextInput, SettingsTextInput,
SettingsSelectInput, SettingsSelectInput,
SettingsSection, SettingsSection,
setSetting, setSetting,
NoteNameInput,
NoteTextArea,
NotesAdditionalSettings,
}; };

View file

@ -17,7 +17,7 @@
import { Locales } from "../localisation/main"; import { Locales } from "../localisation/main";
function printDate(time) { function timestamp2text(time) {
time = new Date(time); time = new Date(time);
return time.toLocaleString(settings.language); return time.toLocaleString(settings.language);
} }
@ -48,4 +48,4 @@ async function getNetLocale(lang, fileName) {
return (await (await fetch(`localisation/${lang}/${fileName}`)).text()) || ""; return (await (await fetch(`localisation/${lang}/${fileName}`)).text()) || "";
} }
export { printDate, reRenderPage, localesProcess, getNetLocale }; export { timestamp2text, reRenderPage, localesProcess, getNetLocale };

View file

@ -75,6 +75,7 @@ let en = {
LocalNote: "Local", LocalNote: "Local",
Menu: "Menu", Menu: "Menu",
SourceCode: "Source code", SourceCode: "Source code",
Edit: "Edit",
}; };
export default en; export default en;

View file

@ -77,6 +77,7 @@ let ru = {
PublishNote: "Публичная", PublishNote: "Публичная",
Menu: "Меню", Menu: "Меню",
SourceCode: "Исходный код", SourceCode: "Исходный код",
Edit: "Изменить",
}; };
export default ru; export default ru;

View file

@ -16,14 +16,11 @@
*/ */
import { ButtonWithIcon } from "../components/button"; import { ButtonWithIcon } from "../components/button";
import { import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
ChevronDoubleRightIcon,
DocumentTextIcon,
} from "@heroicons/react/24/outline";
import { CheckBox } from "../components/checkbox"; import { CheckBox } from "../components/checkbox";
import { useState } from "react"; import { useState } from "react";
import RenderMarkdown from "../components/markdown"; import RenderMarkdown from "../components/markdown";
import { printDate } from "../components/utils"; import { timestamp2text } from "../components/utils";
import rehypeRemark from "rehype-remark/lib"; import rehypeRemark from "rehype-remark/lib";
import ContentEditable from "react-contenteditable"; import ContentEditable from "react-contenteditable";
import ReactDOMServer from "react-dom/server"; import ReactDOMServer from "react-dom/server";
@ -33,11 +30,11 @@ import remarkStringify from "remark-stringify";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import remarkMath from "remark-math"; import remarkMath from "remark-math";
import { import {
NoteNameInput,
NoteTextArea,
NotesAdditionalSettings,
SettingsCheckBox, SettingsCheckBox,
SettingsSection,
} from "../components/settingsInputs"; } from "../components/settingsInputs";
import { inputStyle } from "../components/styles";
import { Complete } from "../components/openai";
function nameUpdate(val, force) { function nameUpdate(val, force) {
if (Date.now() - window.lastSocketUpdate > window.socketTimeout || force) { if (Date.now() - window.lastSocketUpdate > window.socketTimeout || force) {
@ -127,13 +124,7 @@ function CreateNote() {
/> />
</div> </div>
<input <NoteNameInput
type="text"
className={`mb-2 md:w-1/6 w-full ${inputStyle} ${
preview ? "hidden" : ""
}`}
placeholder={locals.NoteName}
maxLength={64}
value={localStorage.getItem("NoteName") || ""} value={localStorage.getItem("NoteName") || ""}
onChange={(e) => { onChange={(e) => {
setName(e.target.value); setName(e.target.value);
@ -146,16 +137,11 @@ function CreateNote() {
}, window.socketTimeout); }, window.socketTimeout);
} }
}} }}
preview={preview}
/> />
<textarea
className={` <NoteTextArea
${inputStyle} value={localStorage.getItem("NoteText") || ""}
w-full
${preview ? "hidden" : ""}
`}
rows="10"
placeholder={locals.NotePlaceholder}
maxLength={5000}
onChange={(e) => { onChange={(e) => {
setText(e.target.value); setText(e.target.value);
localStorage.setItem("NoteText", e.target.value); localStorage.setItem("NoteText", e.target.value);
@ -167,8 +153,8 @@ function CreateNote() {
}, window.socketTimeout); }, window.socketTimeout);
} }
}} }}
value={localStorage.getItem("NoteText") || ""} preview={preview}
></textarea> />
{preview && ( {preview && (
<div className="grid grid-cols-1 lg:grid-cols-2"> <div className="grid grid-cols-1 lg:grid-cols-2">
@ -176,7 +162,7 @@ function CreateNote() {
{name} {name}
</h2> </h2>
<div className="justify-self-center lg:justify-self-end"> <div className="justify-self-center lg:justify-self-end">
{printDate(Date.now())} {timestamp2text(Date.now())}
</div> </div>
</div> </div>
)} )}
@ -213,27 +199,16 @@ function CreateNote() {
/> />
</div> </div>
{settings.additionalFeatures && ( <NotesAdditionalSettings
<div className="justify-self-start lg:justify-self-start"> onClick={(text) => {
<SettingsSection name={locals.AdditionalFeatures}> localStorage.setItem("NoteText", text);
{!!settings.openAiKey && ( setText(text);
<ButtonWithIcon
icon={DocumentTextIcon} if (settings.CollabEdit === true) {
text={locals.AIComplete} textUpdate(text, true);
className="m-1" }
w="w-full" }}
onClick={() => { />
Complete(setText, textUpdate);
}}
/>
)}
<SettingsCheckBox
label={locals.CollabEdit}
settingName="CollabEdit"
/>
</SettingsSection>
</div>
)}
</div> </div>
</div> </div>
); );

View file

@ -16,14 +16,30 @@
*/ */
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { ChevronDoubleLeftIcon, TrashIcon } from "@heroicons/react/24/outline"; import {
ChevronDoubleLeftIcon,
ChevronDoubleRightIcon,
PencilIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import { ButtonWithIcon } from "../components/button"; import { ButtonWithIcon } from "../components/button";
import Note from "../components/note"; import Note from "../components/note";
import { useState } from "react";
import {
NoteNameInput,
NoteTextArea,
NotesAdditionalSettings,
} from "../components/settingsInputs";
function NotePage() { function NotePage() {
let params = useParams(); let params = useParams();
let note = localStorage.getObj("Notes")[params.id]; let notes = localStorage.getObj("Notes");
let note = notes[params.id];
let [edit, setEdit] = useState(false);
let [text, setText] = useState(note.text);
let [name, setName] = useState(note.name);
return ( return (
<div className=""> <div className="">
@ -34,23 +50,71 @@ function NotePage() {
text={locals.Notes} text={locals.Notes}
/> />
{note ? <Note note={note} /> : <div>{locals.NoteNotExists}</div>} {note ? (
edit ? (
<>
<NoteNameInput
value={name}
onChange={(e) => setName(e.target.value)}
/>
<NoteTextArea
value={text}
onChange={(e) => setText(e.target.value)}
/>
<div className="grid grid-cols-1 lg:grid-cols-2 justify-items-center w-full">
<NotesAdditionalSettings
noteText={text}
onClick={(text) => {
setText(text);
}}
/>
</div>
</>
) : (
<Note note={note} />
)
) : (
<div>{locals.NoteNotExists}</div>
)}
{note && ( {note && (
<div className="grid grid-cols-1"> <div className="grid grid-cols-1">
<div className="justify-self-center lg:justify-self-end"> <div className="justify-self-center lg:justify-self-end">
<ButtonWithIcon <ButtonWithIcon
className="mt-4" className="mt-4"
href="/notes" text={edit ? locals.Save : locals.Edit}
text={locals.Delete} icon={PencilIcon}
icon={TrashIcon}
onClick={() => { onClick={() => {
let notesObj = localStorage.getObj("Notes"); if (edit) {
notes[params.id].name = name;
notes[params.id].text = text;
delete notesObj[params.id]; localStorage.setObj("Notes", notes);
}
localStorage.setObj("Notes", notesObj); setEdit(!edit);
}} }}
/> />
<ButtonWithIcon
className="mt-4"
text={locals.Publish}
icon={ChevronDoubleRightIcon}
href={`/notes/publish?local_id=${params.id}`}
/>
{!edit && (
<ButtonWithIcon
className="mt-4"
href="/notes"
text={locals.Delete}
icon={TrashIcon}
onClick={() => {
let notesObj = localStorage.getObj("Notes");
delete notesObj[params.id];
localStorage.setObj("Notes", notesObj);
}}
/>
)}
</div> </div>
</div> </div>
)} )}

View file

@ -17,7 +17,7 @@
import { ButtonWithIcon } from "../components/button"; import { ButtonWithIcon } from "../components/button";
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline"; import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
import { printDate } from "../components/utils"; import { timestamp2text } from "../components/utils";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { inputStyle } from "../components/styles"; import { inputStyle } from "../components/styles";
import { useState } from "react"; import { useState } from "react";
@ -30,7 +30,7 @@ function Notes() {
let notes = Object.entries(notesObj); let notes = Object.entries(notesObj);
for (let [id, note] of notes) { for (let [id, note] of notes) {
note.id = id; note.id = id;
note.textTime = printDate(note.time); note.textTime = timestamp2text(note.time);
notesObj[id] = note; notesObj[id] = note;
} }
localStorage.setObj("Notes", notesObj); localStorage.setObj("Notes", notesObj);
@ -82,7 +82,7 @@ function Notes() {
{item.name} {item.name}
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-2 justify-self-center lg:justify-self-end"> <div className="grid grid-cols-1 lg:grid-cols-2 justify-self-center lg:justify-self-end">
<div className="text-center">{printDate(item.time)}</div> <div className="text-center">{timestamp2text(item.time)}</div>
<div className=""> <div className="">
<ButtonWithIcon <ButtonWithIcon
href={`/notes/${item.id}`} href={`/notes/${item.id}`}

View file

@ -15,7 +15,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { printDate } from "../components/utils"; import { timestamp2text } from "../components/utils";
import { ChevronDoubleLeftIcon } from "@heroicons/react/24/outline"; import { ChevronDoubleLeftIcon } from "@heroicons/react/24/outline";
import { ButtonWithIcon } from "../components/button"; import { ButtonWithIcon } from "../components/button";
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
@ -39,7 +39,7 @@ function PubError() {
{locals.PubError} {locals.PubError}
</h2> </h2>
<div className="justify-self-center lg:justify-self-end"> <div className="justify-self-center lg:justify-self-end">
{printDate(Date.now())} {timestamp2text(Date.now())}
</div> </div>
</div> </div>
<div className="w-full md">{err ? err : locals.PubErrorMsg}</div> <div className="w-full md">{err ? err : locals.PubErrorMsg}</div>

View file

@ -18,7 +18,7 @@
import RenderMarkdown from "../components/markdown"; import RenderMarkdown from "../components/markdown";
import { useState } from "react"; import { useState } from "react";
import { Navigate, useParams } from "react-router-dom"; import { Navigate, useParams } from "react-router-dom";
import { printDate } from "../components/utils"; import { timestamp2text } from "../components/utils";
import { ChevronDoubleLeftIcon } from "@heroicons/react/24/outline"; import { ChevronDoubleLeftIcon } from "@heroicons/react/24/outline";
import { ButtonWithIcon } from "../components/button"; import { ButtonWithIcon } from "../components/button";
@ -73,7 +73,7 @@ function PubNote() {
{note.name || "Загрузка..."} {note.name || "Загрузка..."}
</h2> </h2>
<div className="justify-self-center lg:justify-self-end"> <div className="justify-self-center lg:justify-self-end">
{printDate(note.time || Date.now())} {timestamp2text(note.time || Date.now())}
</div> </div>
</div> </div>
<div className="w-full md break-words"> <div className="w-full md break-words">

View file

@ -17,54 +17,63 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useSearchParams } from "react-router-dom";
function Publish() { function Publish() {
const navigate = useNavigate(); const navigate = useNavigate();
let done = false; const [searchParams] = useSearchParams();
useEffect(() => { useEffect(() => {
if (!done) { let err = false;
done = true;
let err = false; let note;
const note = {
if (searchParams.get("local_id")) {
let localNote =
localStorage.getObj("Notes")[searchParams.get("local_id")];
note = {
name: localNote.name,
text: localNote.text,
};
} else {
note = {
name: localStorage.getItem("NoteName"), name: localStorage.getItem("NoteName"),
text: localStorage.getItem("NoteText"), text: localStorage.getItem("NoteText"),
}; };
if (!note.name) {
err = locals.PubErrorMsgNoName;
}
if (!note.text) {
err = locals.PubErrorMsgNoText;
}
fetch(`/publish`, {
method: "POST",
headers: {
"Content-Type": "application/json;charset=utf-8",
},
body: JSON.stringify(note),
})
.then((data) => {
data
.json()
.then((data) => {
localStorage.removeItem("NoteName");
localStorage.removeItem("NoteText");
navigate(`/pubNotesSafe/${data.id}`, { replace: true });
})
.catch(() => {
if (err == false) {
navigate(`/pubError`, { replace: true });
} else navigate(`/pubError?err=${err}`, { replace: true });
});
})
.catch(() => {
navigate(`/pubError`, { replace: true });
});
} }
}, []);
if (!note.name) {
err = locals.PubErrorMsgNoName;
}
if (!note.text) {
err = locals.PubErrorMsgNoText;
}
fetch(`/publish`, {
method: "POST",
headers: {
"Content-Type": "application/json;charset=utf-8",
},
body: JSON.stringify(note),
})
.then((data) => {
data
.json()
.then((data) => {
localStorage.removeItem("NoteName");
localStorage.removeItem("NoteText");
navigate(`/pubNotesSafe/${data.id}`, { replace: true });
})
.catch(() => {
if (err == false) {
navigate(`/pubError`, { replace: true });
} else navigate(`/pubError?err=${err}`, { replace: true });
});
})
.catch(() => {
navigate(`/pubError`, { replace: true });
});
});
return <div />; return <div />;
} }

View file

@ -16,7 +16,7 @@
*/ */
import { Navigate } from "react-router-dom"; import { Navigate } from "react-router-dom";
import { printDate } from "../components/utils"; import { timestamp2text } from "../components/utils";
function uuidv4() { function uuidv4() {
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
@ -44,7 +44,7 @@ function Save() {
name, name,
text, text,
time, time,
textTime: printDate(time), textTime: timestamp2text(time),
pubTime, pubTime,
pub: !!pubTime, pub: !!pubTime,
}; };