Deployed here: https://moar-notes.herokuapp.com
A productivity app to keep your thoughts organized. Take notes as an infinite, collapsable bullet list. (Or a poor Workflowy clone)
- Backend: A Ruby on Rails REST API
- Database: MongoDB with the Mongoid ORM
- Frontend: A React App used with TypeScript
- Deployment: React code is built & copied to the Rails /public folder & deployed on a single server using Heroku.
- Create an account & log-in.
- Create an infinitely nested list of bullet points.
- Changes are synced with the server in almost real-time.
- Collapse/expand any note with child notes
Enter
-> Adds a new note below the currently focused note.Tab
-> Indents a note towards rightShift
+Tab
-> Indents a note towards leftBackspace
when empty -> Deletes a note. If it has any chlid_notes, they're added to the parent of the deleted note.UpArrow
+DownArrow
-> Navigate up and down using the arrow keysCtrl
+B
-> Make the text boldCtrl
+I
-> Make the text italicizedCtrl
+U
-> Make the text Underlined
- A User has_many Notes
- Notes Model
- To maintain a tree-like structure between the notes, I'm using a pattern called Materialized Paths (http://learnmongodbthehardway.com/schema/categoryhierarchy/)
- Each note has a field called
path
which stores the path of ids of all its parents. - To maintain the sequence of notes, we're using a number field calleed
order
- This allows us to fetch all notes of a user in a single query.
- We can also do queries like fetching the subtree of any note in a single query.
- To maintain the integrity of sequencing and improve performance a MongoDB
unique index
is added for{ user_id: 1, path: 1, order: 1 }
- Authentication
- Password based authentication
- Uses
has_secure_password
by Rails for storing password with bcrypt encryption - Sends a Json Web Token to the client
- The client send the JWT with each api call
- Sever APIs
- POST /users -> register a user, returns an auth_token
- POST /users/login -> login a user, returns an auth_token
- GET /users/auto_login -> checks if an auth_token sent with the api call is valid
- GET /notes -> returns notes in the form of a nested json for the authenticated user
- POST /notes/process_transactions -> applies the transactions sent by the client on the database & returns newly created note_ids
- The core component is
RootNote
. It has all the core logic to handle various features. - The component called
Note
has the bare minumum each single note needs. It is rendered for every single bullet point. - The
serverApis.tsx
handles the communication with the user. - The utils.tsx module deals with creating transactions for changes made by the user.
- Changes to the server are sent when the user hasn't typed anything for 4 seconds.
- In the state, we store
sycnedNotes
¬es
.notes
has the state sycned with what user types at all timesycnedNotes
has the notes that are in sync with the server- When the user hasn't typed anything for 4 seconds, the two are compared to find the changes to be sent to the server.
- The changes are sent to the server in the form of transactions. Transactions may be of type
added
,updated
,deleted
, ormoved
- The server returns the ids for the newly created notes, which are then put in the
notes
&syncedNotes
. sycnedNotes
are updated every time the api is called and gives 200 status response.
- The server returns the ids for the newly created notes, which are then put in the
- The libraries used:
immer
for ease with immutable objectsjsondiffpatch
for comparing the old and the new statereact-contenteditable
Most of these require changes in the frontend and minimal changes on the server
- Zooming in on a specific note
- We should be able to open any bullet point on its own. Changes will be in the frontend, minimal changes in the server. The server already has the support for returning child_tree for any note.
- Undoing & Redoing capabilities
- Can be handled entirely on the frontend. Can store user's actions in react state and use it to undo & redo changes. The existing logic for diffing and creating transactions will not change.
- Tagging
- The current architecure can be extended to add a new field and make changes to the frontend to show tags for a note and show all notes for a tag. (Not sure if the list of notes for a tag should be fetched from local state or from an server API)
- Searching
- Can create an API on the server that can do fuzzy search on all the content in the notes.
- Can use a full-text search engine like ElasticSearch to index the content. (MogoDB's text-search may work as well, need to research)
- The frontend app needs to come up with a way to show the search result from the existing state.
- Drag and drop notes
- Can use some library to handle drag and drop UX, and the handler will change the react state for notes. No other changes are expected.
- Paste outline notes copied from somewhere else and preserve the structure
- Can attach an
onPaste
handler which will parse the pasted data, and understand the structure of the data and modify the react state.
- Can attach an
- Select and copy multiple notes with its structure
- Add handlers to select multiple notes, and a way to generate structured outline data from it.
- Warn user when she's closing tab before all changes are synced
Expand All
&Collapse All
buttons- Offline support
- Can make it Progressive Web App.
- Need to store the state locally.
IndexedDB
can be a good option to store it.
- Real-time synchronization between multiple instances of clients
- Can use Websockets to make it work.
- The current transactions pattern should make it easy to sync the data reliably.
- Adding other authentication methods using OAuth2
- Optimizing for large amounts of data
- Making certain page public
- Easy to implement. Can add caching for these notes as well.
- Better support for mobile phone
- Can add extra buttons that'll stick at the bottom for indending the notes left & right
- While Redux/GraphQL would have been a great choices for the project, due to lock of time, my experience with both was limited, and using them would've meant spending more time learning these instead of implementing the features of the app.
- Hence I avoided using any tech that I was not already proficient in to minimize the time spent in learning it / fixing bugs.
- Using
react-testing-library
withjest
for unit and integration tests. - Most of the code for the core features is covered by integrated tests.
- Didn't do a lot of unit tests due to a lack of time, but if this project was to be used by real users, I would add some essential unit tests before making it live.
- Also skipped tests for some frontend code for non-core features (Login/Regsiter pages) because of the same reason.
- Test coverage: backend:
rake stats
, frontend:cd client && npm test -- --coverage --watchAll=false
- Add following mandatory credentials using
bin/rails credentials:edit
- mongo_url
- mongo_url_test
- Run
bundle install && cd client && npm install
-
Run
rails s -p 3001
in one terminal and in the another terminal you can docd client && npm start
- If you have heroku cli installed, - Run
heroku local -f Procfile.dev
- If you have heroku cli installed, - Run
-
Run tests:
bundle exec rspec spec
rake stats
-> check test coveragecd client && npm run test
- edit:
bin/rails credentials:edit
- access in code:
Rails.application.credentials.cred_name
- production environment should get the correct master key ENV["RAILS_MASTER_KEY"]
- Automatic deploys are enabled for this repo on the branch
main
- To deploy manually:
git push heroku main
- To view production logs:
heroku logs --tail