This website is a MVP clone of evernote.com, with Obsidian-inspired styling & layout. Live website is here.
- Notes
- users can create new Notes (note icon on top left)
- users can rename Notes (input field in the top line)
- users can delete Notes (trash can icon on top right)
- users can assign notes to folders (top line dropdown)
- Folders
- users can create new folders (folder icon on top left)
- users can delete folders (trash can icon next to folder name)
- users can rename folders (double click on folder name)
- rename is saved when you unfocus the field (onblur)
- Markdown editor
- users can switch between READ and EDIT mode (toggle in top line)
- in EDIT mode, the text is entered in Markdown format
- in READ mode, the markdown text is rendered
- Autosave of note title and note content
- as the user types the content is automatically patched (with 1 sec debounce)
Feature not implemented (maybe in the future):
- d3 visualizion of node connections (a graph!)
- tokenized (ts-vector) search of note content
Can be viewed in the Project Wiki.
- Requires a
Postgres
installations - Run the following commands (these instructions are incomplete)
git clone https://github.com/ily123/grafnote cd grafnote npm install npm start
- Make sure to create & populate the
.env
file following the example given in.env.example
.
- Express backend
- React front-end with Redux state management
- Vanilla CSS
- Example below is of two react functional components: a
FileLink
and aFolderLink
navigation items for theSideBar
component.
function FileLink ({ type, payload }) {
const dispatch = useDispatch();
return (
<div
className={type + '-link'}
key={type + payload.id}
onClick={() => dispatch(setActiveNoteId(payload.id))}
>{payload.title}</div>
);
}
function FolderLink ({ type, payload }) {
const dispatch = useDispatch();
const [title, setTitle] = useState(payload.title);
const [readOnly, setReadOnly] = useState(true);
if (!payload || !type) return null;
const sendNewTitle = () => {
setReadOnly(true);
dispatch(editFolderTitle(payload.id, title));
};
return (
<div
className={type + '-link'}
key={type + payload.id}
>
<i className="fas fa-folder" style={{ paddingRight: '5px' }}></i>
<input type="text"
value={title}
readOnly={readOnly}
className={readOnly ? 'inactive' : 'active'}
onDoubleClick={() => setReadOnly(false)}
onBlur={(e) => sendNewTitle(e.target.value)}
onChange={(e) => setTitle(e.target.value)}
></input>
<i className="far fa-trash-alt"
onClick={() => dispatch(deleteFolder(payload.id))}
></i>
</div>
);
};
- Ilya Novikov
Unlicense (https://en.wikipedia.org/wiki/Unlicense).