├── vendor
└── .keep
├── lib
├── assets
│ └── .keep
└── tasks
│ └── .keep
├── public
├── favicon.ico
├── apple-touch-icon.png
├── apple-touch-icon-precomposed.png
├── robots.txt
├── 500.html
├── 422.html
└── 404.html
├── test
├── helpers
│ └── .keep
├── mailers
│ └── .keep
├── models
│ └── .keep
├── system
│ └── .keep
├── controllers
│ └── .keep
├── fixtures
│ ├── .keep
│ └── files
│ │ └── .keep
├── integration
│ └── .keep
├── application_system_test_case.rb
└── test_helper.rb
├── app
├── assets
│ ├── images
│ │ ├── .keep
│ │ ├── flag.png
│ │ ├── map.png
│ │ ├── note.png
│ │ ├── plus.png
│ │ ├── tag.png
│ │ ├── notes.png
│ │ ├── trash.png
│ │ ├── account.png
│ │ ├── notebook.png
│ │ └── green_plus.png
│ ├── javascripts
│ │ ├── channels
│ │ │ └── .keep
│ │ ├── cable.js
│ │ └── application.js
│ ├── config
│ │ └── manifest.js
│ └── stylesheets
│ │ ├── variables.scss
│ │ ├── loading_page.scss
│ │ ├── reset.scss
│ │ ├── notebook_index.scss
│ │ ├── buttons.scss
│ │ ├── tag_index.scss
│ │ ├── note_order_dropdown.scss
│ │ ├── application.scss
│ │ ├── notebook_dropdown.scss
│ │ ├── sidenav.scss
│ │ ├── sidemenu.scss
│ │ ├── session_form_page.scss
│ │ ├── default_page.scss
│ │ ├── full_forms.scss
│ │ ├── note_index.scss
│ │ ├── map.scss
│ │ └── editor.scss
├── models
│ ├── concerns
│ │ └── .keep
│ ├── application_record.rb
│ ├── tagging.rb
│ ├── photo.rb
│ ├── notebook.rb
│ ├── tag.rb
│ ├── note.rb
│ ├── user.rb
│ └── flag.rb
├── controllers
│ ├── concerns
│ │ └── .keep
│ ├── static_pages_controller.rb
│ ├── api
│ │ ├── all_items_controller.rb
│ │ ├── photos_controller.rb
│ │ ├── users_controller.rb
│ │ ├── tags_controller.rb
│ │ ├── flags_controller.rb
│ │ ├── notebooks_controller.rb
│ │ ├── sessions_controller.rb
│ │ └── notes_controller.rb
│ └── application_controller.rb
├── views
│ ├── layouts
│ │ ├── mailer.text.erb
│ │ ├── mailer.html.erb
│ │ └── application.html.erb
│ ├── api
│ │ ├── tags
│ │ │ ├── show.json.jbuilder
│ │ │ └── _tag.json.jbuilder
│ │ ├── flags
│ │ │ ├── show.json.jbuilder
│ │ │ └── _flag.json.jbuilder
│ │ ├── photos
│ │ │ ├── show.json.jbuilder
│ │ │ └── _photo.json.jbuilder
│ │ ├── users
│ │ │ └── _user.json.jbuilder
│ │ ├── notebooks
│ │ │ ├── show.json.jbuilder
│ │ │ └── _notebook.json.jbuilder
│ │ ├── notes
│ │ │ ├── create.json.jbuilder
│ │ │ ├── destroy.json.jbuilder
│ │ │ ├── _note.json.jbuilder
│ │ │ └── update.json.jbuilder
│ │ └── all_items
│ │ │ └── index.json.jbuilder
│ └── static_pages
│ │ └── root.html.erb
├── helpers
│ └── application_helper.rb
├── jobs
│ └── application_job.rb
├── channels
│ └── application_cable
│ │ ├── channel.rb
│ │ └── connection.rb
└── mailers
│ └── application_mailer.rb
├── README_images
├── auth.gif
├── note.gif
├── order.gif
├── autosave.gif
├── expand.gif
├── navbar.png
├── tag_menu.png
├── note_delete.png
├── sorting_menu.png
├── notebook_delete.png
├── notebooks_tags.gif
└── notebook_dropdown.png
├── wiki_images
├── HomePage.jpg
├── NotesIndex.jpg
├── SideMenu.jpg
├── SessionForm.jpg
├── NewAndDeleteForm.jpg
└── NoteForm-fullscreen.jpg
├── bin
├── bundle
├── rake
├── rails
├── yarn
├── spring
├── update
└── setup
├── config
├── spring.rb
├── boot.rb
├── initializers
│ ├── mime_types.rb
│ ├── filter_parameter_logging.rb
│ ├── application_controller_renderer.rb
│ ├── cookies_serializer.rb
│ ├── backtrace_silencers.rb
│ ├── wrap_parameters.rb
│ ├── assets.rb
│ └── inflections.rb
├── cable.yml
├── environment.rb
├── routes.rb
├── database.yml
├── application.rb
├── locales
│ └── en.yml
├── secrets.yml
├── environments
│ ├── test.rb
│ ├── development.rb
│ └── production.rb
└── puma.rb
├── config.ru
├── frontend
├── util
│ ├── entity_api_util.js
│ ├── photo_api_util.js
│ ├── notebook_api_util.js
│ ├── debounce.js
│ ├── session_api_util.js
│ ├── tag_api_util.js
│ ├── flag_api_util.js
│ ├── quill_configs.js
│ ├── route_util.jsx
│ ├── note_api_util.js
│ ├── sorters.js
│ └── marker_util.js
├── components
│ ├── sidemenu
│ │ ├── tag_details.jsx
│ │ ├── notebook_details.jsx
│ │ ├── sidemenu_heading.jsx
│ │ ├── sidemenu_index_item_container.js
│ │ ├── sidemenu_container.js
│ │ ├── sidemenu_index_item.jsx
│ │ └── sidemenu.jsx
│ ├── session
│ │ ├── auth_page.jsx
│ │ ├── logout_form_container.js
│ │ ├── session_form_container.js
│ │ ├── logout_form.jsx
│ │ └── session_form.jsx
│ ├── editor
│ │ ├── tags.jsx
│ │ ├── editor_lower_heading.jsx
│ │ ├── notebook_dropdown_container.js
│ │ ├── editor_container.js
│ │ └── notebook_dropdown.jsx
│ ├── note_index
│ │ ├── all_notes_index_container.js
│ │ ├── note_index_item_container.js
│ │ ├── tag_notes_index_container.js
│ │ ├── notebook_notes_index_container.js
│ │ ├── note_order_options_container.js
│ │ ├── notes_in_map_container.jsx
│ │ ├── filtered_notes_index_container.js
│ │ ├── note_order_option_menu.jsx
│ │ ├── note_index.jsx
│ │ └── note_index_item.jsx
│ ├── map_view
│ │ ├── map_container.js
│ │ ├── map_view_container.js
│ │ ├── location_search.jsx
│ │ ├── map.jsx
│ │ └── map_view.jsx
│ ├── app_container.js
│ ├── sidenav
│ │ ├── sidenav_container.js
│ │ └── sidenav.jsx
│ ├── root.jsx
│ ├── entity_forms
│ │ ├── delete_form_container.js
│ │ ├── create_form_container.js
│ │ ├── delete_form.jsx
│ │ └── create_form.jsx
│ ├── default_page.jsx
│ └── app.jsx
├── store
│ └── store.js
├── reducers
│ ├── errors
│ │ ├── tag_errors_reducer.js
│ │ ├── flag_errors_reducer.js
│ │ ├── note_errors_reducer.js
│ │ ├── notebook_errors_reducer.js
│ │ └── session_errors_reducer.js
│ ├── root_reducer.js
│ ├── session_reducer.js
│ ├── entities_reducer.js
│ ├── errors_reducer.js
│ ├── entities
│ │ ├── notebooks_reducer.js
│ │ ├── flags_reducer.js
│ │ ├── tags_reducer.js
│ │ ├── notes_reducer.js
│ │ └── markers_reducer.js
│ └── ui_reducer.js
├── actions
│ ├── entity_actions.js
│ ├── tag_actions.js
│ ├── notebook_actions.js
│ ├── flag_actions.js
│ ├── ui_actions.js
│ ├── session_actions.js
│ └── note_actions.js
└── omninote.jsx
├── db
├── migrate
│ ├── 20171028044929_add_column_to_notes.rb
│ ├── 20171026213840_remove_column_from_notes.rb
│ ├── 20171030031357_add_user_id_to_tags.rb
│ ├── 20171101005942_remove_note_id_from_photos.rb
│ ├── 20171101010126_add_user_id_to_photos.rb
│ ├── 20171215205806_change_flags_column_type.rb
│ ├── 20171225013511_add_formatted_address_to_flags.rb
│ ├── 20171030030329_create_tags.rb
│ ├── 20171219124020_add_column_to_notes_for_flags.rb
│ ├── 20171215035140_add_column_to_flags.rb
│ ├── 20171030031545_create_taggings.rb
│ ├── 20171025125433_add_timestamp_to_users.rb
│ ├── 20171030032138_add_timestamp_to_tags.rb
│ ├── 20171215053335_add_timestamps_to_flags.rb
│ ├── 20171030032142_add_timestamp_to_taggings.rb
│ ├── 20171025125908_add_timestamp_to_notebooks.rb
│ ├── 20171025125057_create_notebooks.rb
│ ├── 20171031215151_add_attachment_image_to_notes.rb
│ ├── 20171214024813_create_flags.rb
│ ├── 20171023223343_create_users.rb
│ └── 20171026213206_create_notes.rb
└── schema.rb
├── Rakefile
├── .gitignore
├── webpack.config.js
├── package.json
└── Gemfile
/vendor/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/tasks/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/helpers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/mailers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/system/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/controllers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/integration/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/files/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/javascripts/channels/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/views/api/tags/show.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.partial! 'tag', tag: @tag
2 |
--------------------------------------------------------------------------------
/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | class ApplicationJob < ActiveJob::Base
2 | end
3 |
--------------------------------------------------------------------------------
/app/views/api/flags/show.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.partial! 'flag', flag: @flag
2 |
--------------------------------------------------------------------------------
/app/views/api/photos/show.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.partial! 'photo', photo: @photo
2 |
--------------------------------------------------------------------------------
/app/views/api/users/_user.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.id user.id
2 | json.email user.email
3 |
--------------------------------------------------------------------------------
/app/views/api/notebooks/show.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.partial! 'notebook', notebook: @notebook
2 |
--------------------------------------------------------------------------------
/README_images/auth.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/README_images/auth.gif
--------------------------------------------------------------------------------
/README_images/note.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/README_images/note.gif
--------------------------------------------------------------------------------
/README_images/order.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/README_images/order.gif
--------------------------------------------------------------------------------
/app/views/api/photos/_photo.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.imageUrl asset_path(photo.image.url(:original))
2 |
--------------------------------------------------------------------------------
/README_images/autosave.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/README_images/autosave.gif
--------------------------------------------------------------------------------
/README_images/expand.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/README_images/expand.gif
--------------------------------------------------------------------------------
/README_images/navbar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/README_images/navbar.png
--------------------------------------------------------------------------------
/README_images/tag_menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/README_images/tag_menu.png
--------------------------------------------------------------------------------
/app/assets/images/flag.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/app/assets/images/flag.png
--------------------------------------------------------------------------------
/app/assets/images/map.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/app/assets/images/map.png
--------------------------------------------------------------------------------
/app/assets/images/note.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/app/assets/images/note.png
--------------------------------------------------------------------------------
/app/assets/images/plus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/app/assets/images/plus.png
--------------------------------------------------------------------------------
/app/assets/images/tag.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/app/assets/images/tag.png
--------------------------------------------------------------------------------
/wiki_images/HomePage.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/wiki_images/HomePage.jpg
--------------------------------------------------------------------------------
/wiki_images/NotesIndex.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/wiki_images/NotesIndex.jpg
--------------------------------------------------------------------------------
/wiki_images/SideMenu.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/wiki_images/SideMenu.jpg
--------------------------------------------------------------------------------
/app/assets/images/notes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/app/assets/images/notes.png
--------------------------------------------------------------------------------
/app/assets/images/trash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/app/assets/images/trash.png
--------------------------------------------------------------------------------
/wiki_images/SessionForm.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/wiki_images/SessionForm.jpg
--------------------------------------------------------------------------------
/README_images/note_delete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/README_images/note_delete.png
--------------------------------------------------------------------------------
/README_images/sorting_menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/README_images/sorting_menu.png
--------------------------------------------------------------------------------
/app/assets/images/account.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/app/assets/images/account.png
--------------------------------------------------------------------------------
/app/assets/images/notebook.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/app/assets/images/notebook.png
--------------------------------------------------------------------------------
/app/views/api/tags/_tag.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.id tag.id
2 | json.title tag.title
3 | json.noteIds tag.note_ids
4 |
--------------------------------------------------------------------------------
/README_images/notebook_delete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/README_images/notebook_delete.png
--------------------------------------------------------------------------------
/README_images/notebooks_tags.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/README_images/notebooks_tags.gif
--------------------------------------------------------------------------------
/app/assets/images/green_plus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/app/assets/images/green_plus.png
--------------------------------------------------------------------------------
/app/views/api/notes/create.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.note do
2 | json.partial! 'api/notes/note', note: @note
3 | end
4 |
--------------------------------------------------------------------------------
/wiki_images/NewAndDeleteForm.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/wiki_images/NewAndDeleteForm.jpg
--------------------------------------------------------------------------------
/README_images/notebook_dropdown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/README_images/notebook_dropdown.png
--------------------------------------------------------------------------------
/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | self.abstract_class = true
3 | end
4 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 |
--------------------------------------------------------------------------------
/wiki_images/NoteForm-fullscreen.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ommish/Omninote/HEAD/wiki_images/NoteForm-fullscreen.jpg
--------------------------------------------------------------------------------
/app/controllers/static_pages_controller.rb:
--------------------------------------------------------------------------------
1 | class StaticPagesController < ApplicationController
2 | def root
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Channel < ActionCable::Channel::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | //= link_tree ../images
2 | //= link_directory ../javascripts .js
3 | //= link_directory ../stylesheets .css
4 |
--------------------------------------------------------------------------------
/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Connection < ActionCable::Connection::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 | default from: 'from@example.com'
3 | layout 'mailer'
4 | end
5 |
--------------------------------------------------------------------------------
/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/config/spring.rb:
--------------------------------------------------------------------------------
1 | %w(
2 | .ruby-version
3 | .rbenv-vars
4 | tmp/restart.txt
5 | tmp/caching-dev.txt
6 | ).each { |path| Spring.watch(path) }
7 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
2 |
3 | require 'bundler/setup' # Set up gems listed in the Gemfile.
4 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative 'config/environment'
4 |
5 | run Rails.application
6 |
--------------------------------------------------------------------------------
/frontend/util/entity_api_util.js:
--------------------------------------------------------------------------------
1 | export const fetchAll = () => {
2 | return $.ajax({
3 | url: 'api/all_items',
4 | method: 'get',
5 | });
6 | };
7 |
--------------------------------------------------------------------------------
/app/views/api/notes/destroy.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.noteId @note.id
2 | json.notebookId @note.notebook_id
3 | json.tagIds @note.tag_ids
4 | json.flagId @note.flag_id
5 |
--------------------------------------------------------------------------------
/app/models/tagging.rb:
--------------------------------------------------------------------------------
1 | class Tagging < ApplicationRecord
2 | validates :tag_id, uniqueness: { scope: :note_id }
3 |
4 | belongs_to :tag
5 | belongs_to :note
6 | end
7 |
--------------------------------------------------------------------------------
/app/views/api/notebooks/_notebook.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.id notebook.id
2 | json.title notebook.title
3 | json.noteIds notebook.note_ids
4 | json.updatedAt notebook.updated_at
5 |
--------------------------------------------------------------------------------
/db/migrate/20171028044929_add_column_to_notes.rb:
--------------------------------------------------------------------------------
1 | class AddColumnToNotes < ActiveRecord::Migration[5.1]
2 | def change
3 | add_column :notes, :body_plain, :text
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20171026213840_remove_column_from_notes.rb:
--------------------------------------------------------------------------------
1 | class RemoveColumnFromNotes < ActiveRecord::Migration[5.1]
2 | def change
3 | remove_column :notes, :user_id
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20171030031357_add_user_id_to_tags.rb:
--------------------------------------------------------------------------------
1 | class AddUserIdToTags < ActiveRecord::Migration[5.1]
2 | def change
3 | add_column :tags, :user_id, :integer, null: false
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20171101005942_remove_note_id_from_photos.rb:
--------------------------------------------------------------------------------
1 | class RemoveNoteIdFromPhotos < ActiveRecord::Migration[5.1]
2 | def change
3 | remove_column :photos, :note_id
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 |
--------------------------------------------------------------------------------
/db/migrate/20171101010126_add_user_id_to_photos.rb:
--------------------------------------------------------------------------------
1 | class AddUserIdToPhotos < ActiveRecord::Migration[5.1]
2 | def change
3 | add_column :photos, :user_id, :integer, null: false
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20171215205806_change_flags_column_type.rb:
--------------------------------------------------------------------------------
1 | class ChangeFlagsColumnType < ActiveRecord::Migration[5.1]
2 | def change
3 | change_column :flags, :place_id, :string, null: false
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/test/application_system_test_case.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
4 | driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20171225013511_add_formatted_address_to_flags.rb:
--------------------------------------------------------------------------------
1 | class AddFormattedAddressToFlags < ActiveRecord::Migration[5.1]
2 | def change
3 | add_column :flags, :formatted_address, :string
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: async
3 |
4 | test:
5 | adapter: async
6 |
7 | production:
8 | adapter: redis
9 | url: redis://localhost:6379/1
10 | channel_prefix: Omninote_production
11 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/variables.scss:
--------------------------------------------------------------------------------
1 | $black: #000000;
2 | $darker-grey: #4d4d4d;
3 | $dark-grey: #676767;
4 | $grey: #e6e6e6;
5 | $light-grey: #f5f5f5;
6 | $dark-green: #00a14c;
7 | $light-green: #00be59;
8 | $red: #ff1a1a;
9 |
--------------------------------------------------------------------------------
/db/migrate/20171030030329_create_tags.rb:
--------------------------------------------------------------------------------
1 | class CreateTags < ActiveRecord::Migration[5.1]
2 | def change
3 | create_table :tags do |t|
4 | t.string :title, null: false
5 | t.timestamp
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/db/migrate/20171219124020_add_column_to_notes_for_flags.rb:
--------------------------------------------------------------------------------
1 | class AddColumnToNotesForFlags < ActiveRecord::Migration[5.1]
2 | def change
3 | add_column :notes, :flag_id, :integer
4 | add_index :notes, :flag_id
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/db/migrate/20171215035140_add_column_to_flags.rb:
--------------------------------------------------------------------------------
1 | class AddColumnToFlags < ActiveRecord::Migration[5.1]
2 | def change
3 | add_column :flags, :lat, :float, null: false
4 | add_column :flags, :lng, :float, null: false
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/models/photo.rb:
--------------------------------------------------------------------------------
1 | class Photo < ApplicationRecord
2 | has_attached_file :image, default_url: "missing.png", s3_protocol: :https
3 | validates_attachment_content_type :image, content_type: /\Aimage\/.*\Z/
4 |
5 | belongs_to :user
6 | end
7 |
--------------------------------------------------------------------------------
/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [:password]
5 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require_relative 'config/application'
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | load File.expand_path('../spring', __FILE__)
4 | rescue LoadError => e
5 | raise unless e.message.include?('spring')
6 | end
7 | require_relative '../config/boot'
8 | require 'rake'
9 | Rake.application.run
10 |
--------------------------------------------------------------------------------
/db/migrate/20171030031545_create_taggings.rb:
--------------------------------------------------------------------------------
1 | class CreateTaggings < ActiveRecord::Migration[5.1]
2 | def change
3 | create_table :taggings do |t|
4 | t.integer :note_id, null: false
5 | t.integer :tag_id, null: false
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative 'application'
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
7 | Rails.application.configure do
8 | Jbuilder.key_format camelize: :lower
9 | end
10 |
--------------------------------------------------------------------------------
/db/migrate/20171025125433_add_timestamp_to_users.rb:
--------------------------------------------------------------------------------
1 | class AddTimestampToUsers < ActiveRecord::Migration[5.1]
2 | def change
3 | add_column :users, :created_at, :datetime, null: false
4 | add_column :users, :updated_at, :datetime, null: false
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/db/migrate/20171030032138_add_timestamp_to_tags.rb:
--------------------------------------------------------------------------------
1 | class AddTimestampToTags < ActiveRecord::Migration[5.1]
2 | def change
3 | add_column :tags, :created_at, :datetime, null: false
4 | add_column :tags, :updated_at, :datetime, null: false
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/views/api/flags/_flag.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.id flag.id
2 | json.placeId flag.place_id
3 | json.title flag.title
4 | json.lat flag.lat
5 | json.lng flag.lng
6 | json.formattedAddress flag.formatted_address
7 | json.noteIds flag.note_ids
8 | json.updatedAt flag.updated_at
9 |
--------------------------------------------------------------------------------
/app/views/static_pages/root.html.erb:
--------------------------------------------------------------------------------
1 |
6 |
7 | Root Pages
8 |
9 |
--------------------------------------------------------------------------------
/db/migrate/20171215053335_add_timestamps_to_flags.rb:
--------------------------------------------------------------------------------
1 | class AddTimestampsToFlags < ActiveRecord::Migration[5.1]
2 | def change
3 | add_column :flags, :updated_at, :datetime, null: false
4 | add_column :flags, :created_at, :datetime, null: false
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/db/migrate/20171030032142_add_timestamp_to_taggings.rb:
--------------------------------------------------------------------------------
1 | class AddTimestampToTaggings < ActiveRecord::Migration[5.1]
2 | def change
3 | add_column :taggings, :created_at, :datetime, null: false
4 | add_column :taggings, :updated_at, :datetime, null: false
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/frontend/util/photo_api_util.js:
--------------------------------------------------------------------------------
1 | export const createPhoto = (photoData) => {
2 | return $.ajax({
3 | method: 'post',
4 | url: 'api/photos',
5 | data: photoData,
6 | dataType: 'json',
7 | processData: false,
8 | contentType: false,
9 | });
10 | };
11 |
--------------------------------------------------------------------------------
/db/migrate/20171025125908_add_timestamp_to_notebooks.rb:
--------------------------------------------------------------------------------
1 | class AddTimestampToNotebooks < ActiveRecord::Migration[5.1]
2 | def change
3 | add_column :notebooks, :created_at, :datetime, null: false
4 | add_column :notebooks, :updated_at, :datetime, null: false
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/config/initializers/application_controller_renderer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # ActiveSupport::Reloader.to_prepare do
4 | # ApplicationController.renderer.defaults.merge!(
5 | # http_host: 'example.org',
6 | # https: false
7 | # )
8 | # end
9 |
--------------------------------------------------------------------------------
/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Specify a serializer for the signed and encrypted cookie jars.
4 | # Valid options are :json, :marshal, and :hybrid.
5 | Rails.application.config.action_dispatch.cookies_serializer = :json
6 |
--------------------------------------------------------------------------------
/app/views/api/notes/_note.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.id note.id
2 | json.title note.title
3 | json.body note.body
4 | json.bodyPlain note.body_plain
5 | json.notebookId note.notebook_id
6 | json.tagIds note.tag_ids
7 | json.flagId note.flag_id
8 | json.createdAt note.created_at
9 | json.updatedAt note.updated_at
10 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | load File.expand_path('../spring', __FILE__)
4 | rescue LoadError => e
5 | raise unless e.message.include?('spring')
6 | end
7 | APP_PATH = File.expand_path('../config/application', __dir__)
8 | require_relative '../config/boot'
9 | require 'rails/commands'
10 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/models/notebook.rb:
--------------------------------------------------------------------------------
1 | class Notebook < ApplicationRecord
2 | validates :title, :user_id, presence: true
3 | validates :title, uniqueness: { scope: :user_id, case_sensitive: false, message: "title must be unique" }
4 |
5 | belongs_to :user
6 | has_many :notes, dependent: :destroy
7 | has_many :tags, through: :notes
8 | end
9 |
--------------------------------------------------------------------------------
/db/migrate/20171025125057_create_notebooks.rb:
--------------------------------------------------------------------------------
1 | class CreateNotebooks < ActiveRecord::Migration[5.1]
2 | def change
3 | create_table :notebooks do |t|
4 | t.string :title, null: false
5 | t.integer :user_id, null: false
6 |
7 | t.timestamp
8 | end
9 | add_index :notebooks, :user_id
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../../config/environment', __FILE__)
2 | require 'rails/test_help'
3 |
4 | class ActiveSupport::TestCase
5 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
6 | fixtures :all
7 |
8 | # Add more helper methods to be used by all tests here...
9 | end
10 |
--------------------------------------------------------------------------------
/app/controllers/api/all_items_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::AllItemsController < ApplicationController
2 | def index
3 | @notebooks = current_user.notebooks.includes(:notes)
4 | @notes = current_user.notes.includes(:tags, :flag)
5 | @tags = current_user.tags.includes(:notes)
6 | @flags = current_user.flags.includes(:notes)
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/db/migrate/20171031215151_add_attachment_image_to_notes.rb:
--------------------------------------------------------------------------------
1 | class AddAttachmentImageToNotes < ActiveRecord::Migration[4.2]
2 | def self.up
3 | create_table :photos do |t|
4 | t.attachment :image
5 | t.integer :note_id
6 | t.timestamp
7 | end
8 | end
9 |
10 | def self.down
11 | drop_table :photos
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/loading_page.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .loading-page {
4 | height: 100%;
5 | width: 100%;
6 | background: $light-grey;
7 | display: flex;
8 | flex-direction: column;
9 | justify-content: center;
10 | align-items: center;
11 | }
12 |
13 | .loading-page h1 {
14 | color: $darker-grey;
15 | font-size: 30px;
16 | }
17 |
--------------------------------------------------------------------------------
/bin/yarn:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | VENDOR_PATH = File.expand_path('..', __dir__)
3 | Dir.chdir(VENDOR_PATH) do
4 | begin
5 | exec "yarnpkg #{ARGV.join(" ")}"
6 | rescue Errno::ENOENT
7 | $stderr.puts "Yarn executable was not detected in the system."
8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
9 | exit 1
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/frontend/util/notebook_api_util.js:
--------------------------------------------------------------------------------
1 |
2 | export const createNotebook = (notebook) => {
3 | return $.ajax({
4 | url: 'api/notebooks',
5 | method: 'post',
6 | data: { notebook },
7 | });
8 | };
9 |
10 | export const deleteNotebook = (notebookId) => {
11 | return $.ajax({
12 | url: `api/notebooks/${notebookId}`,
13 | method: 'delete',
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/db/migrate/20171214024813_create_flags.rb:
--------------------------------------------------------------------------------
1 | class CreateFlags < ActiveRecord::Migration[5.1]
2 | def change
3 | create_table :flags do |t|
4 | t.integer :place_id, null: false
5 | t.string :title, null: false
6 | t.integer :user_id, null: false
7 | t.timestamp
8 | end
9 | add_index :flags, [:place_id, :user_id], unique: true
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/frontend/components/sidemenu/tag_details.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const TagDetails = (props) => {
4 | return (
5 |
7 | {`${props.item.title}`}
8 | {props.item.noteIds.length}
9 |
10 | );
11 | };
12 |
13 | export default TagDetails;
14 |
--------------------------------------------------------------------------------
/frontend/store/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import logger from 'redux-logger';
4 | import RootReducer from '../reducers/root_reducer';
5 |
6 | const configureStore = (preloadedState = {}) => {
7 | return createStore(RootReducer, preloadedState, applyMiddleware(thunk));
8 | };
9 |
10 | export default configureStore;
11 |
--------------------------------------------------------------------------------
/db/migrate/20171023223343_create_users.rb:
--------------------------------------------------------------------------------
1 | class CreateUsers < ActiveRecord::Migration[5.1]
2 | def change
3 | create_table :users do |t|
4 | t.string :email, null: false
5 | t.string :session_token, null: false
6 | t.string :password_digest, null: false
7 | end
8 |
9 | add_index :users, :email, unique: true
10 | add_index :users, :session_token
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/frontend/reducers/errors/tag_errors_reducer.js:
--------------------------------------------------------------------------------
1 | import { RECEIVE_TAG_ERRORS } from '../../actions/tag_actions';
2 |
3 | const TagErrorsReducer = (oldState = [], action) => {
4 | switch (action.type) {
5 | case RECEIVE_TAG_ERRORS:
6 | return action.errors === undefined ? [] : action.errors;
7 | default:
8 | return oldState;
9 | }
10 | };
11 |
12 | export default TagErrorsReducer;
13 |
--------------------------------------------------------------------------------
/frontend/reducers/errors/flag_errors_reducer.js:
--------------------------------------------------------------------------------
1 | import { RECEIVE_FLAG_ERRORS } from '../../actions/flag_actions';
2 |
3 | const FlagErrorsReducer = (oldState = [], action) => {
4 | switch (action.type) {
5 | case RECEIVE_FLAG_ERRORS:
6 | return action.errors === undefined ? [] : action.errors;
7 | default:
8 | return oldState;
9 | }
10 | };
11 |
12 | export default FlagErrorsReducer;
13 |
--------------------------------------------------------------------------------
/frontend/reducers/errors/note_errors_reducer.js:
--------------------------------------------------------------------------------
1 | import { RECEIVE_NOTE_ERRORS } from '../../actions/note_actions';
2 |
3 | const NoteErrorsReducer = (oldState = [], action) => {
4 | switch (action.type) {
5 | case RECEIVE_NOTE_ERRORS:
6 | return action.errors === undefined ? [] : action.errors;
7 | default:
8 | return oldState;
9 | }
10 | };
11 |
12 | export default NoteErrorsReducer;
13 |
--------------------------------------------------------------------------------
/app/controllers/api/photos_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::PhotosController < ApplicationController
2 | def create
3 | @photo = current_user.photos.new(photo_params)
4 | if @photo.save
5 | render :show
6 | else
7 | render json: @photo.errors.full_messages, status: 422
8 | end
9 | end
10 |
11 | def photo_params
12 | params.require(:photo).permit(:image)
13 | end
14 |
15 | end
16 |
--------------------------------------------------------------------------------
/frontend/util/debounce.js:
--------------------------------------------------------------------------------
1 | export const debounce = (func, delay) => {
2 | let timeoutFunc;
3 | return (callImmediately) => {
4 | const laterFunc = () => {
5 | timeoutFunc = null;
6 | if (!callImmediately) func();
7 | }
8 | window.clearTimeout(timeoutFunc);
9 | timeoutFunc = window.setTimeout(laterFunc, delay);
10 | if (callImmediately || !timeoutFunc) func();
11 | };
12 | };
13 |
--------------------------------------------------------------------------------
/db/migrate/20171026213206_create_notes.rb:
--------------------------------------------------------------------------------
1 | class CreateNotes < ActiveRecord::Migration[5.1]
2 | def change
3 | create_table :notes do |t|
4 | t.string :title, null: false
5 | t.text :body
6 | t.integer :user_id, null: false
7 | t.integer :notebook_id, null: false
8 | t.timestamps
9 | end
10 | add_index :notes, :user_id
11 | add_index :notes, :notebook_id
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/frontend/reducers/errors/notebook_errors_reducer.js:
--------------------------------------------------------------------------------
1 | import { RECEIVE_NOTEBOOK_ERRORS } from '../../actions/notebook_actions';
2 |
3 | const NotebookErrorsReducer = (oldState = [], action) => {
4 | switch (action.type) {
5 | case RECEIVE_NOTEBOOK_ERRORS:
6 | return action.errors === undefined ? [] : action.errors;
7 | default:
8 | return oldState;
9 | }
10 | };
11 |
12 | export default NotebookErrorsReducer;
13 |
--------------------------------------------------------------------------------
/app/assets/javascripts/cable.js:
--------------------------------------------------------------------------------
1 | // Action Cable provides the framework to deal with WebSockets in Rails.
2 | // You can generate new channels where WebSocket features live using the `rails generate channel` command.
3 | //
4 | //= require action_cable
5 | //= require_self
6 | //= require_tree ./channels
7 |
8 | (function() {
9 | this.App || (this.App = {});
10 |
11 | App.cable = ActionCable.createConsumer();
12 |
13 | }).call(this);
14 |
--------------------------------------------------------------------------------
/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/frontend/reducers/errors/session_errors_reducer.js:
--------------------------------------------------------------------------------
1 | import { RECEIVE_USER_ERRORS } from '../../actions/session_actions';
2 |
3 | const initialState = [];
4 | const SessionErrorsReducer = (oldState = initialState, action) => {
5 | switch (action.type) {
6 | case RECEIVE_USER_ERRORS:
7 | return action.errors === undefined ? [] : action.errors;
8 | default:
9 | return oldState;
10 | }
11 | };
12 |
13 | export default SessionErrorsReducer;
14 |
--------------------------------------------------------------------------------
/app/models/tag.rb:
--------------------------------------------------------------------------------
1 | class Tag < ApplicationRecord
2 | validates :title,
3 | presence: true,
4 | uniqueness: {
5 | scope: :user_id,
6 | case_sensitive: false,
7 | message: "tag already exists"
8 | },
9 | length: {
10 | maximum: 40,
11 | message: "40 character limit"
12 | }
13 |
14 | belongs_to :user
15 | has_many :taggings, dependent: :destroy, inverse_of: :tag
16 | has_many :notes, through: :taggings
17 | end
18 |
--------------------------------------------------------------------------------
/frontend/reducers/root_reducer.js:
--------------------------------------------------------------------------------
1 | import EntitiesReducer from './entities_reducer';
2 | import ErrorsReducer from './errors_reducer';
3 | import UIReducer from './ui_reducer';
4 | import SessionReducer from './session_reducer';
5 | import { combineReducers } from 'redux';
6 |
7 | const RootReducer = combineReducers({
8 | entities: EntitiesReducer,
9 | errors: ErrorsReducer,
10 | session: SessionReducer,
11 | ui: UIReducer,
12 | });
13 |
14 | export default RootReducer;
15 |
--------------------------------------------------------------------------------
/frontend/util/session_api_util.js:
--------------------------------------------------------------------------------
1 | export const signup = (user) => {
2 | return $.ajax({
3 | url: 'api/users',
4 | method: 'post',
5 | data: { user }
6 | });
7 | };
8 |
9 | export const login = (user) => {
10 | return $.ajax({
11 | url: 'api/session',
12 | method: 'post',
13 | data: { user }
14 | });
15 | };
16 |
17 | export const logout = () => {
18 | return $.ajax({
19 | url: 'api/session',
20 | method: 'delete',
21 | });
22 | };
23 |
--------------------------------------------------------------------------------
/app/controllers/api/users_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::UsersController < ApplicationController
2 | # TODO render json stuff
3 | def create
4 | @user = User.new(user_params)
5 | if @user.save
6 | login(@user)
7 | render partial: 'user', locals: { user: @user }
8 | else
9 | render json: @user.errors.full_messages, status: 422
10 | end
11 | end
12 |
13 | def user_params
14 | params.require(:user).permit(:email, :password)
15 | end
16 |
17 | end
18 |
--------------------------------------------------------------------------------
/frontend/components/sidemenu/notebook_details.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const NotebookDetails = (props) => {
4 | return (
5 |
7 | {props.item.title}
8 | {new Date(props.item.updatedAt).toDateString()}
9 | {`${props.item.noteIds.length} ${props.notePluralized}`}
10 |
11 | )
12 | }
13 |
14 | export default NotebookDetails;
15 |
--------------------------------------------------------------------------------
/app/models/note.rb:
--------------------------------------------------------------------------------
1 | class Note < ApplicationRecord
2 | validates :title,
3 | presence: true,
4 | uniqueness: {
5 | scope: :notebook_id,
6 | case_sensitive: false,
7 | message: "note already exists in this notebook"
8 | }
9 |
10 | belongs_to :notebook
11 |
12 | has_one :user, through: :notebook
13 |
14 | has_many :taggings, dependent: :destroy, inverse_of: :note
15 | has_many :tags, through: :taggings
16 |
17 | belongs_to :flag, optional: true
18 | end
19 |
--------------------------------------------------------------------------------
/frontend/util/tag_api_util.js:
--------------------------------------------------------------------------------
1 | var snakeCase = require('snake-case');
2 |
3 | export const createTag = (tag) => {
4 | const snakeCaseTag = {};
5 | Object.keys(tag).forEach((tagParam) => {snakeCaseTag[snakeCase(tagParam)] = tag[tagParam];});
6 | return $.ajax({
7 | url: 'api/tags',
8 | method: 'post',
9 | data: { tag: snakeCaseTag }
10 | });
11 | };
12 |
13 | export const deleteTag = (tagId) => {
14 | return $.ajax({
15 | url: `api/tags/${tagId}`,
16 | method: 'delete',
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/frontend/components/sidemenu/sidemenu_heading.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const SidemenuHeading = (props) => {
4 | return (
5 |
6 | {props.itemType.toUpperCase().concat("s")}
7 |
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default SidemenuHeading;
17 |
--------------------------------------------------------------------------------
/frontend/util/flag_api_util.js:
--------------------------------------------------------------------------------
1 | var snakeCase = require('snake-case');
2 |
3 | export const createFlag = (flag) => {
4 | const snakeCaseFlag = {};
5 | Object.keys(flag).forEach((flagParam) => {snakeCaseFlag[snakeCase(flagParam)] = flag[flagParam];});
6 |
7 | return $.ajax({
8 | url: 'api/flags',
9 | method: 'post',
10 | data: {flag: snakeCaseFlag},
11 | });
12 | };
13 |
14 | export const deleteFlag = (flagId) => {
15 | return $.ajax({
16 | url: `api/flags/${flagId}`,
17 | method: `delete`,
18 | });
19 | };
20 |
--------------------------------------------------------------------------------
/frontend/reducers/session_reducer.js:
--------------------------------------------------------------------------------
1 | import { RECEIVE_CURRENT_USER } from '../actions/session_actions';
2 | import { merge } from 'lodash';
3 |
4 | const nullUser = { currentUser: null };
5 |
6 | const SessionReducer = (oldState = nullUser, action) => {
7 | let newState;
8 | switch(action.type) {
9 | case RECEIVE_CURRENT_USER:
10 | newState = merge({}, oldState);
11 | newState.currentUser = action.currentUser;
12 | return newState;
13 | default:
14 | return oldState;
15 | }
16 | };
17 |
18 | export default SessionReducer;
19 |
--------------------------------------------------------------------------------
/app/controllers/api/tags_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::TagsController < ApplicationController
2 |
3 | def create
4 | @tag = current_user.tags.new(tag_params)
5 | if @tag.save
6 | render :show
7 | else
8 | render json: @tag.errors.messages.values.flatten, status: 422
9 | end
10 | end
11 |
12 | def destroy
13 | @tag = current_user.tags.includes(:notes).find(params[:id])
14 | render :show
15 | @tag.destroy!
16 | end
17 |
18 | def tag_params
19 | params.require(:tag).permit(:title, note_ids: [])
20 | end
21 |
22 | end
23 |
--------------------------------------------------------------------------------
/frontend/reducers/entities_reducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import NotebooksReducer from './entities/notebooks_reducer';
3 | import NotesReducer from './entities/notes_reducer';
4 | import TagsReducer from './entities/tags_reducer';
5 | import FlagsReducer from './entities/flags_reducer';
6 | import MarkersReducer from './entities/markers_reducer';
7 |
8 | export default combineReducers({
9 | notebooks: NotebooksReducer,
10 | notes: NotesReducer,
11 | tags: TagsReducer,
12 | flags: FlagsReducer,
13 | markers: MarkersReducer,
14 | });
15 |
--------------------------------------------------------------------------------
/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json]
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/bin/spring:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | # This file loads spring without using Bundler, in order to be fast.
4 | # It gets overwritten when you run the `spring binstub` command.
5 |
6 | unless defined?(Spring)
7 | require 'rubygems'
8 | require 'bundler'
9 |
10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read)
11 | spring = lockfile.specs.detect { |spec| spec.name == "spring" }
12 | if spring
13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
14 | gem 'spring', spring.version
15 | require 'spring/binstub'
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/controllers/api/flags_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::FlagsController < ApplicationController
2 | def create
3 | @flag = current_user.flags.new(flag_params)
4 | if @flag.save
5 | render :show
6 | else
7 | render json: @flag.errors.messages.values.flatten, status: 422
8 | end
9 | end
10 |
11 | def destroy
12 | @flag = current_user.flags.includes(:notes).find(params[:id])
13 | render :show
14 | @flag.destroy!
15 | end
16 |
17 | def flag_params
18 | params.require(:flag).permit(:title, :place_id, :lat, :lng, :formatted_address)
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/app/controllers/api/notebooks_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::NotebooksController < ApplicationController
2 |
3 | def create
4 | @notebook = current_user.notebooks.new(notebook_params)
5 | if @notebook.save
6 | render :show
7 | else
8 | render json: @notebook.errors.messages.values.flatten, status: 422
9 | end
10 | end
11 |
12 | def destroy
13 | @notebook = current_user.notebooks.includes(:notes => :tags).find(params[:id])
14 | render :show
15 | @notebook.destroy!
16 | end
17 |
18 | def notebook_params
19 | params.require(:notebook).permit(:title)
20 | end
21 |
22 | end
23 |
--------------------------------------------------------------------------------
/frontend/actions/entity_actions.js:
--------------------------------------------------------------------------------
1 | import * as EntityUtil from '../util/entity_api_util';
2 |
3 | export const RECEIVE_ALL_ENTITIES = "RECEIVE_ALL_ENTITIES";
4 |
5 | export const receiveAllEntities = (entitiesRes) => {
6 | return {
7 | type: RECEIVE_ALL_ENTITIES,
8 | notebooks: entitiesRes.notebooks,
9 | notes: entitiesRes.notes,
10 | tags: entitiesRes.tags,
11 | flags: entitiesRes.flags,
12 | };
13 | };
14 |
15 | export const fetchAll = () => {
16 | return (dispatch) => {
17 | return EntityUtil.fetchAll().then((entitiesRes) => dispatch(receiveAllEntities(entitiesRes)))
18 | };
19 | };
20 |
--------------------------------------------------------------------------------
/frontend/components/sidemenu/sidemenu_index_item_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import SidemenuIndexItem from './sidemenu_index_item';
3 | import { toggleDeleteForm, toggleSidemenu } from '../../actions/ui_actions';
4 | import { withRouter } from 'react-router-dom';
5 |
6 | const mapDispatchToProps = (dispatch, ownProps) => {
7 | return {
8 | toggleSidemenu: () => dispatch(toggleSidemenu()),
9 | toggleDeleteForm: (id) => dispatch(toggleDeleteForm({id, type: ownProps.itemType})),
10 | };
11 | };
12 |
13 | export default withRouter(connect(null, mapDispatchToProps)(SidemenuIndexItem));
14 |
--------------------------------------------------------------------------------
/app/controllers/api/sessions_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::SessionsController < ApplicationController
2 | def create
3 | @user = User.find_by_credentials(params[:user][:email], params[:user][:password])
4 | if @user
5 | login(@user)
6 | render partial: 'api/users/user', locals: { user: @user }
7 | else
8 | render json: ["Invalid username/password combination"], status: 404
9 | end
10 | end
11 |
12 | def destroy
13 | if current_user
14 | logout
15 | render json: {}
16 | else
17 | render json: ['Not logged in'], status: 404
18 | end
19 | end
20 |
21 |
22 | end
23 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | protect_from_forgery with: :exception
3 |
4 | helper_method :current_user, :logged_in?
5 |
6 | def login(user)
7 | session[:session_token] = user.reset_session_token!
8 | @current_user = user
9 | end
10 |
11 | def current_user
12 | @current_user ||= User.find_by(session_token: session[:session_token])
13 | end
14 |
15 | def logged_in?
16 | !!current_user
17 | end
18 |
19 | def logout
20 | current_user.reset_session_token!
21 | session[:session_token] = nil
22 | end
23 |
24 | end
25 |
--------------------------------------------------------------------------------
/app/views/api/notes/update.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.note do
2 | json.partial! 'note', note: @note
3 | end
4 |
5 | json.notebooks do
6 | @notebooks.each do |notebook|
7 | json.set! notebook.id do
8 | json.partial! 'api/notebooks/notebook', notebook: notebook
9 | end
10 | end
11 | end
12 |
13 | json.tags do
14 | @tags.each do |tag|
15 | json.set! tag.id do
16 | json.partial! 'api/tags/tag', tag: tag
17 | end
18 | end
19 | end
20 |
21 | json.flags do
22 | @flags.each do |flag|
23 | json.set! flag.id do
24 | json.partial! 'api/flags/flag', flag: flag
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/frontend/components/session/auth_page.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SessionForm from './session_form_container';
3 | import { Route } from 'react-router-dom';
4 |
5 | class AuthPage extends React.Component {
6 |
7 | render() {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | }
18 | }
19 |
20 | export default AuthPage;
21 |
--------------------------------------------------------------------------------
/frontend/components/editor/tags.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Tags = (props) => {
4 | return (
5 |
6 |
7 | Add Tags:
8 |
14 |
15 |
16 |
19 |
20 | );
21 | };
22 |
23 | export default Tags;
24 |
--------------------------------------------------------------------------------
/frontend/reducers/errors_reducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import SessionErrorsReducer from './errors/session_errors_reducer';
3 | import NotebookErrorsReducer from './errors/notebook_errors_reducer';
4 | import NoteErrorsReducer from './errors/note_errors_reducer';
5 | import TagErrorsReducer from './errors/tag_errors_reducer';
6 | import FlagErrorsReducer from './errors/flag_errors_reducer';
7 |
8 | export default combineReducers({
9 | sessionErrors: SessionErrorsReducer,
10 | notebookErrors: NotebookErrorsReducer,
11 | noteErrors: NoteErrorsReducer,
12 | tagErrors: TagErrorsReducer,
13 | flagErrors: FlagErrorsReducer,
14 | });
15 |
--------------------------------------------------------------------------------
/frontend/components/session/logout_form_container.js:
--------------------------------------------------------------------------------
1 | import LogoutForm from './logout_form';
2 | import { connect } from 'react-redux';
3 | import { logout } from '../../actions/session_actions';
4 | import { toggleModal } from '../../actions/ui_actions';
5 |
6 | const mapStateToProps = (state, ownProps) => {
7 | return {
8 | logoutForm: state.ui.logoutForm,
9 | }
10 | }
11 |
12 | const mapDispatchToProps = (dispatch, ownProps) => {
13 | return {
14 | logout: () => dispatch(logout()),
15 | toggleLogoutForm: () => dispatch(toggleModal("logoutForm")),
16 | };
17 | };
18 |
19 | export default connect(mapStateToProps, mapDispatchToProps)(LogoutForm);
20 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
3 |
4 | root to: 'static_pages#root'
5 | namespace :api, defaults: { format: :json } do
6 | resource :session, only: [:create, :destroy]
7 | resources :users, only: [:create, :show]
8 | resources :notebooks, only: [:create, :destroy]
9 | resources :notes, only: [:create, :update, :destroy]
10 | resources :tags, only: [:create, :destroy]
11 | resources :photos, only: [:create]
12 | resources :flags, only: [:create, :destroy]
13 | resources :all_items, only: [:index]
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/frontend/omninote.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import configureStore from './store/store';
4 | import Root from './components/root';
5 | import * as NoteActions from './actions/note_actions';
6 |
7 | document.addEventListener('DOMContentLoaded', () => {
8 | const root = document.getElementById('root');
9 | let store;
10 | if (window.currentUser) {
11 | const preloadedState = { session: { currentUser: window.currentUser } };
12 | delete window.currentUser;
13 | store = configureStore(preloadedState);
14 | } else {
15 | store = configureStore();
16 | }
17 | ReactDOM.render(, root);
18 | });
19 |
--------------------------------------------------------------------------------
/frontend/components/note_index/all_notes_index_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import NoteIndex from './note_index';
3 | import { sortItems } from '../../util/sorters';
4 | import { withRouter } from 'react-router-dom';
5 |
6 | const mapStateToProps = (state, ownProps) => {
7 | const noteOrder = state.ui.noteOrder;
8 | const fullEditor = state.ui.fullEditor;
9 | const noteIndexHeader = "All Notes";
10 | const notes = sortItems(Object.values(state.entities.notes), noteOrder);
11 | return {
12 | noteIndexHeader,
13 | notes,
14 | noteOrder,
15 | fullEditor,
16 | };
17 | };
18 |
19 | export default withRouter(connect(mapStateToProps, null)(NoteIndex));
20 |
--------------------------------------------------------------------------------
/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = '1.0'
5 |
6 | # Add additional assets to the asset load path.
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 | # Add Yarn node_modules folder to the asset load path.
9 | Rails.application.config.assets.paths << Rails.root.join('node_modules')
10 |
11 | # Precompile additional assets.
12 | # application.js, application.css, and all non-JS/CSS in the app/assets
13 | # folder are already added.
14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css )
15 |
--------------------------------------------------------------------------------
/app/views/api/all_items/index.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.notebooks do
2 | @notebooks.each do |notebook|
3 | json.set! notebook.id do
4 | json.partial! 'api/notebooks/notebook', notebook: notebook
5 | end
6 | end
7 | end
8 |
9 | json.notes do
10 | @notes.each do |note|
11 | json.set! note.id do
12 | json.partial! 'api/notes/note', note: note
13 | end
14 | end
15 | end
16 |
17 | json.tags do
18 | @tags.each do |tag|
19 | json.set! tag.id do
20 | json.partial! 'api/tags/tag', tag: tag
21 | end
22 | end
23 | end
24 |
25 | json.flags do
26 | @flags.each do |flag|
27 | json.set! flag.id do
28 | json.partial! 'api/flags/flag', flag: flag
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite version 3.x
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem 'sqlite3'
6 | #
7 | default: &default
8 | adapter: postgresql
9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
10 | timeout: 5000
11 | encoding: unicode
12 |
13 |
14 | development:
15 | <<: *default
16 | database: omninote_development
17 |
18 | # Warning: The database defined as "test" will be erased and
19 | # re-generated from your development database when you run "rake".
20 | # Do not set this db to the same as development or production.
21 | test:
22 | <<: *default
23 | database: omninote_test
24 |
25 | production:
26 | <<: *default
27 | database: omninote_production
28 |
--------------------------------------------------------------------------------
/frontend/components/map_view/map_container.js:
--------------------------------------------------------------------------------
1 | import Map from './map';
2 | import { connect } from 'react-redux';
3 | import { withRouter } from 'react-router-dom';
4 | import { receiveFlags } from '../../actions/flag_actions';
5 |
6 | const mapStateToProps = (state, ownProps) => {
7 | const flags = Object.values(state.entities.flags);
8 | const notes = Object.values(state.entities.notes);
9 | return {
10 | flags: flags,
11 | notes: notes,
12 | };
13 | };
14 |
15 | const mapDispatchToProps = (dispatch, ownProps) => {
16 | return {
17 | setMarkers: (flags, googleMap, infoWindow, notes) => dispatch(receiveFlags(flags, googleMap, infoWindow, notes)),
18 | };
19 | };
20 |
21 | export default connect(mapStateToProps, mapDispatchToProps)(Map);
22 |
--------------------------------------------------------------------------------
/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's
5 | // vendor/assets/javascripts directory can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // compiled file. JavaScript code in this file should be added after the last require_* statement.
9 | //
10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11 | // about supported directives.
12 | //
13 | //= require rails-ujs
14 | //= require_tree .
15 | //= require jquery
16 | //= require jquery_ujs
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile '~/.gitignore_global'
6 |
7 | # Ignore bundler config.
8 | /.bundle
9 |
10 | # Ignore the default SQLite database.
11 | /db/*.sqlite3
12 | /db/*.sqlite3-journal
13 |
14 | # Ignore all logfiles and tempfiles.
15 | /log/*
16 | /tmp/*
17 | !/log/.keep
18 | !/tmp/.keep
19 |
20 | /node_modules
21 | /yarn-error.log
22 |
23 | .byebug_history
24 |
25 | node_modules/
26 | bundle.js
27 | bundle.js.map
28 | .byebug_history
29 | .DS_Store
30 | npm-debug.log
31 |
32 | # Ignore application configuration
33 | /config/application.yml
34 |
--------------------------------------------------------------------------------
/frontend/components/map_view/map_view_container.js:
--------------------------------------------------------------------------------
1 | import MapView from './map_view';
2 | import { connect } from 'react-redux';
3 | import { withRouter } from 'react-router-dom';
4 | import { toggleModal, toggleDeleteForm } from '../../actions/ui_actions';
5 | import { sortItems } from '../../util/sorters';
6 |
7 | const mapStateToProps = (state, ownProps) => {
8 | return {
9 | flags: sortItems(Object.values(state.entities.flags), 4),
10 | mapViewOpen: state.ui.mapView,
11 | };
12 | };
13 |
14 | const mapDispatchToProps = (dispatch, ownProps) => {
15 | return {
16 | toggleMapView: () => dispatch(toggleModal('mapView')),
17 | toggleDeleteForm: (id) => dispatch(toggleDeleteForm({id, type: "flag"}))
18 | };
19 | };
20 |
21 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MapView));
22 |
--------------------------------------------------------------------------------
/frontend/util/quill_configs.js:
--------------------------------------------------------------------------------
1 |
2 | export const quillModules = {
3 | toolbar: [
4 | ['bold', 'italic', 'underline', 'strike'],
5 | ['blockquote', 'code-block'],
6 | [{ 'list': 'ordered'}, { 'list': 'bullet' }],
7 | [{ 'script': 'sub'}, { 'script': 'super' }],
8 | [{ 'indent': '-1'}, { 'indent': '+1' }],
9 | [{ 'direction': 'rtl' }],
10 | [{ 'size': ['small', false, 'large', 'huge'] }],
11 | [{ 'color': [] }, { 'background': [] }],
12 | ['link'],
13 | [{ 'font': [] }],
14 | [{ 'align': [] }],
15 | ['clean']],
16 | };
17 |
18 | export const quillFormats = [
19 | 'header', 'font', 'size', 'bold', 'italic', 'underline', 'strike',
20 | 'blockquote', 'code-block', 'list', 'bullet', 'script', 'indent',
21 | 'color', 'background', 'align', 'clean', 'direction', 'image', 'link'
22 | ];
23 |
--------------------------------------------------------------------------------
/frontend/components/session/session_form_container.js:
--------------------------------------------------------------------------------
1 | import { login, signup, receiveUserErrors } from '../../actions/session_actions';
2 | import SessionForm from './session_form';
3 | import { connect } from 'react-redux';
4 | import { withRouter } from 'react-router-dom';
5 |
6 | const mapStateToProps = (state, ownProps) => {
7 | return {
8 | sessionErrors: state.errors.sessionErrors,
9 | };
10 | };
11 |
12 | const mapDispatchToProps = (dispatch, ownProps) => {
13 | const submitForm = ownProps.location.pathname === "/login" ? login : signup;
14 | return {
15 | clearUserErrors: () => dispatch(receiveUserErrors([])),
16 | submitForm: (user) => dispatch(submitForm(user)),
17 | demoLogin: (user) => dispatch(login(user)),
18 | };
19 | };
20 |
21 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(SessionForm));
22 |
--------------------------------------------------------------------------------
/frontend/components/note_index/note_index_item_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import NoteIndexItem from './note_index_item';
3 | import { withRouter } from 'react-router-dom';
4 | import { toggleDeleteForm } from '../../actions/ui_actions';
5 |
6 | const mapStateToProps = (state, ownProps) => {
7 | const bodySnippet = ownProps.note.bodyPlain.length < 100 ? ownProps.note.bodyPlain : ownProps.note.bodyPlain.concat("...");
8 | const notebook = state.entities.notebooks[ownProps.note.notebookId];
9 |
10 | return {
11 | notebook,
12 | bodySnippet
13 | };
14 | };
15 |
16 | const mapDispatchToProps = (dispatch, ownProps) => {
17 | return {
18 | toggleDeleteForm: (id) => dispatch(toggleDeleteForm({ id, type: "note"})),
19 | };
20 | };
21 |
22 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(NoteIndexItem));
23 |
--------------------------------------------------------------------------------
/frontend/components/note_index/tag_notes_index_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import NoteIndex from './note_index';
3 | import { sortItems } from '../../util/sorters';
4 | import { withRouter } from 'react-router-dom';
5 |
6 | const mapStateToProps = (state, ownProps) => {
7 | let notes = [];
8 | const noteOrder = state.ui.noteOrder;
9 | const fullEditor = state.ui.fullEditor;
10 | const tag = state.entities.tags[ownProps.match.params.tagId];
11 | const noteIndexHeader = `Tag: ${tag.title}`;
12 | const noteIds = tag.noteIds;
13 | noteIds.forEach((noteId) => notes.push(state.entities.notes[noteId]));
14 | notes = sortItems(notes, noteOrder);
15 | return {
16 | noteIndexHeader,
17 | notes,
18 | noteOrder,
19 | fullEditor,
20 | };
21 | };
22 |
23 | export default withRouter(connect(mapStateToProps, null)(NoteIndex));
24 |
--------------------------------------------------------------------------------
/bin/update:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'pathname'
3 | require 'fileutils'
4 | include FileUtils
5 |
6 | # path to your application root.
7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
8 |
9 | def system!(*args)
10 | system(*args) || abort("\n== Command #{args} failed ==")
11 | end
12 |
13 | chdir APP_ROOT do
14 | # This script is a way to update your development environment automatically.
15 | # Add necessary update steps to this file.
16 |
17 | puts '== Installing dependencies =='
18 | system! 'gem install bundler --conservative'
19 | system('bundle check') || system!('bundle install')
20 |
21 | puts "\n== Updating database =="
22 | system! 'bin/rails db:migrate'
23 |
24 | puts "\n== Removing old logs and tempfiles =="
25 | system! 'bin/rails log:clear tmp:clear'
26 |
27 | puts "\n== Restarting application server =="
28 | system! 'bin/rails restart'
29 | end
30 |
--------------------------------------------------------------------------------
/frontend/components/note_index/notebook_notes_index_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import NoteIndex from './note_index';
3 | import { sortItems } from '../../util/sorters';
4 | import { withRouter } from 'react-router-dom';
5 |
6 | const mapStateToProps = (state, ownProps) => {
7 | let notes = [];
8 | const noteOrder = state.ui.noteOrder;
9 | const fullEditor = state.ui.fullEditor;
10 | const notebook = state.entities.notebooks[ownProps.match.params.notebookId];
11 | const noteIndexHeader = `Notebook: ${notebook.title}`;
12 | const noteIds = notebook.noteIds;
13 | noteIds.forEach((noteId) => notes.push(state.entities.notes[noteId]));
14 | notes = sortItems(notes, noteOrder);
15 | return {
16 | noteIndexHeader,
17 | notes,
18 | noteOrder,
19 | fullEditor,
20 | };
21 | };
22 |
23 | export default withRouter(connect(mapStateToProps, null)(NoteIndex));
24 |
--------------------------------------------------------------------------------
/frontend/components/app_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import App from './app';
3 | import { fetchAll } from '../actions/entity_actions';
4 |
5 | const mapStateToProps = (state, ownProps) => {
6 | let notFound = false;
7 | if (ownProps.match.params.noteId && !state.entities.notes[ownProps.match.params.noteId] ||
8 | ownProps.match.params.notebookId && !state.entities.notebooks[ownProps.match.params.notebookId] ||
9 | ownProps.match.params.tagId && !state.entities.tags[ownProps.match.params.tagId]) {
10 | notFound = true;
11 | }
12 | return {
13 | initialState: state.entities.notes.initialState,
14 | itemType: state.ui.createForm.itemType,
15 | notFound,
16 | };
17 | };
18 |
19 | const mapDispatchToProps = (dispatch) => {
20 | return {
21 | fetchAll: () => dispatch(fetchAll()),
22 | };
23 | };
24 |
25 | export default connect(mapStateToProps, mapDispatchToProps)(App);
26 |
--------------------------------------------------------------------------------
/frontend/components/sidemenu/sidemenu_container.js:
--------------------------------------------------------------------------------
1 | import Sidemenu from './sidemenu';
2 | import { connect } from 'react-redux';
3 | import { withRouter } from 'react-router-dom';
4 | import { toggleCreateForm, toggleSidemenu } from '../../actions/ui_actions';
5 | import { sortItems } from '../../util/sorters';
6 |
7 | const mapStateToProps = (state, ownProps) => {
8 | return {
9 | items: sortItems(Object.values(state.entities[`${state.ui.sidemenuItemType}s`]), 4),
10 | sidemenu: state.ui.sidemenu,
11 | itemType: state.ui.sidemenuItemType,
12 | sidemenuOpen: ((state.ui.sidemenu === "sidemenu-open"))
13 | };
14 | };
15 |
16 | const mapDispatchToProps = (dispatch, ownProps) => {
17 | return {
18 | toggleSidemenu: () => dispatch(toggleSidemenu()),
19 | toggleCreateForm: (itemType) => dispatch(toggleCreateForm(itemType)),
20 | };
21 | };
22 |
23 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Sidemenu));
24 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/reset.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | html, body, div, span, h1, h2, h3, h4, h5, h6, p, blockquote, a, img,
4 | strong, ol, ul, li, form, label, table, caption, tr, th, td, article,
5 | aside, figure, figcaption, footer, header, nav, section, button, input, textarea {
6 | margin: 0;
7 | padding: 0;
8 | border: 0;
9 | font: inherit;
10 | font-size: 100%;
11 | list-style: none;
12 | font-family: 'Lato', sans-serif;
13 | letter-spacing: 1.5px;
14 | -webkit-appearance: none;
15 | text-decoration: none;
16 | color: $black;
17 | outline: none;
18 | background-color: transparent;
19 | }
20 |
21 | h1 {
22 | font-size: 60px;
23 | font-weight: 700;
24 | }
25 |
26 | h2 {
27 | font-size: 30px;
28 | font-weight: 700;
29 | }
30 |
31 | h3 {
32 | font-size: 20px;
33 | font-weight: 400;
34 | }
35 |
36 | h4 {
37 | font-size: 14px;
38 | font-weight: 200;
39 | }
40 |
41 | h5 {
42 | font-size: 10px;
43 | font-weight: 200;
44 | }
45 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative 'boot'
2 |
3 | require 'rails/all'
4 |
5 | # Require the gems listed in Gemfile, including any gems
6 | # you've limited to :test, :development, or :production.
7 | Bundler.require(*Rails.groups)
8 |
9 | module Omninote
10 | class Application < Rails::Application
11 | # Initialize configuration defaults for originally generated Rails version.
12 | config.load_defaults 5.1
13 |
14 | # Settings in config/environments/* take precedence over those specified here.
15 | # Application configuration should go into files in config/initializers
16 | # -- all .rb files in that directory are automatically loaded.
17 | config.paperclip_defaults = {
18 | :storage => :s3,
19 | :s3_credentials => {
20 | :bucket => ENV["s3_bucket"],
21 | :access_key_id => ENV["s3_access_key_id"],
22 | :secret_access_key => ENV["s3_secret_access_key"],
23 | :s3_region => ENV["s3_region"] }
24 | }
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # The following keys must be escaped otherwise they will not be retrieved by
20 | # the default I18n backend:
21 | #
22 | # true, false, on, off, yes, no
23 | #
24 | # Instead, surround them with single quotes.
25 | #
26 | # en:
27 | # 'true': 'foo'
28 | #
29 | # To learn more, please read the Rails Internationalization guide
30 | # available at http://guides.rubyonrails.org/i18n.html.
31 |
32 | en:
33 | hello: "Hello world"
34 |
--------------------------------------------------------------------------------
/frontend/components/editor/editor_lower_heading.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const EditorLowerHeading = (props) => {
4 | return (
5 |
6 |
13 |
14 |
15 |
{props.saved ? "Saved!" : ""}
16 |
Save
20 |
23 | {props.fullEditor ? "Close" : "Expand"}
24 |
25 |
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/frontend/components/note_index/note_order_options_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import NoteOrderOptionMenu from './note_order_option_menu';
3 | import { toggleModal, toggleNoteOrder } from '../../actions/ui_actions';
4 |
5 | const noteOrderOptions = [
6 | 'Date Updated (newest first) ',
7 | 'Date Created (newest first)',
8 | 'Date Updated (oldest first)',
9 | 'Date Created (oldest first)',
10 | 'Title (ascending)',
11 | 'Title (descending)'];
12 |
13 | const mapStateToProps = (state) => {
14 | return {
15 | noteOrder: state.ui.noteOrder,
16 | noteOrderDropdown: state.ui.noteOrderDropdown,
17 | options: noteOrderOptions,
18 | };
19 | };
20 |
21 | const mapDispatchToProps = (dispatch) => {
22 | return {
23 | toggleNoteOrderDropdown: () => dispatch(toggleModal("noteOrderDropdown")),
24 | toggleNoteOrder: (order) => dispatch(toggleNoteOrder(order)),
25 | };
26 | };
27 |
28 | export default connect(mapStateToProps, mapDispatchToProps)(NoteOrderOptionMenu);
29 |
--------------------------------------------------------------------------------
/frontend/actions/tag_actions.js:
--------------------------------------------------------------------------------
1 | import * as TagUtil from '../util/tag_api_util';
2 |
3 | export const RECEIVE_TAG = "RECEIVE_TAG";
4 | export const REMOVE_TAG = "REMOVE_TAG";
5 | export const RECEIVE_TAG_ERRORS = "RECEIVE_TAG_ERRORS";
6 |
7 | export const receiveTag = (tag) => {
8 | return {
9 | type: RECEIVE_TAG,
10 | tag,
11 | };
12 | };
13 |
14 | export const removeTag = (tag) => {
15 | return {
16 | type: REMOVE_TAG,
17 | tag,
18 | };
19 | };
20 |
21 | export const receiveTagErrors = (errors) => {
22 | return {
23 | type: RECEIVE_TAG_ERRORS,
24 | errors: errors.responseJSON,
25 | };
26 | };
27 |
28 | export const deleteTag = (tagId) => {
29 | return (dispatch) => {
30 | return TagUtil.deleteTag(tagId)
31 | .then((tag) => dispatch(removeTag(tag)));
32 | };
33 | };
34 |
35 | export const createTag = (tag) => {
36 | return (dispatch) => {
37 | return TagUtil.createTag(tag)
38 | .then((newTag) => dispatch(receiveTag(newTag)),
39 | (errors) => dispatch(receiveTagErrors(errors))
40 | );};
41 | };
42 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/notebook_index.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .notebook-index-item {
4 | display: flex;
5 | flex-direction: row;
6 | align-items: center;
7 | justify-content: space-between;
8 | padding: 10px 20px;
9 |
10 | &:hover {
11 | cursor: pointer;
12 | background-color: $light-green;
13 | transition: background-color 0.2s;
14 | li, h3 {
15 | color: white;
16 | transition: color 0.2s;
17 | }
18 | }
19 | }
20 |
21 |
22 | .notebook-details {
23 | width: 100%;
24 | display: flex;
25 | flex-direction: column;
26 | font-size: 12px;
27 | font-weight: 200;
28 | font-family: 'Roboto Slab', serif;
29 | overflow: hidden;
30 | }
31 |
32 | .notebook-details > li > h3 {
33 | overflow: hidden;
34 | white-space: nowrap;
35 | text-overflow: ellipsis;
36 | }
37 |
38 |
39 | .notebook-trash-icon {
40 | height: 20px;
41 | margin: 5px;
42 | width: auto;
43 | }
44 |
45 | .notebook-separator {
46 | border-bottom: 1px solid $grey;
47 | color: $dark-grey;
48 | font-size: 30px;
49 | padding: 10px 20px;
50 | }
51 |
--------------------------------------------------------------------------------
/frontend/components/note_index/notes_in_map_container.jsx:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import NoteIndex from './note_index';
3 | import { sortItems } from '../../util/sorters';
4 | import { withRouter } from 'react-router-dom';
5 |
6 | const mapStateToProps = (state, ownProps) => {
7 | const searchQuery = ownProps.match.params.flagIds.split(",").map((id) => parseInt(id));
8 | let notes = [];
9 | const noteIds = [];
10 | searchQuery.forEach((flagId) => {
11 | state.entities.flags[flagId].noteIds.forEach((noteId) => noteIds.push(noteId));
12 | });
13 | if (searchQuery !== "nonefound") {
14 | notes = sortItems(
15 | noteIds.map((noteId) => state.entities.notes[noteId]),
16 | noteOrder
17 | );
18 | }
19 | const noteOrder = state.ui.noteOrder;
20 | const fullEditor = state.ui.fullEditor;
21 | const noteIndexHeader = `Filtered notes by location`;
22 |
23 | return {
24 | noteIndexHeader,
25 | notes,
26 | noteOrder,
27 | fullEditor,
28 | };
29 | };
30 |
31 | export default withRouter(connect(mapStateToProps, null)(NoteIndex));
32 |
--------------------------------------------------------------------------------
/frontend/util/route_util.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { Route, Redirect, withRouter } from 'react-router-dom';
4 |
5 | const Auth = ({ component: Component, path, loggedIn }) => {
6 | return (
7 | (
8 | !loggedIn? (
9 |
10 | ) : (
11 |
12 | )
13 | )} />
14 | );
15 | };
16 |
17 | const Protected = ({component: Component, path, loggedIn}) => {
18 | return (
19 | (
20 | loggedIn ? (
21 |
22 | ) : (
23 |
24 | )
25 | )
26 | } />
27 | );
28 | };
29 |
30 | const mapStateToProps = (state) => {
31 | return {
32 | loggedIn: Boolean(state.session.currentUser),
33 | };
34 | };
35 |
36 | export const AuthRoute = withRouter(connect(mapStateToProps, null)(Auth));
37 | export const ProtectedRoute = withRouter(connect(mapStateToProps, null)(Protected));
38 |
--------------------------------------------------------------------------------
/frontend/components/editor/notebook_dropdown_container.js:
--------------------------------------------------------------------------------
1 | import NotebookDropdown from './notebook_dropdown';
2 | import { connect } from 'react-redux';
3 | import { toggleModal, toggleSelectedNotebook, toggleCreateForm } from '../../actions/ui_actions';
4 | import { sortItems } from '../../util/sorters';
5 | import { withRouter } from 'react-router-dom';
6 |
7 |
8 | const mapStateToProps = (state, ownProps) => {
9 | return {
10 | notes: state.entities.notes,
11 | selectedNotebook: state.ui.selectedNotebook,
12 | notebookDropdown: state.ui.notebookDropdown,
13 | allNotebooks: sortItems(Object.values(state.entities.notebooks), 4),
14 | };
15 | };
16 |
17 | const mapDispatchToProps = (dispatch, ownProps) => {
18 | return {
19 | toggleNotebookDropdown: () => dispatch(toggleModal("notebookDropdown")),
20 | toggleSelectedNotebook: (notebookId, clicked) => dispatch(toggleSelectedNotebook(notebookId, clicked)),
21 | toggleCreateForm: () => dispatch(toggleCreateForm("notebook")),
22 | };
23 | };
24 |
25 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(NotebookDropdown));
26 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'pathname'
3 | require 'fileutils'
4 | include FileUtils
5 |
6 | # path to your application root.
7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
8 |
9 | def system!(*args)
10 | system(*args) || abort("\n== Command #{args} failed ==")
11 | end
12 |
13 | chdir APP_ROOT do
14 | # This script is a starting point to setup your application.
15 | # Add necessary setup steps to this file.
16 |
17 | puts '== Installing dependencies =='
18 | system! 'gem install bundler --conservative'
19 | system('bundle check') || system!('bundle install')
20 |
21 | # Install JavaScript dependencies if using Yarn
22 | # system('bin/yarn')
23 |
24 |
25 | # puts "\n== Copying sample files =="
26 | # unless File.exist?('config/database.yml')
27 | # cp 'config/database.yml.sample', 'config/database.yml'
28 | # end
29 |
30 | puts "\n== Preparing database =="
31 | system! 'bin/rails db:setup'
32 |
33 | puts "\n== Removing old logs and tempfiles =="
34 | system! 'bin/rails log:clear tmp:clear'
35 |
36 | puts "\n== Restarting application server =="
37 | system! 'bin/rails restart'
38 | end
39 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 |
2 | const path = require('path');
3 | const webpack = require('webpack');
4 | var plugins = [];
5 | const devPlugins = [];
6 |
7 | const prodPlugins = [
8 | new webpack.DefinePlugin({
9 | 'process.env': {
10 | 'NODE_ENV': JSON.stringify('production'),
11 | },
12 | }),
13 | new webpack.optimize.UglifyJsPlugin({
14 | compress: {
15 | warnings: true,
16 | },
17 | })
18 | ];
19 |
20 | plugins = plugins.concat(
21 | process.env.NODE_ENV === 'production' ? prodPlugins : devPlugins
22 | );
23 |
24 | module.exports = {
25 | context: __dirname,
26 | entry: './frontend/omninote.jsx',
27 | output: {
28 | path: path.resolve(__dirname, 'app', 'assets', 'javascripts'),
29 | filename: 'bundle.js'
30 | },
31 | plugins: plugins,
32 | resolve: {
33 | extensions: ['.js', '.jsx', '*']
34 | },
35 | module: {
36 | loaders: [
37 | {
38 | test: /\.jsx?$/,
39 | exclude: /(node_modules|bower_components)/,
40 | loader: 'babel-loader',
41 | query: {
42 | presets: ['react', 'es2015']
43 | }
44 | }
45 | ]
46 | },
47 | devtool: 'source-map',
48 | };
49 |
--------------------------------------------------------------------------------
/frontend/util/note_api_util.js:
--------------------------------------------------------------------------------
1 | var snakeCase = require('snake-case');
2 |
3 | // export const fetchNotes = () => {
4 | // return $.ajax({
5 | // url: 'api/notes',
6 | // method: 'get',
7 | // });
8 | // };
9 | //
10 | // export const fetchNote = (noteId) => {
11 | // return $.ajax({
12 | // url: `api/notes/${noteId}`,
13 | // method: 'get',
14 | // });
15 | // };
16 |
17 | export const createNote = (note) => {
18 | const snakeCaseNote = {};
19 | Object.keys(note).forEach((noteParam) => {snakeCaseNote[snakeCase(noteParam)] = note[noteParam];});
20 | return $.ajax({
21 | url: `api/notes`,
22 | method: 'post',
23 | data: { note: snakeCaseNote }
24 | });
25 | };
26 |
27 | export const updateNote = (note) => {
28 | const snakeCaseNote = {};
29 | Object.keys(note).forEach((noteParam) => {snakeCaseNote[snakeCase(noteParam)] = note[noteParam];});
30 | return $.ajax({
31 | url: `api/notes/${note.id}`,
32 | method: 'patch',
33 | data: { note: snakeCaseNote }
34 | });
35 | };
36 |
37 | export const deleteNote = (noteId) => {
38 | return $.ajax({
39 | url: `api/notes/${noteId}`,
40 | method: 'delete',
41 | });
42 | };
43 |
--------------------------------------------------------------------------------
/frontend/components/note_index/filtered_notes_index_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import NoteIndex from './note_index';
3 | import { sortItems } from '../../util/sorters';
4 | import { withRouter } from 'react-router-dom';
5 |
6 | const mapStateToProps = (state, ownProps) => {
7 | let itemType;
8 | if (ownProps.match.params.notebookId) {
9 | itemType = 'notebook';
10 | } else if (ownProps.match.params.tagId) {
11 | itemType = 'tag';
12 | } else if (ownProps.match.params.flagId) {
13 | itemType = 'flag';
14 | }
15 |
16 | let notes = [];
17 | const noteOrder = state.ui.noteOrder;
18 | const fullEditor = state.ui.fullEditor;
19 | const item = state.entities[`${itemType}s`][ownProps.match.params[`${itemType}Id`]];
20 | const noteIndexHeader = `${itemType.toUpperCase()}: ${item.title}`;
21 | const noteIds = item.noteIds;
22 | noteIds.forEach((noteId) => notes.push(state.entities.notes[noteId]));
23 | notes = sortItems(notes, noteOrder);
24 | return {
25 | noteIndexHeader,
26 | notes,
27 | noteOrder,
28 | fullEditor,
29 | };
30 | };
31 |
32 | export default withRouter(connect(mapStateToProps, null)(NoteIndex));
33 |
--------------------------------------------------------------------------------
/frontend/components/sidenav/sidenav_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import SideNav from './sidenav';
3 | import { logout } from '../../actions/session_actions';
4 | import { withRouter } from 'react-router-dom';
5 | import { toggleModal, toggleSidemenu, toggleSidemenuItemType } from '../../actions/ui_actions';
6 |
7 | const mapStateToProps = (state) => {
8 | return {
9 | sidemenuItemType: state.ui.sidemenuItemType,
10 | sidemenuOpen: state.ui.sidemenu === "sidemenu-open",
11 | currentUser: state.session.currentUser.email,
12 | fullEditor: state.ui.fullEditor,
13 | };
14 | };
15 |
16 | const mapDispatchToProps = (dispatch) => {
17 | return {
18 | // dispatch actions to toggle UI slices of state
19 | logout: () => dispatch(logout()),
20 | toggleFullEditor: () => dispatch(toggleModal("fullEditor")),
21 | toggleSidemenuItemType: (itemType) => dispatch(toggleSidemenuItemType(itemType)),
22 | toggleSidemenu: () => dispatch(toggleSidemenu()),
23 | toggleLogoutForm: () => dispatch(toggleModal("logoutForm")),
24 | toggleMapView: () => dispatch(toggleModal('mapView')),
25 | };
26 | };
27 |
28 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(SideNav));
29 |
--------------------------------------------------------------------------------
/frontend/actions/notebook_actions.js:
--------------------------------------------------------------------------------
1 | import * as NotebookUtil from '../util/notebook_api_util';
2 |
3 | export const RECEIVE_NOTEBOOK = "RECEIVE_NOTEBOOK";
4 | export const REMOVE_NOTEBOOK = "REMOVE_NOTEBOOK";
5 | export const RECEIVE_NOTEBOOK_ERRORS = "RECEIVE_NOTEBOOK_ERRORS";
6 |
7 | export const receiveNotebook = (newNotebook) => {
8 | return {
9 | type: RECEIVE_NOTEBOOK,
10 | notebook: newNotebook,
11 | };
12 | };
13 |
14 | export const removeNotebook = (notebook) => {
15 | return {
16 | type: REMOVE_NOTEBOOK,
17 | notebook,
18 | };
19 | };
20 |
21 | export const receiveNotebookErrors = (errors) => {
22 | return {
23 | type: RECEIVE_NOTEBOOK_ERRORS,
24 | errors: errors.responseJSON,
25 | };
26 | };
27 |
28 | export const deleteNotebook = (notebookId) => {
29 | return (dispatch) => {
30 | return NotebookUtil.deleteNotebook(notebookId)
31 | .then((notebook) => dispatch(removeNotebook(notebook)));
32 | };
33 | };
34 |
35 | export const createNotebook = (notebook) => {
36 | return (dispatch) => {
37 | return NotebookUtil.createNotebook(notebook)
38 | .then((newNotebook) => dispatch(receiveNotebook(newNotebook)),
39 | (errors) => dispatch(receiveNotebookErrors(errors))
40 | );};
41 | };
42 |
--------------------------------------------------------------------------------
/frontend/components/root.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Provider } from 'react-redux';
3 | import { HashRouter, Switch, Route } from 'react-router-dom';
4 | import { AuthRoute, ProtectedRoute } from '../util/route_util';
5 | import DefaultPage from './default_page';
6 | import App from './app_container';
7 | import AuthPage from './session/auth_page';
8 |
9 | const Root = ({store}) => (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | );
28 |
29 | export default Root;
30 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/buttons.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 |
4 | button:hover, input[type="submit"]:hover, a:hover {
5 | cursor: pointer;
6 | }
7 |
8 |
9 | .button {
10 |
11 | color: white;
12 | padding: 5px 10px;
13 | margin: 10px;
14 | border-radius: 6px;
15 | width: 100%;
16 | height: 45px;
17 |
18 |
19 | &.small {
20 | height: 30px;
21 | width: 70%;
22 | padding: 3px;
23 | margin: 5px;
24 | font-size: 14px;
25 | }
26 |
27 | &.narrow {
28 | width: 100px;
29 | }
30 |
31 | &.disabled {
32 | pointer-events: none;
33 | opacity: 0.5;
34 | }
35 |
36 | &.tiny {
37 | width: 20px;
38 | height: 20px;
39 | font-size: 10px;
40 | padding: 2px;
41 | margin: 3px;
42 | }
43 |
44 | &.green {
45 | background: $light-green;
46 | &:hover {
47 | background: $dark-green;
48 | transition: background 0.2s;
49 | }
50 |
51 | }
52 |
53 | &.red {
54 | color: black;
55 | background: $grey;
56 | &:hover {
57 | color: white;
58 | background: $dark-grey;
59 | transition: background 0.2s;
60 | }
61 | }
62 |
63 | &.grey {
64 | color: black;
65 | background: $grey;
66 | &:hover {
67 | color: white;
68 | background: $dark-grey;
69 | transition: background 0.2s;
70 | }
71 |
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/tag_index.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .tag-index-item {
4 | display: flex;
5 | flex-direction: row;
6 | align-items: center;
7 | margin: 5px;
8 | flex-shrink: 2;
9 | }
10 |
11 | .tag-index-item:hover {
12 | .tag-trash-icon {
13 | visibility: visible;
14 | background: $light-green;
15 | cursor: pointer;
16 | transition: background 0.3s;
17 | }
18 |
19 | .tag-details {
20 | background: $light-green;
21 | transition: background 0.3s;
22 | p {
23 | color: white;
24 | }
25 | }
26 | }
27 |
28 | .tag-trash-icon {
29 | visibility: hidden;
30 | padding: 3px;
31 | border-radius: 5px;
32 | height: 20px;
33 | width: auto;
34 | }
35 |
36 | .tag-details {
37 | display: flex;
38 | flex-direction: row;
39 | justify-content: center;
40 | padding: 5px;
41 | margin: 0 5px;
42 | border-radius: 5px;
43 | font-size: 14px;
44 | overflow: hidden;
45 | text-overflow: ellipsis;
46 | white-space: nowrap;
47 | border: 1px solid $grey;
48 | background: $light-grey;
49 | color: $darker-grey;
50 |
51 | p:last-of-type {
52 | color: $light-green;
53 | }
54 | }
55 |
56 | .tag-separator {
57 | display: block;
58 | font-size: 20px;
59 | color: $dark-grey;
60 | width: 100%;
61 | margin-top: 25px;
62 | margin-bottom: 10px;
63 | }
64 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/note_order_dropdown.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .sort-option-div {
4 | width: 100%;
5 | min-height: 40px;
6 | display: flex;
7 | align-items: center;
8 | justify-content: flex-end;
9 | border-bottom: 1px solid $light-grey;
10 | padding-bottom: 5px;
11 | background: white;
12 |
13 | :hover {
14 | cursor: pointer;
15 | }
16 | }
17 |
18 | .sort-button {
19 | padding-right: 20px;
20 | position: relative;
21 | color: $darker-grey;
22 | }
23 |
24 | .order-dropdown {
25 | width: 250px;
26 | height: auto;
27 | top: 35px;
28 | right: 0;
29 | position: absolute;
30 | background: white;
31 | border: 2px solid $light-grey;
32 | display: flex;
33 | flex-direction: column;
34 | z-index: 45;
35 | }
36 |
37 | .order-dropdown-closed {
38 | display: none;
39 | }
40 |
41 | .order-dropdown-overlay {
42 | height: 100%;
43 | width: 100%;
44 | top: 0px;
45 | left: 0px;
46 | position: fixed;
47 | overflow: hidden;
48 | z-index: 44;
49 | }
50 |
51 | .order-dropdown-overlay-closed {
52 | display: none;
53 | }
54 |
55 | .note-order {
56 | padding: 5px;
57 | color: $darker-grey;
58 | text-align: left;
59 | }
60 |
61 | .note-order.selected {
62 | color: $light-green;
63 | font-weight: 700;
64 | }
65 |
66 | .note-order:hover, .note-order.selected:hover {
67 | background: $light-green;
68 | color: white;
69 | }
70 |
--------------------------------------------------------------------------------
/config/secrets.yml:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key is used for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 |
6 | # Make sure the secret is at least 30 characters and all random,
7 | # no regular words or you'll be exposed to dictionary attacks.
8 | # You can use `rails secret` to generate a secure secret key.
9 |
10 | # Make sure the secrets in this file are kept private
11 | # if you're sharing your code publicly.
12 |
13 | # Shared secrets are available across all environments.
14 |
15 | # shared:
16 | # api_key: a1B2c3D4e5F6
17 |
18 | # Environmental secrets are only available for that specific environment.
19 |
20 | development:
21 | secret_key_base: 0b806dfab4087ce5e25ca5cde7792075793c75cd083b3489011ec562ce0f3fd585b0248196c2885282b2e2ea13d5e56c31abc9cc7aa31287a7395ba7f89be08d
22 |
23 | test:
24 | secret_key_base: 6e1005cf8c4c7a5af5d6d2b8844c4c114da57a46f9eb80cfcf330064cc6f020dea05090286de46f858c9cce82b714bb852c9a150025f3610a9d3cfdc0a95dfda
25 |
26 | # Do not keep production secrets in the unencrypted secrets file.
27 | # Instead, either read values from the environment.
28 | # Or, use `bin/rails secrets:setup` to configure encrypted secrets
29 | # and move the `production:` environment over there.
30 |
31 | production:
32 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
33 |
--------------------------------------------------------------------------------
/frontend/actions/flag_actions.js:
--------------------------------------------------------------------------------
1 | import * as FlagUtil from '../util/flag_api_util';
2 |
3 | export const RECEIVE_FLAG = 'RECEIVE_FLAG';
4 | export const REMOVE_FLAG = 'REMOVE_FLAG';
5 | export const RECEIVE_FLAG_ERRORS = 'RECEIVE_FLAG_ERRORS';
6 | export const RECEIVE_FLAGS = 'RECEIVE_FLAGS';
7 |
8 | export const receiveFlag = (newFlag) => {
9 | return {
10 | type: RECEIVE_FLAG,
11 | flag: newFlag,
12 | };
13 | };
14 |
15 | export const removeFlag = (flag) => {
16 | return {
17 | type: REMOVE_FLAG,
18 | flag,
19 | };
20 | };
21 |
22 | export const receiveFlagErrors = (errors) => {
23 | return {
24 | type: RECEIVE_FLAG_ERRORS,
25 | errors: errors.responseJSON,
26 | };
27 | };
28 |
29 | export const receiveFlags = (flags, googleMap, infoWindow, notes) => {
30 | return {
31 | type: RECEIVE_FLAGS,
32 | flags,
33 | googleMap,
34 | infoWindow,
35 | notes,
36 | };
37 | ;}
38 |
39 |
40 | export const deleteFlag = (flagId, googleMap) => {
41 | return (dispatch) => {
42 | return FlagUtil.deleteFlag(flagId)
43 | .then((flag) => dispatch(removeFlag(flag, googleMap)));
44 | };
45 | };
46 |
47 | export const createFlag = (flag, googleMap) => {
48 | return (dispatch) => {
49 | return FlagUtil.createFlag(flag)
50 | .then((flag) => dispatch(receiveFlag(flag, googleMap)),
51 | (errors) => dispatch(receiveFlagErrors(errors))
52 | );};
53 | };
54 |
--------------------------------------------------------------------------------
/frontend/components/map_view/location_search.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class LocationSearch extends React.Component {
4 | constructor(props) {
5 | super(props);
6 | this.state = {inputVal: ""};
7 | }
8 |
9 | setAutocomplete() {
10 | const options = {
11 | };
12 | this.locationAutocomplete = new google.maps.places.Autocomplete(this.searchInputEl, options);
13 | this.locationAutocomplete.addListener('place_changed', (e) => {
14 | const location = this.locationAutocomplete.getPlace();
15 | this.props.selectLocation(location.geometry.location.lat(), location.geometry.location.lng(), location.name, location.place_id, location.formatted_address);
16 | });
17 | }
18 |
19 | componentDidMount() {
20 | this.searchInputEl = document.getElementById(`location-search-input-${this.props.renderedOn}`);
21 | }
22 |
23 | render() {
24 | if (this.searchInputEl && window.google && !this.locationAutocomplete) {
25 | this.setAutocomplete();
26 | }
27 | const placeholder = this.props.renderedOn === "map" ? " ⚐ Search Location" : " ⚐ Add Flag";
28 |
29 | return (
30 |
31 |
35 |
36 | );
37 | }
38 | }
39 |
40 | export default LocationSearch;
41 |
--------------------------------------------------------------------------------
/app/controllers/api/notes_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::NotesController < ApplicationController
2 |
3 | def destroy
4 | @note = current_user.notes.includes(:tags).find(params[:id])
5 | @note.destroy!
6 | render :destroy
7 | end
8 |
9 | def update
10 | @note = current_user.notes.includes(:tags, :flag, :notebook).find(params[:id])
11 |
12 | notebook_ids = [@note.notebook_id]
13 | tag_ids = @note.tag_ids
14 | flag_ids = @note.flag ? [@note.flag_id] : []
15 |
16 | if @note.update(note_params)
17 |
18 | notebook_ids << @note.notebook_id if !notebook_ids.include?(@note.notebook_id)
19 | flag_ids << @note.flag_id if @note.flag_id && !flag_ids.include?(@note.flag_id)
20 | tag_ids = (tag_ids + @note.tag_ids).uniq
21 |
22 | @notebooks = Notebook.where(id: [notebook_ids])
23 | @flags = Flag.where(id: [flag_ids])
24 | @tags = Tag.where(id: [tag_ids])
25 | render :update
26 | else
27 | render json: @note.errors.messages.values.flatten, status: 422
28 | end
29 | end
30 |
31 | def create
32 | @note = current_user.notes.new(note_params)
33 | if @note.save
34 | render :create
35 | else
36 | render json: @note.errors.messages.values.flatten, status: 422
37 | end
38 | end
39 |
40 | def note_params
41 | params.require(:note).permit(:title, :body, :body_plain, :notebook_id, :flag_id, tag_ids: [])
42 | end
43 |
44 | end
45 |
--------------------------------------------------------------------------------
/frontend/reducers/entities/notebooks_reducer.js:
--------------------------------------------------------------------------------
1 | import { RECEIVE_ALL_ENTITIES } from '../../actions/entity_actions';
2 | import { RECEIVE_NOTEBOOK, REMOVE_NOTEBOOK } from '../../actions/notebook_actions';
3 | import { RECEIVE_CURRENT_USER } from '../../actions/session_actions';
4 | import { RECEIVE_NEW_NOTE, RECEIVE_UPDATED_NOTE, REMOVE_NOTE } from '../../actions/note_actions';
5 | import { merge } from 'lodash';
6 |
7 | const NotebooksReducer = (oldState = {}, action) => {
8 | const newState = merge({}, oldState);
9 | switch (action.type) {
10 | case RECEIVE_CURRENT_USER:
11 | return action.currentUser ? oldState : {};
12 | case RECEIVE_ALL_ENTITIES:
13 | return action.notebooks ? Object.assign({}, action.notebooks) : {};
14 | case RECEIVE_NOTEBOOK:
15 | newState[action.notebook.id] = action.notebook;
16 | return newState;
17 | case REMOVE_NOTEBOOK:
18 | delete newState[action.notebook.id];
19 | return newState;
20 | case RECEIVE_NEW_NOTE:
21 | newState[action.note.notebookId].noteIds.push(action.note.id);
22 | return newState;
23 | case RECEIVE_UPDATED_NOTE:
24 | return Object.assign(newState, action.notebooks);
25 | case REMOVE_NOTE:
26 | newState[action.notebookId].noteIds = newState[action.notebookId].noteIds.filter((noteId) => noteId !== action.noteId);
27 | return newState;
28 | default:
29 | return oldState;
30 | }
31 | };
32 |
33 | export default NotebooksReducer;
34 |
--------------------------------------------------------------------------------
/frontend/actions/ui_actions.js:
--------------------------------------------------------------------------------
1 | export const TOGGLE_DELETE_FORM = "TOGGLE_DELETE_FORM";
2 | export const TOGGLE_CREATE_FORM = "TOGGLE_CREATE_FORM";
3 | export const TOGGLE_NOTE_ORDER = "TOGGLE_NOTE_ORDER";
4 | export const TOGGLE_SELECTED_NOTEBOOK = "TOGGLE_SELECTED_NOTEBOOK";
5 | export const TOGGLE_MODAL = "TOGGLE_MODAL";
6 | export const TOGGLE_SIDEMENU = "TOGGLE_SIDEMENU";
7 | export const TOGGLE_SIDEMENU_ITEM_TYPE = "TOGGLE_SIDEMENU_ITEM_TYPE";
8 |
9 | export const toggleModal = (modalName) => {
10 | return {
11 | type: TOGGLE_MODAL,
12 | modalName,
13 | };
14 | };
15 |
16 | export const toggleSidemenuItemType = (itemType) => {
17 | return {
18 | type: TOGGLE_SIDEMENU_ITEM_TYPE,
19 | itemType,
20 | };
21 | };
22 |
23 | export const toggleSidemenu = () => {
24 | return {
25 | type: TOGGLE_SIDEMENU,
26 | };
27 | };
28 |
29 | export const toggleSelectedNotebook = (notebookId, clicked) => {
30 | return {
31 | type: TOGGLE_SELECTED_NOTEBOOK,
32 | notebookId,
33 | clicked,
34 | };
35 | };
36 |
37 | export const toggleDeleteForm = (toDelete) => {
38 | return {
39 | type: TOGGLE_DELETE_FORM,
40 | toDelete,
41 | };
42 | };
43 |
44 | export const toggleCreateForm = (itemType) => {
45 | return {
46 | type: TOGGLE_CREATE_FORM,
47 | itemType,
48 | };
49 | };
50 |
51 | export const toggleNoteOrder = (order) => {
52 | return {
53 | type: TOGGLE_NOTE_ORDER,
54 | order,
55 | };
56 | };
57 |
--------------------------------------------------------------------------------
/frontend/reducers/entities/flags_reducer.js:
--------------------------------------------------------------------------------
1 | import { RECEIVE_ALL_ENTITIES } from '../../actions/entity_actions';
2 | import { RECEIVE_FLAG, REMOVE_FLAG } from '../../actions/flag_actions';
3 | import { RECEIVE_CURRENT_USER } from '../../actions/session_actions';
4 | import { RECEIVE_NEW_NOTE, RECEIVE_UPDATED_NOTE, REMOVE_NOTE } from '../../actions/note_actions';
5 | import { merge } from 'lodash';
6 |
7 | const FlagsReducer = (oldState = {}, action) => {
8 | let newState = merge({}, oldState);
9 | switch (action.type) {
10 | case RECEIVE_CURRENT_USER:
11 | return action.currentUser ? oldState : {};
12 | case RECEIVE_ALL_ENTITIES:
13 | return action.flags ? merge({}, action.flags) : {};
14 | case RECEIVE_FLAG:
15 | newState[action.flag.id] = action.flag;
16 | return newState;
17 | case REMOVE_FLAG:
18 | delete newState[action.flag.id];
19 | return newState;
20 | case RECEIVE_NEW_NOTE:
21 | if (action.note.flagId) {
22 | newState[action.note.flagId].noteIds.push(action.note.id);
23 | }
24 | return newState;
25 | case RECEIVE_UPDATED_NOTE:
26 | return merge(newState, action.flags);
27 | case REMOVE_NOTE:
28 | if (action.flagId) {
29 | newState[action.flagId].noteIds = newState[action.flagId].noteIds.filter((noteId) => noteId !== action.noteId);
30 | }
31 | return newState;
32 | default:
33 | return oldState;
34 | }
35 | };
36 |
37 | export default FlagsReducer;
38 |
--------------------------------------------------------------------------------
/frontend/actions/session_actions.js:
--------------------------------------------------------------------------------
1 | import * as SessionUtil from '../util/session_api_util';
2 |
3 | export const RECEIVE_CURRENT_USER = 'RECEIVE_CURRENT_USER';
4 | export const RECEIVE_USER_ERRORS = 'RECEIVE_USER_ERRORS';
5 |
6 | export const receiveCurrentUser = (currentUser) => {
7 | return {
8 | type: RECEIVE_CURRENT_USER,
9 | currentUser,
10 | };
11 | };
12 |
13 | export const receiveUserErrors = (errors) => {
14 | return {
15 | type: RECEIVE_USER_ERRORS,
16 | errors: errors.responseJSON,
17 | };
18 | };
19 |
20 | export const login = (user) => {
21 | return (dispatch) => {
22 | return SessionUtil.login(user).then((userRes) => {
23 | dispatch(receiveCurrentUser(userRes));
24 | dispatch(receiveUserErrors([]));
25 | },
26 | (userErrors) => dispatch(receiveUserErrors(userErrors))
27 | );};
28 | };
29 |
30 | export const signup = (user) => {
31 | return (dispatch) => {
32 | return SessionUtil.signup(user).then((userRes) => {
33 | dispatch(receiveCurrentUser(userRes));
34 | dispatch(receiveUserErrors([]));
35 | },
36 | (userErrors) => dispatch(receiveUserErrors(userErrors))
37 | );};
38 | };
39 |
40 | export const logout = () => {
41 | return (dispatch) => {
42 | return SessionUtil.logout().then(() => {
43 | dispatch(receiveCurrentUser(null));
44 | dispatch(receiveUserErrors([]));
45 | },
46 | (errors) => dispatch(receiveUserErrors(errors))
47 | );};
48 | };
49 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/application.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
6 | * vendor/assets/stylesheets directory can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10 | * files in this directory. Styles in this file should be added after the last require_* statement.
11 | * It is generally better to create a new file per style scope.
12 | *
13 | *= require reset
14 | *= require buttons
15 | *= require_tree .
16 | *= require_self
17 | */
18 |
19 | @import "variables";
20 |
21 | .app-page {
22 | overflow: hidden;
23 | display: flex;
24 | flex-direction: row;
25 | align-items: flex-start;
26 | height: 100%;
27 | width: 100%;
28 | box-sizing: border-box;
29 | }
30 |
31 | body, html {
32 | height: 100vh;
33 | overflow: hidden;
34 | }
35 |
36 | #root {
37 | height: 100vh;
38 | overflow: hidden;
39 | }
40 |
41 | ::-webkit-scrollbar {
42 | width: 10px;
43 | }
44 |
45 | ::-webkit-scrollbar-track {
46 | background: $light-grey;
47 | border-radius: 7px;;
48 | }
49 |
50 | ::-webkit-scrollbar-thumb {
51 | background: $grey;
52 | border-radius: 7px;
53 | }
54 |
--------------------------------------------------------------------------------
/frontend/reducers/entities/tags_reducer.js:
--------------------------------------------------------------------------------
1 | import { RECEIVE_TAG, REMOVE_TAG } from '../../actions/tag_actions';
2 | import { RECEIVE_ALL_ENTITIES } from '../../actions/entity_actions';
3 | import { RECEIVE_CURRENT_USER } from '../../actions/session_actions';
4 | import { RECEIVE_NEW_NOTE, RECEIVE_UPDATED_NOTE, REMOVE_NOTE } from '../../actions/note_actions';
5 | import { merge } from 'lodash';
6 |
7 | const TagsReducer = (oldState = {}, action) => {
8 | const newState = merge({}, oldState);
9 | switch (action.type) {
10 | case RECEIVE_CURRENT_USER:
11 | return action.currentUser ? oldState : {};
12 | case RECEIVE_ALL_ENTITIES:
13 | return action.tags ? Object.assign({}, action.tags) : {};
14 | case RECEIVE_TAG:
15 | newState[action.tag.id] = action.tag;
16 | return newState;
17 | case REMOVE_TAG:
18 | delete newState[action.tag.id];
19 | return newState;
20 | case RECEIVE_NEW_NOTE:
21 | action.note.tagIds.forEach((tagId) => {
22 | newState[tagId].noteIds.push(action.note.id);
23 | });
24 | return newState;
25 | case RECEIVE_UPDATED_NOTE:
26 | return Object.assign(newState, action.tags);
27 | case REMOVE_NOTE:
28 | action.tagIds.forEach((tagId) => {
29 | newState[tagId].noteIds = newState[tagId].noteIds.filter((noteId) => noteId !== action.noteId);
30 | });
31 | return newState;
32 | default:
33 | return oldState;
34 | }
35 | };
36 |
37 | export default TagsReducer;
38 |
--------------------------------------------------------------------------------
/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User < ApplicationRecord
2 | validates :email, :session_token, presence: true
3 | validates :email, uniqueness: true, case_sensitive: false
4 | validates :password, length: { minimum: 6, allow_nil: true }
5 | validate :valid_email
6 |
7 | attr_reader :password
8 |
9 | after_initialize :ensure_session_token
10 |
11 | has_many :notebooks, dependent: :destroy
12 | has_many :notes, through: :notebooks
13 | has_many :tags, dependent: :destroy
14 | has_many :photos, dependent: :destroy
15 | has_many :flags, dependent: :destroy
16 |
17 | def valid_email
18 | if !email.include?("@") && email != ""
19 | errors[:base] << "Email is invalid"
20 | end
21 | end
22 |
23 | def self.find_by_credentials(email, password)
24 | user = User.find_by(email: email)
25 | user && user.is_password?(password) ? user : nil
26 | end
27 |
28 | def self.generate_token
29 | SecureRandom::urlsafe_base64
30 | end
31 |
32 | def password=(password)
33 | @password = password
34 | self.password_digest = BCrypt::Password.create(password)
35 | end
36 |
37 | def is_password?(password)
38 | BCrypt::Password.new(self.password_digest).is_password?(password)
39 | end
40 |
41 | def ensure_session_token
42 | self.session_token = User.generate_token
43 | end
44 |
45 | def reset_session_token!
46 | self.session_token = User.generate_token
47 | self.save!
48 | self.session_token
49 | end
50 |
51 | end
52 |
--------------------------------------------------------------------------------
/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Omninote
5 | <%= csrf_meta_tags %>
6 |
7 |
8 |
9 |
10 |
12 |
13 | <%= stylesheet_link_tag 'application', media: 'all' %>
14 | <%= javascript_include_tag 'application' %>
15 |
28 |
29 |
30 |
31 | <%= yield %>
32 |
33 |
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Omninote",
3 | "private": true,
4 | "dependencies": {
5 | "babel-core": "^6.26.0",
6 | "babel-loader": "^7.1.2",
7 | "babel-preset-es2015": "^6.24.1",
8 | "babel-preset-react": "^6.24.1",
9 | "lodash": "^4.6.0",
10 | "react": "^16.0.0",
11 | "react-dom": "^16.0.0",
12 | "react-md-spinner": "^0.2.5",
13 | "react-modal": "^3.0.4",
14 | "react-quill": "^1.1.0",
15 | "react-redux": "^5.0.6",
16 | "react-router": "^4.2.0",
17 | "react-router-dom": "^4.2.2",
18 | "redux": "^3.7.2",
19 | "redux-logger": "^3.0.6",
20 | "redux-thunk": "^2.2.0",
21 | "snake-case": "^2.1.0",
22 | "webpack": "^3.8.1"
23 | },
24 | "description": "Omninote is a single-page clone of Evernote, a web application for organizing notes in rich format text through notebooks and tags.",
25 | "version": "1.0.0",
26 | "main": "webpack.config.js",
27 | "directories": {
28 | "test": "test"
29 | },
30 | "engines": {
31 | "node": "4.1.1",
32 | "npm": "2.1.x"
33 | },
34 | "scripts": {
35 | "test": "echo \"Error: no test specified\" && exit 1",
36 | "start": "webpack --watch",
37 | "postinstall": "webpack"
38 | },
39 | "repository": {
40 | "type": "git",
41 | "url": "git+https://github.com/ommish/Omninote.git"
42 | },
43 | "keywords": [],
44 | "author": "",
45 | "license": "ISC",
46 | "bugs": {
47 | "url": "https://github.com/ommish/Omninote/issues"
48 | },
49 | "homepage": "https://github.com/ommish/Omninote#readme"
50 | }
51 |
--------------------------------------------------------------------------------
/frontend/components/note_index/note_order_option_menu.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class NoteOrderOptions extends React.Component {
4 |
5 | constructor(props) {
6 | super(props);
7 | this.toggleNoteOrderDropdown = this.toggleNoteOrderDropdown.bind(this);
8 | }
9 |
10 | toggleNoteOrderDropdown (e) {
11 | e.stopPropagation();
12 | this.props.toggleNoteOrderDropdown();
13 | }
14 |
15 | toggleNoteOrder (order) {
16 | return (e) => {
17 | this.props.toggleNoteOrder(order);
18 | this.toggleNoteOrderDropdown(e);
19 | };
20 | }
21 |
22 | render () {
23 | const options = this.props.options.map((option, i) => (
24 |
27 | {option}
28 | )
29 | );
30 | return (
31 | [
32 | ,
42 |
45 |
46 | ]
47 | );
48 | }
49 | }
50 |
51 |
52 | export default NoteOrderOptions;
53 |
--------------------------------------------------------------------------------
/app/models/flag.rb:
--------------------------------------------------------------------------------
1 | class Flag < ApplicationRecord
2 | validates :place_id, :title, :lat, :lng, presence: true
3 | validates :place_id, uniqueness: { scope: :user }
4 |
5 | belongs_to :user
6 | has_many :notes, dependent: :nullify
7 |
8 | end
9 |
10 |
11 | # polymorphic notes
12 | # polymorphic association- noteable- notes can belong to notebook or Flag
13 | # add migration for notes so it holds foreign key + foreign class
14 | # pros: polymorphic associations!
15 | # cons: editor component would have to double as a notebook note editor and a map note editor (or )
16 |
17 | # OR
18 |
19 | # new table for new kind of note, specfic for locations?
20 |
21 | # OR
22 | # just new kind of tag that's a location, so just adding another belongs to association on the same notes
23 | # notes can belong both to a notebook and a location
24 | # add modal for location selector
25 | # another modal to view tagged locations
26 | # or combine modals so index and
27 |
28 | # but then does a note need to belong to both a notebook and a location?
29 |
30 |
31 | # click add flag
32 | # opens map view
33 | # user inputs search query
34 | # autocomplete for matching places
35 | # add flag button on location marker
36 | # add flag button will create new flag, apply name to note editor view
37 | # add flag button turns into change flag button
38 |
39 | # map view shows map and list of all flagged notes on left, with locations within map view
40 | # location markers also have button to filter notes to only that location's notes
41 | # button at top of flagged locations list to see all flagged locations
42 |
--------------------------------------------------------------------------------
/frontend/components/session/logout_form.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Modal from 'react-modal';
3 | import { merge } from 'lodash';
4 |
5 | class LogoutForm extends React.Component {
6 |
7 | constructor(props) {
8 | super(props);
9 | this.handleSubmit = this.handleSubmit.bind(this);
10 | this.handleCancel = this.handleCancel.bind(this);
11 | this.toggleLogoutForm = this.props.toggleLogoutForm.bind(this);
12 | }
13 |
14 | handleSubmit(e) {
15 | e.stopPropagation();
16 | this.toggleLogoutForm();
17 | this.props.logout()
18 | }
19 |
20 | handleCancel(e) {
21 | e.stopPropagation();
22 | this.toggleLogoutForm();
23 | }
24 |
25 | render() {
26 | return (
27 |
33 |
34 |
Log Out
35 |
36 |
Are you sure you want to leave?
37 |
38 |
40 | Cancel
41 |
44 | Log Out
45 |
46 |
47 |
48 | );
49 | }
50 | }
51 |
52 | export default LogoutForm;
53 |
--------------------------------------------------------------------------------
/frontend/components/sidemenu/sidemenu_index_item.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import NotebookDetails from './notebook_details';
3 | import TagDetails from './tag_details';
4 |
5 | class SidemenuIndexItem extends React.Component {
6 |
7 | constructor (props) {
8 | super(props);
9 | this.handleClick = this.handleClick.bind(this);
10 | }
11 |
12 | handleClick (id) {
13 | return (e) => {
14 | if (e.target.id === "delete") {
15 | this.props.toggleDeleteForm(id);
16 | e.stopPropagation();
17 | } else {
18 | this.props.toggleSidemenu();
19 | const path = `/${this.props.itemType}s/${this.props.item.id}`;
20 | if (this.props.location.pathname !== path) {
21 | this.props.history.push(path);
22 | }
23 | e.stopPropagation();
24 | }
25 | };
26 | }
27 |
28 | render () {
29 | const notePluralized = this.props.item.noteIds.length === 1 ? "note" : "notes";
30 | return (
31 |
33 | {this.props.itemType === "notebook" ?
34 | :
35 | }
36 |
40 |
41 | );
42 | }
43 | }
44 |
45 | export default SidemenuIndexItem;
46 |
--------------------------------------------------------------------------------
/frontend/components/entity_forms/delete_form_container.js:
--------------------------------------------------------------------------------
1 | import DeleteForm from './delete_form';
2 | import { connect } from 'react-redux';
3 | import { deleteNotebook } from '../../actions/notebook_actions';
4 | import { deleteNote } from '../../actions/note_actions';
5 | import { deleteTag } from '../../actions/tag_actions';
6 | import { deleteFlag } from '../../actions/flag_actions';
7 | import { toggleDeleteForm, toggleSelectedNotebook } from '../../actions/ui_actions';
8 | import { withRouter } from 'react-router-dom';
9 |
10 | const mapStateToProps = (state, ownProps) => {
11 | const item = state.entities[`${ownProps.itemType}s`][state.ui.deleteForm.id];
12 | return {
13 | item,
14 | formTitle: `DELETE ${ownProps.itemType}`,
15 | deleteForm: state.ui.deleteForm,
16 | formMessage: "Are you sure you want to delete",
17 | formMessageTitle: `${item ? item.title.concat("?") : ""}`,
18 | };
19 | };
20 |
21 | const mapDispatchToProps = (dispatch, ownProps) => {
22 | let action;
23 | switch(ownProps.itemType) {
24 | case "notebook":
25 | action = deleteNotebook;
26 | break;
27 | case "note":
28 | action = deleteNote;
29 | break;
30 | case "tag":
31 | action = deleteTag;
32 | break;
33 | case "flag":
34 | action = deleteFlag;
35 | break;
36 | default:
37 | break;
38 | }
39 | return {
40 | deleteItem: (id) => dispatch(action(id)),
41 | toggleDeleteForm: (toDelete) => dispatch(toggleDeleteForm(toDelete)),
42 | toggleSelectedNotebook: () => dispatch(toggleSelectedNotebook(null, false)),
43 | };
44 | };
45 |
46 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(DeleteForm));
47 |
--------------------------------------------------------------------------------
/frontend/util/sorters.js:
--------------------------------------------------------------------------------
1 | export const sortItems = (notes, sortOrder) => {
2 | return notes.sort(comparingFunctions[sortOrder]);
3 | };
4 |
5 | const updatedAtNewest = (a, b) => {
6 | if (a.updatedAt > b.updatedAt) {
7 | return -1;
8 | }
9 | else if (a.updatedAt < b.updatedAt) {
10 | return 1;
11 | } else {
12 | return 0;
13 | }
14 | };
15 |
16 | const updatedAtOldest = (a, b) => {
17 | if (a.updatedAt < b.updatedAt) {
18 | return -1;
19 | }
20 | else if (a.updatedAt > b.updatedAt) {
21 | return 1;
22 | } else {
23 | return 0;
24 | }
25 | };
26 |
27 | const createdAtNewest = (a, b) => {
28 | if (a.createdAt > b.createdAt) {
29 | return -1;
30 | }
31 | else if (a.createdAt < b.createdAt) {
32 | return 1;
33 | } else {
34 | return 0;
35 | }
36 | };
37 |
38 | const createdAtOldest = (a, b) => {
39 | if (a.createdAt < b.createdAt) {
40 | return -1;
41 | }
42 | else if (a.createdAt > b.createdAt) {
43 | return 1;
44 | } else {
45 | return 0;
46 | }
47 | };
48 |
49 | const titleAsc = (a, b) => {
50 | if (a.title.toLowerCase() < b.title.toLowerCase()) {
51 | return -1;
52 | }
53 | else if (a.title.toLowerCase() > b.title.toLowerCase()) {
54 | return 1;
55 | } else {
56 | return 0;
57 | }
58 | };
59 |
60 | const titleDesc = (a, b) => {
61 | if (a.title.toLowerCase() > b.title.toLowerCase()) {
62 | return -1;
63 | }
64 | else if (a.title.toLowerCase() < b.title.toLowerCase()) {
65 | return 1;
66 | } else {
67 | return 0;
68 | }
69 | };
70 |
71 |
72 | const comparingFunctions = [
73 | updatedAtNewest,
74 | createdAtNewest,
75 | updatedAtOldest,
76 | createdAtOldest,
77 | titleAsc,
78 | titleDesc
79 | ];
80 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/notebook_dropdown.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .select-notebook {
4 | margin-right: 15px;
5 | position: relative;
6 | min-width: 300px;
7 | max-width: 300px;
8 | }
9 |
10 | .select-notebook:hover {
11 | cursor: pointer;
12 | }
13 |
14 | .select-notebook > :first-child {
15 | white-space: nowrap;
16 | text-overflow: ellipsis;
17 | overflow: hidden;
18 | margin-bottom: 15px;
19 | }
20 |
21 | .notebook-dropdown-open {
22 | min-width: 300px;
23 | max-width: 300px;
24 | background: white;
25 | display: flex;
26 | z-index: 45;
27 | position: absolute;
28 | top: 30px;
29 | border: 1px solid $light-grey;
30 | }
31 |
32 | .notebook-dropdown-closed {
33 | display: none;
34 | }
35 |
36 | .notebook-dropdown-overlay-open {
37 | height: 100%;
38 | width: 100%;
39 | z-index: 44;
40 | top: 0px;
41 | left: 0px;
42 | position: fixed;
43 | overflow: hidden;
44 | }
45 |
46 | .notebook-dropdown-overlay-closed {
47 | display: none;
48 | }
49 |
50 | .notebook-list {
51 | max-height: 300px;
52 | width: 100%;
53 | overflow-y: scroll;
54 | overflow-x: hidden;
55 | display: flex;
56 | flex-direction: column;
57 | align-items: flex-start;
58 | }
59 |
60 | .notebook-dropdown-item {
61 | width: 100%;
62 | padding: 5px;
63 | color: $darker-grey;
64 | text-align: left;
65 | box-sizing: border-box;
66 | word-wrap: break-word;
67 | }
68 |
69 | .notebook-dropdown-item.selected {
70 | color: $light-green;
71 | font-weight: 700;
72 | }
73 |
74 | .notebook-dropdown-item:hover, .notebook-dropdown-item.selected:hover {
75 | background: $light-green;
76 | color: white;
77 | }
78 |
79 | .notebook-dropdown-item:last-of-type {
80 | background-color: $light-grey;
81 | }
82 |
--------------------------------------------------------------------------------
/frontend/actions/note_actions.js:
--------------------------------------------------------------------------------
1 | import * as NoteUtil from '../util/note_api_util';
2 |
3 | export const RECEIVE_NEW_NOTE = "RECEIVE_NEW_NOTE";
4 | export const RECEIVE_UPDATED_NOTE = "RECEIVE_UPDATED_NOTE";
5 | export const REMOVE_NOTE = "REMOVE_NOTE";
6 | export const RECEIVE_NOTE_ERRORS = "RECEIVE_NOTE_ERRORS";
7 |
8 | export const receiveNewNote = ({note}) => {
9 | return {
10 | type: RECEIVE_NEW_NOTE,
11 | note,
12 | };
13 | };
14 |
15 | export const receiveUpdatedNote = ({note, tags, notebooks, flags}) => {
16 | return {
17 | type: RECEIVE_UPDATED_NOTE,
18 | note,
19 | tags,
20 | notebooks,
21 | flags,
22 | };
23 | };
24 |
25 | export const removeNote = ({noteId, notebookId, tagIds, flagId}) => {
26 | return {
27 | type: REMOVE_NOTE,
28 | noteId,
29 | tagIds,
30 | notebookId,
31 | flagId,
32 | };
33 | };
34 |
35 | export const receiveNoteErrors = (errors) => {
36 | return {
37 | type: RECEIVE_NOTE_ERRORS,
38 | errors: errors.responseJSON,
39 | };
40 | };
41 |
42 | export const deleteNote = (noteId) => {
43 | return (dispatch) => {
44 | return NoteUtil.deleteNote(noteId)
45 | .then((noteRes) => dispatch(removeNote(noteRes)));
46 | };
47 | };
48 |
49 | export const createNote = (note) => {
50 | return (dispatch) => {
51 | return NoteUtil.createNote(note)
52 | .then((noteRes) => dispatch(receiveNewNote(noteRes)),
53 | (errors) => dispatch(receiveNoteErrors(errors))
54 | );};
55 | };
56 |
57 | export const updateNote = (note) => {
58 | return (dispatch) => {
59 | return NoteUtil.updateNote(note)
60 | .then((noteRes) => dispatch(receiveUpdatedNote(noteRes)),
61 | (errors) => dispatch(receiveNoteErrors(errors))
62 | );};
63 | };
64 |
--------------------------------------------------------------------------------
/frontend/components/default_page.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SessionForm from './session/session_form_container';
3 | import { Link } from 'react-router-dom';
4 |
5 | class DefaultPage extends React.Component {
6 |
7 | constructor (props) {
8 | super(props);
9 | }
10 |
11 | render () {
12 | return (
13 |
15 |
16 |
17 |
18 | OMNINOTE
19 |
20 |
21 | Log In
22 |
23 |
24 |
25 |
26 | Meet Omninote, your second brain.
27 |
28 |
30 |
32 |
35 |
36 |
47 |
48 | );
49 | }
50 |
51 | }
52 |
53 | export default DefaultPage;
54 |
55 | // button icons by
56 | // 146 raster icons by The Working Group
57 | // License: Creative Commons (Attribution-Share Alike 3.0 Unported)
58 |
--------------------------------------------------------------------------------
/frontend/components/entity_forms/create_form_container.js:
--------------------------------------------------------------------------------
1 | import CreateForm from './create_form';
2 | import { connect } from 'react-redux';
3 | import { createNotebook, receiveNotebookErrors } from '../../actions/notebook_actions';
4 | import { createTag, receiveTagErrors } from '../../actions/tag_actions';
5 | import { withRouter } from 'react-router-dom';
6 | import { toggleModal, toggleCreateForm, toggleSelectedNotebook } from '../../actions/ui_actions';
7 |
8 | const mapStateToProps = (state, ownProps) => {
9 | let formMessage;
10 | let buttonMessage;
11 | let errors;
12 |
13 | if (ownProps.itemType === "notebook") {
14 | formMessage = "Title your notebook";
15 | buttonMessage = "Create notebook";
16 | errors = state.errors.notebookErrors;
17 | } else {
18 | formMessage = "Name your tag";
19 | buttonMessage = "Create tag";
20 | errors = state.errors.tagErrors;
21 | }
22 | return {
23 | notebookDropdownOpen: state.ui.notebookDropdown,
24 | createFormType: state.ui.createForm.itemType,
25 | formMessage,
26 | buttonMessage,
27 | errors,
28 | };
29 | };
30 |
31 | const mapDispatchToProps = (dispatch, ownProps) => {
32 | const action = ownProps.itemType === "notebook" ? createNotebook : createTag;
33 | const clearItemErrors = ownProps.itemType === "notebook" ? receiveNotebookErrors : receiveTagErrors;
34 | return {
35 | createItem: (item) => dispatch(action(item)),
36 | toggleCreateForm: (itemType) => dispatch(toggleCreateForm(itemType)),
37 | clearItemErrors: () => dispatch(clearItemErrors([])),
38 | toggleNotebookDropdown: () => dispatch(toggleModal("notebookDropdown")),
39 | toggleSelectedNotebook: (id) => dispatch(toggleSelectedNotebook(id, true)),
40 | };
41 | };
42 |
43 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(CreateForm));
44 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sidenav.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .sidenav {
4 | position: absolute;
5 | top: 0;
6 | left: 0;
7 | display: flex;
8 | flex-direction: column;
9 | align-items: center;
10 | justify-content: flex-start;
11 | flex-wrap: nowrap;
12 | height: 100%;
13 | min-width: 70px;
14 | background-color: $light-grey;
15 | border-right: 1px solid $grey;
16 | z-index: 35;
17 | transition: left .5s;
18 | }
19 |
20 | .closed-nav {
21 | left: -100px;
22 | transition: left .5s;
23 | }
24 |
25 | .sidenav-icon-list {
26 | display: flex;
27 | flex-direction: column;
28 | align-items: center;
29 | justify-content: space-around;
30 | width: 100%;
31 | z-index: 35;
32 | }
33 |
34 | .sidenav-icon-list > li {
35 | margin-bottom: 30px;
36 | }
37 |
38 | .sidenav-icon-list > li:nth-of-type(1) {
39 | margin-top: 20px;
40 | margin-bottom: 50px;
41 | }
42 |
43 | .circle-button {
44 | border-radius: 50%;
45 | width: 40px;
46 | height: 40px;
47 | background-color: white;
48 | display: flex;
49 | align-items: center;
50 | justify-content: center;
51 |
52 | .sidenav-tooltip {
53 | display: none;
54 | }
55 | }
56 |
57 | .circle-button:hover {
58 | border: 1px solid $light-green;
59 | transition: border .3s;
60 |
61 | .sidenav-tooltip {
62 | font-size: 14px;
63 | display: flex;
64 | position: fixed;
65 | z-index: 60;
66 | left: 55px;
67 | height: auto;
68 | width: auto;
69 | background-color: $dark-grey;
70 | border-radius: 3px;
71 | padding: 3px;
72 | color: white;
73 | align-items: center;
74 | flex-wrap: wrap;
75 | }
76 | }
77 |
78 | .sidenav-icon {
79 | height: 20px;
80 | width: auto;
81 | }
82 |
83 | .sidenav-logo {
84 | height: 35px;
85 | width: auto;
86 | }
87 |
88 | .circle-button.logout:hover {
89 | border: 1px solid blue;
90 | }
91 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sidemenu.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | @-webkit-keyframes slideout {
4 | from { left: -430px; }
5 | to { left: 0px; }
6 | }
7 |
8 | @-webkit-keyframes slidein {
9 | from { transform: translateX(530px); }
10 | to { transform: translateX(0); }
11 | }
12 |
13 | // for initial state
14 | .hidden {
15 | display: none;
16 | }
17 |
18 | .sidemenu-overlay.closed-sidemenu-overlay {
19 | height: 0;
20 | background: rgba(255, 255, 255, 0);
21 | transition: background .4s, height .4s;
22 | }
23 |
24 | .sidemenu-overlay {
25 | height: 100%;
26 | width: 100%;
27 | top: 0px;
28 | left: 70px;
29 | background: rgba(255, 255, 255, 0.81);
30 | position: absolute;
31 | overflow: hidden;
32 | z-index: 30;
33 | transition: background .4s;
34 | }
35 |
36 | .closed-sidemenu {
37 | background: white;
38 | left: -500px;
39 | top: 0px;
40 | height: 100%;
41 | width: 500px;
42 | border-right: 2px solid $grey;
43 | position: fixed;
44 | overflow-y: scroll;
45 | -webkit-animation: slidein .4s ease-in;
46 | z-index: 30;
47 | }
48 |
49 | .sidemenu-open {
50 | background: white;
51 | left: 70px;
52 | top: 0px;
53 | height: 100%;
54 | width: 500px;
55 | border-right: 2px solid $grey;
56 | position: fixed;
57 | overflow-y: scroll;
58 | -webkit-animation: slideout .4s ease-in;
59 | z-index: 30;
60 | }
61 |
62 | .sidemenu-sticky {
63 | position: sticky;
64 | top: 0;
65 | background: white;
66 | }
67 |
68 |
69 | .sidemenu-heading {
70 | display: flex;
71 | flex-direction: row;
72 | border-bottom: 1px solid $light-grey;
73 | justify-content: space-between;
74 | padding: 20px;
75 | h2 {
76 | text-transform: uppercase;
77 | color: $darker-grey;
78 | }
79 | }
80 |
81 | .tag-sidemenu-list {
82 | display: flex;
83 | flex-direction: row;
84 | flex-wrap: wrap;
85 | padding-left: 10px;
86 | padding-right: 10px;
87 | }
88 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/session_form_page.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .auth-page {
4 | padding-top: 100px;
5 | display: flex;
6 | align-items: center;
7 | justify-content: flex-start;
8 | flex-direction: column;
9 | height: 100%;
10 | width: 100%;
11 | background-color: $grey;
12 | overflow: hidden;
13 | }
14 |
15 | .auth-logo {
16 | width: 10%;
17 | border-bottom: 3px solid $dark-grey;
18 | margin-bottom: 30px;
19 | padding-bottom: 20px;
20 | display: flex;
21 | justify-content: center;
22 |
23 | img {
24 | width: 60%;
25 | height: 60%;
26 | }
27 | }
28 |
29 | .default-form {
30 | width: 70%;
31 | padding-left: 10%;
32 | }
33 |
34 | .full-page-form {
35 | width: 25%;
36 | height: auto;
37 | background: white;
38 | padding: 50px 80px;
39 | }
40 |
41 | .session-form {
42 | width: 100%;
43 | height: 100%;
44 | display: flex;
45 | flex-direction: column;
46 | justify-content: center;
47 | align-items: center;
48 |
49 | h2 {
50 | align-self: flex-start;
51 | }
52 |
53 | input {
54 | margin-top: 7px;
55 | margin-bottom: 7px;
56 | background: $light-grey;
57 | padding-top: 15px;
58 | padding-bottom: 15px;
59 | padding-left: 10px;
60 | width: 100%;
61 | height: auto;
62 | border-radius: 6px;
63 | box-sizing: border-box;
64 | }
65 |
66 | ul {
67 | width: 100%;
68 | display: flex;
69 | flex-direction: column;
70 | align-self: flex-start;
71 | }
72 |
73 | ul li {
74 | align-items: flex-start;
75 | justify-content: flex-start;
76 | color: $red;
77 | margin-bottom: 5px;
78 | align-self: flex-start;
79 | }
80 |
81 | p, a {
82 | margin-bottom: 5px;
83 | align-self: flex-start;
84 | }
85 |
86 | a:hover {
87 | color: $light-green;
88 | transition: color 0.2s;
89 | }
90 |
91 | .error-present {
92 | border: 1px solid $red;
93 | }
94 |
95 | }
96 |
--------------------------------------------------------------------------------
/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/frontend/reducers/entities/notes_reducer.js:
--------------------------------------------------------------------------------
1 | import { RECEIVE_NEW_NOTE, RECEIVE_UPDATED_NOTE, REMOVE_NOTE } from '../../actions/note_actions';
2 | import { REMOVE_NOTEBOOK } from '../../actions/notebook_actions';
3 | import { REMOVE_FLAG } from '../../actions/flag_actions';
4 | import { RECEIVE_TAG, REMOVE_TAG } from '../../actions/tag_actions';
5 | import { RECEIVE_ALL_ENTITIES } from '../../actions/entity_actions';
6 | import { RECEIVE_CURRENT_USER } from '../../actions/session_actions';
7 | import { merge } from 'lodash';
8 |
9 | const initialState = {initialState: true};
10 |
11 | const NotesReducer = (oldState = initialState, action) => {
12 | const newState = merge({}, oldState);
13 | switch (action.type) {
14 | case RECEIVE_CURRENT_USER:
15 | return action.currentUser ? oldState : initialState;
16 | case RECEIVE_ALL_ENTITIES:
17 | return action.notes ? Object.assign({}, action.notes) : {};
18 | case REMOVE_NOTEBOOK:
19 | action.notebook.noteIds.forEach((noteId) => {
20 | delete newState[noteId];
21 | });
22 | return newState;
23 | case RECEIVE_TAG:
24 | action.tag.noteIds.forEach((noteId) => {
25 | newState[noteId].tagIds.push(action.tag.id);
26 | });
27 | return newState;
28 | case REMOVE_TAG:
29 | action.tag.noteIds.forEach((noteId) => {
30 | newState[noteId].tagIds = newState[noteId].tagIds.filter((tagId) => tagId !== action.tag.id);
31 | });
32 | return newState;
33 | case REMOVE_FLAG:
34 | action.flag.noteIds.forEach((noteId) => {
35 | newState[noteId].flagId = null;
36 | });
37 | return newState;
38 | case RECEIVE_NEW_NOTE:
39 | case RECEIVE_UPDATED_NOTE:
40 | newState[action.note.id] = action.note;
41 | return newState;
42 | case REMOVE_NOTE:
43 | delete newState[action.noteId];
44 | return newState;
45 | default:
46 | return oldState;
47 | }
48 | };
49 |
50 | export default NotesReducer;
51 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure public file server for tests with Cache-Control for performance.
16 | config.public_file_server.enabled = true
17 | config.public_file_server.headers = {
18 | 'Cache-Control' => "public, max-age=#{1.hour.seconds.to_i}"
19 | }
20 |
21 | # Show full error reports and disable caching.
22 | config.consider_all_requests_local = true
23 | config.action_controller.perform_caching = false
24 |
25 | # Raise exceptions instead of rendering exception templates.
26 | config.action_dispatch.show_exceptions = false
27 |
28 | # Disable request forgery protection in test environment.
29 | config.action_controller.allow_forgery_protection = false
30 | config.action_mailer.perform_caching = false
31 |
32 | # Tell Action Mailer not to deliver emails to the real world.
33 | # The :test delivery method accumulates sent emails in the
34 | # ActionMailer::Base.deliveries array.
35 | config.action_mailer.delivery_method = :test
36 |
37 | # Print deprecation notices to the stderr.
38 | config.active_support.deprecation = :stderr
39 |
40 | # Raises error for missing translations
41 | # config.action_view.raise_on_missing_translations = true
42 | end
43 |
--------------------------------------------------------------------------------
/frontend/components/map_view/map.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class Map extends React.Component {
4 |
5 | constructor(props) {
6 | super(props);
7 | this.mapLoaded = false;
8 | }
9 |
10 | setMap() {
11 | this.googleMap = new google.maps.Map(this.mapDiv, {
12 | zoom: 8,
13 | center: {
14 | lat: this.props.mapCenter.lat,
15 | lng: this.props.mapCenter.lng,
16 | },
17 | });
18 |
19 | this.props.updateBounds(this.googleMap.getBounds());
20 |
21 | this.googleMap.addListener('bounds_changed', (e) => {
22 | this.props.updateBounds(this.googleMap.getBounds());
23 | this.props.setMapCenter(this.googleMap.getCenter().lat(), this.googleMap.getCenter().lng())
24 | this.findFlagsInRange();
25 | });
26 |
27 | this.infoWindow = new google.maps.InfoWindow({content: ""});
28 |
29 | this.props.setMarkers(this.props.flags, this.googleMap, this.infoWindow, this.props.notes);
30 |
31 | this.mapLoaded = true;
32 | }
33 |
34 | findFlagsInRange() {
35 | const flagsInRange = this.props.flags.filter(
36 | (flag) => this.props.mapBounds.contains({lat: flag.lat,lng: flag.lng,})
37 | );
38 | this.props.setFlagsInRange(flagsInRange);
39 | }
40 |
41 | componentDidMount() {
42 | this.mapDiv = document.getElementById('map');
43 | }
44 |
45 | componentWillReceiveProps(newProps) {
46 | if (!this.mapLoaded && this.mapDiv && window.google) {
47 | this.setMap();
48 | } else if (this.mapLoaded && (this.props.mapCenter.lat !== newProps.mapCenter.lat || this.props.mapCenter.lng !== newProps.mapCenter.lng)) {
49 | this.googleMap.panTo(newProps.mapCenter);
50 | } else if (this.mapLoaded && this.props.mapBounds && this.props.flags !== newProps.flags) {
51 | this.findFlagsInRange();
52 | }
53 | }
54 |
55 | render() {
56 | return (
57 |
58 | );
59 | }
60 | }
61 |
62 | export default Map;
63 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/default_page.scss:
--------------------------------------------------------------------------------
1 |
2 | @import "variables";
3 |
4 | .default-page {
5 |
6 | .footer-list a:hover, .right-nav a:hover {
7 | color: $light-green;
8 | transition: color 0.2s;
9 | }
10 |
11 | header {
12 | box-sizing: border-box;
13 | width: 100%;
14 | display: flex;
15 | flex-direction: row;
16 | padding: 30px 150px;
17 | border-bottom: 1px solid $light-grey;
18 | justify-content: space-between;
19 | align-items: center;
20 |
21 | .left-nav, .right-nav {
22 | display: flex;
23 | align-items: center;
24 | }
25 |
26 | .left-nav{
27 |
28 | a {
29 | color: $light-green;
30 | font-family: 'Roboto Slab', serif;
31 | font-size: 30px;
32 | }
33 | img {
34 | height: 40px;
35 | margin-right: 10px;
36 | width: auto;
37 | align-self: flex-end;
38 | }
39 | }
40 |
41 | .right-nav {
42 | a:hover {
43 | color: $light-green;
44 | transition: color 0.2s;
45 | }
46 | }
47 | }
48 |
49 | main {
50 | display: flex;
51 | padding: 50px 150px;
52 | min-height: 400px;
53 | }
54 |
55 | section:nth-of-type(1) {
56 | font-size: 70px;
57 | width: 50%;
58 | }
59 |
60 | section:nth-of-type(2) {
61 | margin-left: 50px;
62 | border-right: 1px solid $light-grey;
63 | }
64 |
65 | section:nth-of-type(3) {
66 | margin-right: 50px
67 | }
68 |
69 | section:nth-of-type(4) {
70 | width: 35%;
71 | }
72 |
73 |
74 | footer {
75 | box-sizing: border-box;
76 | width: 100%;
77 | position: absolute;
78 | bottom: 0;
79 |
80 | .footer-list {
81 | display: flex;
82 | border-top: 1px solid $light-grey;
83 | height: 100px;
84 | align-items: center;
85 | justify-content: flex-start;
86 | padding: 0px 150px;
87 |
88 | a {
89 | padding-right: 30px;
90 | }
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/frontend/components/note_index/note_index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import NoteIndexItem from './note_index_item_container';
3 | import NoteOrderOptionMenu from './note_order_options_container';
4 | import DeleteForm from '../entity_forms/delete_form_container';
5 |
6 | class NoteIndex extends React.Component {
7 |
8 | constructor() {
9 | super();
10 | this.state = { searchQuery: "" };
11 | this.handleSearchInput = this.handleSearchInput.bind(this);
12 | }
13 |
14 | componentWillReceiveProps(newProps) {
15 | if (this.props.location.pathname !== newProps.location.pathname) {
16 | this.setState({searchQuery: ""});
17 | }
18 | }
19 |
20 | handleSearchInput(e) {
21 | const newState = {searchQuery: e.target.value};
22 | this.setState(newState);
23 | }
24 |
25 | render () {
26 | let notes = this.props.notes.map((note) => {
27 | if ((note.bodyPlain.toLowerCase().includes(this.state.searchQuery.toLowerCase()))
28 | || (note.title.toLowerCase().includes(this.state.searchQuery.toLowerCase()))) {
29 | return (
30 |
33 | );
34 | }});
35 | if (notes.length < 1) {
36 | notes = Click + to add a new note!
;
37 | }
38 |
39 | return (
40 |
41 |
42 |
43 | {this.props.noteIndexHeader}
44 |
45 |
50 |
51 |
52 |
55 |
56 |
57 | );
58 | }
59 | }
60 |
61 | export default NoteIndex;
62 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/full_forms.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | @-webkit-keyframes fadein {
4 | 0% { background-color: rgba(255, 255, 255, 0);}
5 | 50% { background-color: rgba(255, 255, 255, .5);}
6 | 80% { background-color: rgba(255, 255, 255, 0.7);}
7 | 100% { background-color: rgba(255, 255, 255, 0.8);}
8 | }
9 |
10 |
11 | .full-form-open {
12 | height: 100%;
13 | width: 100%;
14 | top: 0;
15 | left: 0;
16 | position: absolute;
17 | background-color: white;
18 | display: flex;
19 | flex-direction: column;
20 | justify-content: center;
21 | align-items: center;
22 | background-color: rgba(255, 255, 255, 0.9);
23 | overflow: hidden;
24 | z-index: 50;
25 | -webkit-animation: fadein .1s ease-in;
26 |
27 | }
28 |
29 | .full-form {
30 | width: 30%;
31 | height: auto;
32 | display: flex;
33 | flex-direction: column;
34 | align-items: center;
35 |
36 | input {
37 | width: 100%;
38 | margin-top: 20px;
39 | margin-bottom: 20px;
40 | font-size: 26px;
41 | font-family: 'Roboto Slab', serif;
42 | text-align: center;
43 | }
44 | }
45 |
46 | .full-form-message {
47 | margin-top: 20px;
48 | margin-bottom: 10px;
49 | font-size: 26px;
50 | font-family: 'Roboto Slab', serif;
51 | text-align: center;
52 | }
53 |
54 | .full-form-message-title {
55 | margin-bottom: 20px;
56 | font-size: 26px;
57 | font-family: 'Roboto Slab', serif;
58 | text-align: center;
59 | word-wrap: break-word;
60 | }
61 |
62 | .full-form-overlay {
63 | height: 100%;
64 | width: 100%;
65 | top: 0;
66 | left: 0;
67 | position: absolute;
68 | overflow: hidden;
69 | }
70 |
71 | .full-form-header {
72 | color: $darker-grey;
73 | font-size: 20px;
74 | border-bottom: 1px solid $grey;
75 | padding-bottom: 10px;
76 | text-transform: uppercase;
77 | text-align: center;
78 | }
79 |
80 | .full-form-button-container {
81 | margin-top: 10px;
82 | margin-botom: 10px;
83 | display: flex;
84 | flex-direction: row;
85 | flex-wrap: nowrap;
86 | width: 100%
87 | }
88 |
89 | .create-errors {
90 | color: $red;
91 | }
92 |
--------------------------------------------------------------------------------
/frontend/components/note_index/note_index_item.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NavLink } from 'react-router-dom';
3 | import DeleteForm from '../entity_forms/delete_form_container';
4 |
5 | class NoteIndexItem extends React.Component {
6 |
7 | handleClick (id) {
8 | return (e) => {
9 | if (e.target.id === "delete") {
10 | e.stopPropagation();
11 | this.props.toggleDeleteForm(id);
12 | } else {
13 | e.stopPropagation();
14 | let path;
15 | if (this.props.match.params.notebookId) {
16 | path = `/notebooks/${this.props.match.params.notebookId}/notes/${this.props.note.id}`;
17 | } else if (this.props.match.params.tagId) {
18 | path = `/tags/${this.props.match.params.tagId}/notes/${this.props.note.id}`;
19 | } else if (this.props.match.params.flagId) {
20 | path = `/flags/${this.props.match.params.flagId}/notes/${this.props.note.id}`;
21 | } else {
22 | path = `/notes/${this.props.note.id}`;
23 | }
24 | if (this.props.location.pathname !== path) {
25 | this.props.history.push(path);
26 | }
27 | }
28 | };
29 | }
30 |
31 | render () {
32 |
33 | return (
34 |
38 |
39 | {this.props.note.title}
40 | {new Date(this.props.note.updatedAt).toDateString()}
41 | {this.props.bodySnippet}
42 |
43 |
48 |
49 | );
50 | }
51 | }
52 |
53 | export default NoteIndexItem;
54 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Do not eager load code on boot.
10 | config.eager_load = false
11 |
12 | # Show full error reports.
13 | config.consider_all_requests_local = true
14 |
15 | # Enable/disable caching. By default caching is disabled.
16 | if Rails.root.join('tmp/caching-dev.txt').exist?
17 | config.action_controller.perform_caching = true
18 |
19 | config.cache_store = :memory_store
20 | config.public_file_server.headers = {
21 | 'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}"
22 | }
23 | else
24 | config.action_controller.perform_caching = false
25 |
26 | config.cache_store = :null_store
27 | end
28 |
29 | # Don't care if the mailer can't send.
30 | config.action_mailer.raise_delivery_errors = false
31 |
32 | config.action_mailer.perform_caching = false
33 |
34 | # Print deprecation notices to the Rails logger.
35 | config.active_support.deprecation = :log
36 |
37 | # Raise an error on page load if there are pending migrations.
38 | config.active_record.migration_error = :page_load
39 |
40 | # Debug mode disables concatenation and preprocessing of assets.
41 | # This option may cause significant delays in view rendering with a large
42 | # number of complex assets.
43 | config.assets.debug = true
44 |
45 | # Suppress logger output for asset requests.
46 | config.assets.quiet = true
47 |
48 | # Raises error for missing translations
49 | # config.action_view.raise_on_missing_translations = true
50 |
51 | # Use an evented file watcher to asynchronously detect changes in source code,
52 | # routes, locales, etc. This feature depends on the listen gem.
53 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker
54 |
55 |
56 |
57 | end
58 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | git_source(:github) do |repo_name|
4 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
5 | "https://github.com/#{repo_name}.git"
6 | end
7 |
8 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
9 | gem 'rails', '~> 5.1.4'
10 | # Use sqlite3 as the database for Active Record
11 | gem 'pg'
12 | # Use Puma as the app server
13 | gem 'puma', '~> 3.7'
14 | # Use SCSS for stylesheets
15 | gem 'sass-rails', '~> 5.0'
16 | # Use Uglifier as compressor for JavaScript assets
17 | gem 'uglifier', '>= 1.3.0'
18 | # See https://github.com/rails/execjs#readme for more supported runtimes
19 | # gem 'therubyracer', platforms: :ruby
20 |
21 | # Use CoffeeScript for .coffee assets and views
22 | gem 'coffee-rails', '~> 4.2'
23 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
24 | gem 'jbuilder', '~> 2.5'
25 | # Use Redis adapter to run Action Cable in production
26 | # gem 'redis', '~> 3.0'
27 | # Use ActiveModel has_secure_password
28 | # gem 'bcrypt', '~> 3.1.7'
29 |
30 | # Use Capistrano for deployment
31 | # gem 'capistrano-rails', group: :development
32 | gem 'bcrypt'
33 | gem 'jquery-rails'
34 | gem 'aws-sdk', '< 3.0'
35 | gem 'figaro', '~> 1.0'
36 | gem "paperclip", '~> 5.0.0'
37 |
38 |
39 | group :development, :test do
40 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console
41 | gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
42 | # Adds support for Capybara system testing and selenium driver
43 | gem 'capybara', '~> 2.13'
44 | gem 'selenium-webdriver'
45 | gem 'pry-rails'
46 |
47 | end
48 |
49 | group :development do
50 | # Access an IRB console on exception pages or by using <%= console %> anywhere in the code.
51 | gem 'web-console', '>= 3.3.0'
52 | gem 'listen', '>= 3.0.5', '< 3.2'
53 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
54 | gem 'spring'
55 | gem 'spring-watcher-listen', '~> 2.0.0'
56 | gem 'better_errors'
57 | gem 'binding_of_caller'
58 |
59 | end
60 |
61 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
62 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
63 |
--------------------------------------------------------------------------------
/frontend/components/editor/editor_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { withRouter } from 'react-router-dom';
3 | import Editor from './editor';
4 | import { updateNote, createNote, receiveNoteErrors } from '../../actions/note_actions';
5 | import { createTag, receiveTagErrors } from '../../actions/tag_actions';
6 | import { createFlag, receiveFlagErrors } from '../../actions/flag_actions';
7 | import { toggleModal, toggleSelectedNotebook } from '../../actions/ui_actions';
8 | import { createPhoto } from '../../util/photo_api_util.js';
9 | import { sortItems } from '../../util/sorters';
10 |
11 | const mapStateToProps = (state, ownProps) => {
12 | let note;
13 | let flag;
14 | if (ownProps.match.params.noteId) {
15 | note = state.entities.notes[parseInt(ownProps.match.params.noteId)];
16 | } else {
17 | note = { id: null, title: "", body: "", bodyPlain: "", notebookId: state.ui.selectedNotebook.id, tagIds: [], flagId: null };
18 | }
19 |
20 | if (note.flagId) {
21 | flag = state.entities.flags[note.flagId];
22 | } else {
23 | flag = { id: null, placeId: null, title: "", lat: null, lng: null };
24 | }
25 | return {
26 | note,
27 | flag,
28 | selectedNotebook: state.ui.selectedNotebook,
29 | fullEditor: state.ui.fullEditor,
30 | allTags: sortItems(Object.values(state.entities.tags), 4),
31 | allFlags: Object.values(state.entities.flags),
32 | noteErrors: state.errors.noteErrors,
33 | tagErrors: state.errors.tagErrors,
34 | };
35 | };
36 |
37 | const mapDispatchToProps = (dispatch, ownProps) => {
38 | const action = ownProps.match.params.noteId ? updateNote : createNote;
39 | return {
40 | action: (note) => dispatch(action(note)),
41 | toggleFullEditor: () => dispatch(toggleModal("fullEditor")),
42 | createTag: (tag) => dispatch(createTag(tag)),
43 | createFlag: (flag) => dispatch(createFlag(flag)),
44 | clearFlagErrors: () => dispatch(receiveFlagErrors([])),
45 | clearNoteErrors: () => dispatch(receiveNoteErrors([])),
46 | clearTagErrors: () => dispatch(receiveTagErrors([])),
47 | createPhoto: (photoData) => createPhoto(photoData),
48 | toggleMapView: () => dispatch(toggleModal("mapView")),
49 | };
50 | };
51 |
52 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Editor));
53 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/note_index.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .note-index {
4 | position: absolute;
5 | top: 0;
6 | left: 70px;
7 | width: 25%;
8 | height: 100%;
9 | border-right: 1px solid $grey;
10 | display: flex;
11 | flex-direction: column;
12 | justify-content: flex-start;
13 | overflow-y: scroll;
14 | box-sizing: border-box;
15 | transition: left .5s;
16 | }
17 |
18 | .closed-index {
19 | left: -500px;
20 | transition: left .5s;
21 | }
22 |
23 | .note-index-sticky {
24 | position: sticky;
25 | top: 0;
26 | }
27 |
28 |
29 | .note-index-heading {
30 | padding: 20px;
31 | font-size: 20px;
32 | background-color: $darker-grey;
33 | color: white;
34 | min-height: 90px;
35 | font-family: 'Roboto Slab', serif;
36 | overflow: hidden;
37 | text-overflow: ellipsis;
38 | position: sticky;
39 | top: 0;
40 | }
41 |
42 | .note-item {
43 | padding: 0 10px;
44 | border: 3px solid white;
45 | display: flex;
46 | flex-direction: row;
47 | align-items: center;
48 | justify-content: space-between;
49 | height: 100px;
50 | cursor: pointer;
51 | }
52 |
53 | .note-info {
54 | max-width: 90%;
55 | display: flex;
56 | flex-direction: column;
57 | }
58 |
59 | .note-item:hover {
60 | background-color: $light-green;
61 | border-color: $light-green;
62 | transition: background-color 0.2s, border-color 0.2s;
63 |
64 | li, p {
65 | color: white;
66 | }
67 | }
68 |
69 | .note-item.active {
70 | border-color: $grey;
71 | }
72 |
73 |
74 | .note-item-title {
75 | font-family: 'Roboto Slab', serif;
76 | font-size: 16px;
77 | overflow: hidden;
78 | white-space: nowrap;
79 | text-overflow: ellipsis;
80 | }
81 |
82 | .note-item-date {
83 | font-size: 12px;
84 | font-weight: 400;
85 | }
86 |
87 | .note-item-body-snippet {
88 | font-size: 12px;
89 | font-weight: 200;
90 | }
91 |
92 |
93 | .note-trash-icon {
94 | height: 20px;
95 | margin: 5px;
96 | width: auto;
97 | }
98 |
99 | .search-bar {
100 | width: 100%;
101 | background: $light-grey;
102 | color: $darker-grey;
103 | padding: 10px;
104 | min-height: 40px;
105 | overflow-x: hidden;
106 | box-sizing: border-box;
107 | }
108 |
109 | .no-items {
110 | padding: 20px;
111 | font-size: 20px;
112 | color: $dark-grey;
113 | }
114 |
--------------------------------------------------------------------------------
/frontend/reducers/ui_reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | TOGGLE_MODAL,
3 | TOGGLE_NOTE_ORDER,
4 | TOGGLE_SELECTED_NOTEBOOK,
5 | TOGGLE_DELETE_FORM,
6 | TOGGLE_SIDEMENU,
7 | TOGGLE_SIDEMENU_ITEM_TYPE,
8 | TOGGLE_CREATE_FORM,
9 | } from '../actions/ui_actions';
10 | import { RECEIVE_CURRENT_USER } from '../actions/session_actions';
11 | import { merge } from 'lodash';
12 |
13 | const initialState = {
14 | initial: true,
15 | sidemenu: "hidden",
16 | sidemenuItemType: "notebook",
17 | createForm: {itemType: ""},
18 | deleteForm: {id: false, type: ""},
19 | logoutForm: false,
20 | selectedNotebook: {id: null, clicked: false},
21 | fullEditor: false,
22 | notebookDropdown: false,
23 | noteOrder: 0,
24 | noteOrderDropdown: false,
25 | mapView: false,
26 | };
27 |
28 | const UIReducer = (oldState = initialState, action) => {
29 | let newState;
30 | switch (action.type) {
31 | case RECEIVE_CURRENT_USER:
32 | return action.currentUser ? oldState : initialState;
33 | case TOGGLE_SIDEMENU_ITEM_TYPE:
34 | newState = merge({}, oldState);
35 | if (action.itemType === "notebook") {
36 | newState.sidemenuItemType = "notebook";
37 | } else {
38 | newState.sidemenuItemType = "tag";
39 | }
40 | return newState;
41 | case TOGGLE_MODAL:
42 | newState = merge({}, oldState);
43 | newState[action.modalName] = !(newState[action.modalName]);
44 | return newState;
45 | case TOGGLE_SIDEMENU:
46 | newState = merge({}, oldState);
47 | if (newState.sidemenu === "sidemenu-open") {
48 | newState.sidemenu = "closed-sidemenu";
49 | } else {
50 | newState.sidemenu = "sidemenu-open";
51 | }
52 | return newState;
53 | case TOGGLE_DELETE_FORM:
54 | newState = merge({}, oldState);
55 | newState.deleteForm = action.toDelete;
56 | return newState;
57 | case TOGGLE_CREATE_FORM:
58 | newState = merge({}, oldState);
59 | newState.createForm.itemType = action.itemType;
60 | return newState;
61 | case TOGGLE_NOTE_ORDER:
62 | newState = merge({}, oldState);
63 | newState.noteOrder = action.order;
64 | return newState;
65 | case TOGGLE_SELECTED_NOTEBOOK:
66 | newState = merge({}, oldState);
67 | newState.selectedNotebook.id = action.notebookId;
68 | newState.selectedNotebook.clicked = action.clicked;
69 | return newState;
70 | default:
71 | return oldState;
72 | }
73 | };
74 |
75 | export default UIReducer;
76 |
--------------------------------------------------------------------------------
/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers: a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum; this matches the default thread size of Active Record.
6 | #
7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
8 | threads threads_count, threads_count
9 |
10 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
11 | #
12 | port ENV.fetch("PORT") { 3000 }
13 |
14 | # Specifies the `environment` that Puma will run in.
15 | #
16 | environment ENV.fetch("RAILS_ENV") { "development" }
17 |
18 | # Specifies the number of `workers` to boot in clustered mode.
19 | # Workers are forked webserver processes. If using threads and workers together
20 | # the concurrency of the application would be max `threads` * `workers`.
21 | # Workers do not work on JRuby or Windows (both of which do not support
22 | # processes).
23 | #
24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
25 |
26 | # Use the `preload_app!` method when specifying a `workers` number.
27 | # This directive tells Puma to first boot the application and load code
28 | # before forking the application. This takes advantage of Copy On Write
29 | # process behavior so workers use less memory. If you use this option
30 | # you need to make sure to reconnect any threads in the `on_worker_boot`
31 | # block.
32 | #
33 | # preload_app!
34 |
35 | # If you are preloading your application and using Active Record, it's
36 | # recommended that you close any connections to the database before workers
37 | # are forked to prevent connection leakage.
38 | #
39 | # before_fork do
40 | # ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord)
41 | # end
42 |
43 | # The code in the `on_worker_boot` will be called if you are using
44 | # clustered mode by specifying a number of `workers`. After each worker
45 | # process is booted, this block will be run. If you are using the `preload_app!`
46 | # option, you will want to use this block to reconnect to any threads
47 | # or connections that may have been created at application boot, as Ruby
48 | # cannot share connections between processes.
49 | #
50 | # on_worker_boot do
51 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
52 | # end
53 | #
54 |
55 | # Allow puma to be restarted by `rails restart` command.
56 | plugin :tmp_restart
57 |
--------------------------------------------------------------------------------
/frontend/util/marker_util.js:
--------------------------------------------------------------------------------
1 |
2 | export function addNoteTitle(marker, note) {
3 | marker.noteTitles[note.id] = `- ${note.title} `;
4 | return marker;
5 | };
6 |
7 | function removeNoteTitle(marker, noteId) {
8 | delete marker.noteTitles[noteId];
9 | };
10 |
11 | function setNewInfoWIndowContent(flag, marker, notes) {
12 | marker.noteTitles = {};
13 | notes.forEach((note) => {
14 | if (flag.id === note.flagId) {
15 | addNoteTitle(marker, note);
16 | }
17 | });
18 |
19 | marker.infoHeading = flag.noteIds.length > 0 ? `Notes at ${flag.title}: ` : `No notes for ${flag.title} `;
20 |
21 | setInfoWindowContent(marker);
22 | };
23 |
24 | export function setInfoWindowContent (marker) {
25 | marker.infoHeading = Object.values(marker.noteTitles).length > 0 ? `Notes at ${marker.title}: ` : `No notes for ${marker.title} `;
26 |
27 | marker.infoWindowContent =
28 | ``+
29 | `${marker.infoHeading}`+
30 | `
`+
31 | `${Object.values(marker.noteTitles).join("")}`+
32 | ` `+
33 | '
';
34 | }
35 |
36 | export function createMarkers(flags, googleMap, infoWindow, notes) {
37 | const newMarkers = {};
38 | flags.forEach((flag) => {
39 | newMarkers[flag.id] = createMarker(flag, googleMap, infoWindow, notes);
40 | });
41 | return newMarkers;
42 | };
43 |
44 | export function createMarker(flag, googleMap, infoWindow, notes = []) {
45 | const marker = new google.maps.Marker({
46 | position: {
47 | lat: flag.lat,
48 | lng: flag.lng,
49 | },
50 | title: flag.title,
51 | label: `${flag.noteIds.length}`,
52 | map: googleMap,
53 | });
54 |
55 | marker.color = "white";
56 |
57 | setNewInfoWIndowContent(flag, marker, notes);
58 |
59 | marker.addListener('click', () => {
60 | infoWindow.setContent(marker.infoWindowContent);
61 | infoWindow.open(googleMap, marker);
62 | });
63 | return marker;
64 | };
65 |
66 | export function removeMarker(marker) {
67 | marker.setMap(null);
68 | return marker;
69 | };
70 |
71 | export function addNoteToMarker (flagId, marker, note) {
72 | const oldLabel = marker.getLabel();
73 | marker.setLabel(`${parseInt(oldLabel) + 1}`)
74 | addNoteTitle(marker, note);
75 | setInfoWindowContent(marker);
76 | return marker;
77 | }
78 |
79 | export function removeNoteFromMarker(flagId, marker, noteId) {
80 | const oldLabel = marker.getLabel();
81 | marker.setLabel(`${parseInt(oldLabel) - 1}`)
82 | removeNoteTitle(marker, noteId);
83 | setInfoWindowContent(marker);
84 | return marker;
85 | }
86 |
--------------------------------------------------------------------------------
/frontend/components/entity_forms/delete_form.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Modal from 'react-modal';
3 | import { merge } from 'lodash';
4 |
5 | class DeleteForm extends React.Component {
6 |
7 | constructor(props) {
8 | super(props);
9 | this.handleSubmit = this.handleSubmit.bind(this);
10 | this.handleCancel = this.handleCancel.bind(this);
11 | this.closeModal = this.closeModal.bind(this);
12 | }
13 |
14 | redirect(path) {
15 | this.props.history.push(path);
16 | }
17 |
18 | handleSubmit(e) {
19 | if (this.props.itemType === "notebook") {
20 | this.props.toggleSelectedNotebook();
21 | if (parseInt(this.props.match.params.notebookId) === this.props.item.id) {
22 | this.redirect('/notes');
23 | }
24 | } else if (this.props.itemType === "tag") {
25 | if (parseInt(this.props.match.params.tagId) === this.props.item.id) {
26 | this.redirect('/notes');
27 | }
28 | } else if (this.props.itemType === "note") {
29 | if (parseInt(this.props.match.params.noteId) === this.props.item.id) {
30 | if (this.props.match.params.notebookId) {
31 | this.redirect(`/notebooks/${this.props.match.params.notebookId}`);
32 | } else {
33 | this.redirect('/notes');
34 | }
35 | }
36 | } else {
37 | this.redirect('/notes');
38 | }
39 |
40 | this.props.deleteItem(this.props.item.id).then(() => {
41 | this.closeModal();
42 | });
43 | }
44 |
45 | handleCancel(e) {
46 | this.closeModal();
47 | }
48 |
49 | closeModal () {
50 | this.props.toggleDeleteForm({id: false, type: ""});
51 | }
52 |
53 | render() {
54 | const formContent = this.props.item ?
55 | (
56 |
57 |
58 |
{this.props.formTitle}
59 |
{this.props.formMessage}
60 |
{this.props.formMessageTitle}
61 |
62 |
65 | Cancel
66 |
67 |
70 | Delete
71 |
72 |
73 |
74 | ) :
75 |
;
76 |
77 | return (
78 |
84 | {formContent}
85 |
86 | );
87 | }
88 | }
89 |
90 | export default DeleteForm;
91 |
--------------------------------------------------------------------------------
/frontend/components/app.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SideNav from './sidenav/sidenav_container';
3 | import Sidemenu from './sidemenu/sidemenu_container';
4 | import Editor from './editor/editor_container';
5 | import AllNotes from './note_index/all_notes_index_container';
6 | import FilteredNotes from './note_index/filtered_notes_index_container';
7 | import { Route, Switch, Redirect } from 'react-router-dom';
8 | import MDSpinner from 'react-md-spinner';
9 | import CreateForm from './entity_forms/create_form_container';
10 | import LogoutForm from './session/logout_form_container';
11 | import MapView from './map_view/map_view_container';
12 | import NotesInMap from './note_index/notes_in_map_container';
13 |
14 | class App extends React.Component {
15 |
16 | componentDidMount() {
17 | this.props.fetchAll();
18 | }
19 |
20 | render () {
21 | if (this.props.initialState) {
22 | return (
23 |
24 |
Just a moment!
25 |
26 |
27 | );
28 | } else if (this.props.notFound) {
29 | return (
30 |
31 | );
32 | }
33 | else {
34 | return (
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | );}
65 | }
66 | }
67 |
68 | export default App;
69 |
--------------------------------------------------------------------------------
/frontend/reducers/entities/markers_reducer.js:
--------------------------------------------------------------------------------
1 | import { RECEIVE_FLAG, REMOVE_FLAG, RECEIVE_FLAGS } from '../../actions/flag_actions';
2 | import { RECEIVE_CURRENT_USER } from '../../actions/session_actions';
3 | import { RECEIVE_NEW_NOTE, RECEIVE_UPDATED_NOTE, REMOVE_NOTE } from '../../actions/note_actions';
4 | import { createMarkers, createMarker, removeMarker, addNoteToMarker, removeNoteFromMarker, addNoteTitle, setInfoWindowContent } from '../../util/marker_util';
5 | import { merge } from 'lodash';
6 |
7 | const initialState = {googleMap: null, markers: {}, infoWindow: null};
8 |
9 | const MarkersReducer = (oldState = initialState, action) => {
10 | const newState = merge({}, oldState);
11 | let newMarker;
12 | switch (action.type) {
13 | case RECEIVE_CURRENT_USER:
14 | return action.currentUser ? oldState : initialState;
15 | case RECEIVE_FLAGS:
16 | const newMarkers = createMarkers(action.flags, action.googleMap, action.infoWindow, action.notes);
17 | return {
18 | googleMap: action.googleMap,
19 | markers: newMarkers,
20 | infoWindow: action.infoWindow
21 | };
22 | case RECEIVE_FLAG:
23 | newMarker = createMarker(action.flag, newState.googleMap, newState.infoWindow);
24 | newState.markers[action.flag.id] = newMarker;
25 | return newState;
26 | case REMOVE_FLAG:
27 | removeMarker(newState.markers[action.flag.id]);
28 | delete newState.markers[action.flag.id];
29 | return newState;
30 | case RECEIVE_NEW_NOTE:
31 | if (action.flagId) {
32 | newMarker = addNoteToMarker(action.flagId, newState.markers[action.flagId], action.note);
33 | newState.markers[flag.id] = newMarker;
34 | }
35 | return newState;
36 | case RECEIVE_UPDATED_NOTE:
37 | if (action.flags) {
38 | Object.values(action.flags).forEach((flag) => {
39 | const oldNoteIds = Object.keys(newState.markers[flag.id].noteTitles);
40 | const newNoteIds = flag.noteIds.map((noteId) => noteId.toString());
41 | const noteId = action.note.id.toString();
42 | if (oldNoteIds.includes(noteId) && !newNoteIds.includes(noteId)) {
43 | newMarker = removeNoteFromMarker(flag.id, newState.markers[flag.id], action.note.id);
44 | newState.markers[flag.id] = newMarker;
45 | } else if (!oldNoteIds.includes(noteId) && newNoteIds.includes(noteId)) {
46 | newMarker = addNoteToMarker(flag.id, newState.markers[flag.id], action.note);
47 | newState.markers[flag.id] = newMarker;
48 | } else {
49 | newMarker = addNoteTitle(newState.markers[flag.id], action.note);
50 | setInfoWindowContent(newMarker);
51 | newState.markers[flag.id] = newMarker;
52 | }
53 | });
54 | }
55 | return newState;
56 | case REMOVE_NOTE:
57 | if (action.flagId) {
58 | newMarker = removeNoteFromMarker(action.flagId, newState.markers[action.flagId], action.noteId);
59 | newState.markers[action.flagId] = newMarker;
60 | }
61 | return newState;
62 | default:
63 | return oldState;
64 | }
65 | };
66 |
67 | export default MarkersReducer;
68 |
--------------------------------------------------------------------------------
/frontend/components/session/session_form.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { merge } from 'lodash';
3 | import { Link } from 'react-router-dom';
4 |
5 | class SessionForm extends React.Component {
6 |
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | user: { email: "", password: "" },
11 | sessionErrors: [],
12 | };
13 | this.handleSubmit = this.handleSubmit.bind(this);
14 | this.demoLogin = this.demoLogin.bind(this);
15 | }
16 |
17 | componentWillUnmount() {
18 | this.props.clearUserErrors();
19 | }
20 |
21 | componentWillReceiveProps(newProps) {
22 | this.setState({sessionErrors: newProps.sessionErrors});
23 | }
24 |
25 | handleChange(field) {
26 | return (e) => {
27 | const newUser = merge({}, this.state.user);
28 | newUser[field] = e.target.value;
29 | this.setState({user: newUser});
30 | };
31 | }
32 |
33 | handleSubmit (e) {
34 | e.preventDefault();
35 | const newUser = merge({}, this.state.user);
36 | this.props.submitForm(newUser);
37 | }
38 |
39 | demoLogin (e) {
40 | e.preventDefault();
41 | const demoUser = {email: "demo@gmail.com", password: "demodemo"};
42 | this.props.demoLogin(demoUser);
43 | }
44 |
45 | render () {
46 | const formType = this.props.location.pathname === "/login" ? "Log In" : "Sign Up";
47 | let errors = this.state.sessionErrors.map((error, i) => {error} );
48 | let errorPresent = errors.length > 0 ? "error-present" : "";
49 |
50 | let authLink;
51 | let authMessage;
52 | if (this.props.location.pathname === '/login') {
53 | authMessage = Don't have an account?
;
54 | authLink = Sign Up Here;
55 | } else if (this.props.location.pathname === '/signup'){
56 | authMessage = Already signed up?
;
57 | authLink = Log In Here;
58 | }
59 |
60 | return (
61 |
62 |
63 |
{formType}
64 |
65 |
71 |
77 |
{formType}
80 |
81 |
{errors}{authMessage}{authLink}
82 |
Demo Log In
85 |
86 |
87 |
88 | );
89 | }
90 |
91 | }
92 |
93 | export default SessionForm;
94 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/map.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .map-view {
4 | height: 100%;
5 | width: 100%;
6 | box-sizing: border-box;
7 | display: flex;
8 | justify-content: space-between;
9 | z-index: 49;
10 | background-color: rgba(255, 255, 255, 1.0);
11 | position: absolute;
12 | right: 0;
13 | padding: 50px;
14 |
15 | &.closed {
16 | visibility: hidden;
17 | }
18 | }
19 |
20 | .flag-list {
21 | width: 25%;
22 | height: 100%;
23 | overflow-y: scroll;
24 | overflow-x: hidden;
25 | display: flex;
26 | flex-direction: column;
27 | border: 1px solid $grey;
28 |
29 | .flag-list-sticky {
30 | position: sticky;
31 | top: 0;
32 | }
33 |
34 | .flag-list-header {
35 | width: 100%;
36 | height: 150px;
37 | min-height: 150px;
38 | background: $darker-grey;
39 | padding: 30px;
40 | box-sizing: border-box;
41 |
42 | h3 {
43 | font-size: 20px;
44 | color: white;
45 | }
46 | }
47 |
48 | .flag-list-select-area {
49 | padding: 10px;
50 | color: $dark-grey;
51 | background: white;
52 | margin: 0;
53 | width: 100%;
54 | min-height: 40px;
55 | box-sizing: border-box;
56 | border-bottom: 1px solid $grey;
57 |
58 | &:hover {
59 | cursor: pointer;
60 | }
61 | }
62 | }
63 |
64 | .flag-list-item {
65 | width: 100%;
66 | height: 70px;
67 | display: flex;
68 | flex-direction: row;
69 | justify-content: space-between;
70 | align-items: center;
71 | padding: 10px;
72 | box-sizing: border-box;
73 |
74 | .flag-list-item-name {
75 | width: 65%;
76 | align-items: flex-start;
77 | display: flex;
78 | flex-direction: column;
79 |
80 | p:nth-of-type(1) {
81 | font-size: 16px;
82 | margin-bottom: 3px;
83 | }
84 | p:nth-of-type(2) {
85 | font-size: 10px;
86 | }
87 | }
88 |
89 | h4 {
90 | text-align: right;
91 | }
92 |
93 | &:hover {
94 | cursor: pointer;
95 | background-color: $light-green;
96 | transition: background-color 0.2s;
97 |
98 | p, h4 {
99 | color: white;
100 | }
101 |
102 | }
103 |
104 | > div {
105 | display: flex;
106 | align-items: center;
107 |
108 | .flag-trash-icon {
109 | height: 20px;
110 | margin: 5px;
111 | width: auto;
112 | }
113 | }
114 |
115 | }
116 |
117 | .map-div {
118 | width: 75%;
119 | height: 100%;
120 | display: flex;
121 | flex-direction: column;
122 | justify-content: flex-start;
123 | border: 1px solid $grey;
124 |
125 | .map-inputs {
126 | margin: 20px 0;
127 | padding: 0 15px;
128 | display: flex;
129 | justify-content: space-between;
130 | align-items: center;
131 |
132 | #location-search-input-map {
133 | padding: 7px;
134 | border: 1px solid $grey;
135 | border-radius: 5px;
136 | }
137 |
138 | button {
139 | }
140 | }
141 |
142 | #map {
143 | height: 100%;
144 | width: 100%;
145 | }
146 | }
147 |
148 | #location-search-input-editor {
149 | padding: 7px;
150 | border: 1px solid $grey;
151 | border-radius: 5px;
152 | }
153 |
--------------------------------------------------------------------------------
/frontend/components/sidemenu/sidemenu.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SidemenuIndexItem from './sidemenu_index_item_container';
3 | import CreateForm from '../entity_forms/create_form_container';
4 | import DeleteForm from '../entity_forms/delete_form_container';
5 | import SidemenuHeading from './sidemenu_heading';
6 |
7 | class Sidemenu extends React.Component {
8 |
9 | constructor(props) {
10 | super(props);
11 | this.state = { searchQuery: "" };
12 | this.handleSearchInput = this.handleSearchInput.bind(this);
13 | this.toggleCreateForm = this.toggleCreateForm.bind(this);
14 | }
15 |
16 | componentWillReceiveProps(newProps) {
17 | if (this.props.sidemenuOpen !== newProps.sidemenuOpen) {
18 | this.setState({searchQuery: ""});
19 | }
20 | }
21 |
22 | handleSearchInput(e) {
23 | const newState = {searchQuery: e.target.value};
24 | this.setState(newState);
25 | }
26 |
27 | toggleCreateForm(itemType) {
28 | return (e) => {
29 | this.props.toggleCreateForm(itemType);
30 | };
31 | }
32 |
33 | queriedItemsByFirstLetter() {
34 | const allItems = [];
35 | let firstItem = true;
36 | this.props.items.forEach((item, i) => {
37 | if (item.title.toLowerCase().includes(this.state.searchQuery.toLowerCase())) {
38 | if (firstItem || this.props.items[i - 1].title.slice(0, 1).toUpperCase() !== item.title.slice(0, 1).toUpperCase()) {
39 | allItems.push(item.title.slice(0, 1).toUpperCase());
40 | firstItem = false;
41 | }
42 | allItems.push(item);
43 | }
44 | });
45 | return allItems;
46 | }
47 |
48 | render () {
49 | return (
50 |
84 | );
85 | }
86 | }
87 |
88 | export default Sidemenu;
89 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/editor.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .editor-heading {
4 | max-width: 100%;
5 | display: flex;
6 | justify-content: space-between;
7 | align-items: flex-start;
8 | padding-bottom: 15px;
9 |
10 | .editor-dropdowns {
11 | display: flex;
12 | flex-direction: column;
13 | margin-top: 5px;
14 | }
15 |
16 | .editor-address {
17 | font-size: 14px;
18 | }
19 | }
20 |
21 | .editor-lower-heading {
22 | width: 100%;
23 | border-top: 1px solid $light-grey;
24 | display: flex;
25 | justify-content: space-between;
26 | margin: 10px 0;
27 | }
28 |
29 | .tags {
30 | display: flex;
31 | width: calc(100% - 300px);
32 | }
33 |
34 | .tags-label {
35 | min-width: 170px;
36 | display: flex;
37 | flex-direction: column;
38 | align-items: flex-end;
39 | margin-top: 5px;
40 | }
41 |
42 | .tag-input {
43 | margin-top: 20px;
44 | text-align: right;
45 | }
46 |
47 | .tag-list {
48 | width: 100%;
49 | max-height: 75px;
50 | min-height: 75px;
51 | display: flex;
52 | flex-wrap: wrap;
53 | align-items: flex-start;
54 | overflow-y: scroll;
55 | }
56 |
57 | .tag-button {
58 | height: auto;
59 | background: $light-grey;
60 | margin: 5px;
61 | border-radius: 5px;
62 | padding: 3px;
63 | text-decoration: line-through;
64 | transition: background 0.2s, color 0.3s;
65 | }
66 |
67 | .tag-button.selected {
68 | background: $light-green;
69 | color: white;
70 | text-decoration: none;
71 | transition: color 0.3s;
72 | transition: background 0.2s, color 0.3s;
73 | }
74 |
75 | .editor-buttons {
76 | display: flex;
77 | flex-direction: row;
78 | align-items: center;
79 | position: fixed;
80 | right: 20px;
81 | }
82 |
83 | .title {
84 | font-family: 'Roboto Slab', serif;
85 | font-size: 26px;
86 | color: $light-green;
87 | width: calc(100% - 300px);
88 | padding: 10px 0;
89 |
90 | }
91 |
92 | .editor-errors li {
93 | color: $red;
94 | width: auto;
95 | }
96 |
97 | .note-editor {
98 | position: absolute;
99 | left: calc(25% + 70px);
100 | height: 100%;
101 | width: calc(100% - 25% - 70px);
102 | padding: 20px;
103 | box-sizing: border-box;
104 | background: white;
105 | display: flex;
106 | flex-direction: column;
107 | transition: left .5s, width .5s;
108 | }
109 |
110 | .note-editor.full-editor {
111 | width: 100%;
112 | left: 0;
113 | transition: left .5s, width .5s;
114 | }
115 |
116 |
117 | .note-editor-quill {
118 | width: 100%;
119 | height: 90%;
120 | box-sizing: border-box;
121 | background: white;
122 | display: flex;
123 | flex-direction: column;
124 | margin-bottom: 40px;
125 | margin-top: 10px;
126 | border-style: none;
127 | }
128 |
129 | .title::-webkit-input-placeholder {
130 | font-family: 'Roboto Slab', serif;
131 | font-size: 30px;
132 | color: $light-green;
133 | }
134 |
135 | .saved {
136 | visibility: visible;
137 | display: flex;
138 | justify-content: center;
139 | align-items: center;
140 | color: $light-green;
141 | }
142 |
143 | .ql-snow.ql-container {
144 | border-style: none;
145 | }
146 |
147 | .ql-snow.ql-toolbar {
148 | border-style: none;
149 | border-bottom: 1px solid $light-grey;
150 | }
151 |
152 | .ql-editor strong {
153 | font-weight: 700;
154 | }
155 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead
2 | # of editing this file, please use the migrations feature of Active Record to
3 | # incrementally modify your database, and then regenerate this schema definition.
4 | #
5 | # Note that this schema.rb definition is the authoritative source for your
6 | # database schema. If you need to create the application database on another
7 | # system, you should be using db:schema:load, not running all the migrations
8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
9 | # you'll amass, the slower it'll run and the greater likelihood for issues).
10 | #
11 | # It's strongly recommended that you check this file into your version control system.
12 |
13 | ActiveRecord::Schema.define(version: 20171225013511) do
14 |
15 | # These are extensions that must be enabled in order to support this database
16 | enable_extension "plpgsql"
17 |
18 | create_table "flags", force: :cascade do |t|
19 | t.string "place_id", null: false
20 | t.string "title", null: false
21 | t.integer "user_id", null: false
22 | t.float "lat", null: false
23 | t.float "lng", null: false
24 | t.datetime "updated_at", null: false
25 | t.datetime "created_at", null: false
26 | t.string "formatted_address"
27 | t.index ["place_id", "user_id"], name: "index_flags_on_place_id_and_user_id", unique: true
28 | end
29 |
30 | create_table "notebooks", force: :cascade do |t|
31 | t.string "title", null: false
32 | t.integer "user_id", null: false
33 | t.datetime "created_at", null: false
34 | t.datetime "updated_at", null: false
35 | t.index ["user_id"], name: "index_notebooks_on_user_id"
36 | end
37 |
38 | create_table "notes", force: :cascade do |t|
39 | t.string "title", null: false
40 | t.text "body"
41 | t.integer "notebook_id", null: false
42 | t.datetime "created_at", null: false
43 | t.datetime "updated_at", null: false
44 | t.text "body_plain"
45 | t.integer "flag_id"
46 | t.index ["flag_id"], name: "index_notes_on_flag_id"
47 | t.index ["notebook_id"], name: "index_notes_on_notebook_id"
48 | end
49 |
50 | create_table "photos", id: :serial, force: :cascade do |t|
51 | t.string "image_file_name"
52 | t.string "image_content_type"
53 | t.integer "image_file_size"
54 | t.datetime "image_updated_at"
55 | t.integer "user_id", null: false
56 | end
57 |
58 | create_table "taggings", force: :cascade do |t|
59 | t.integer "note_id", null: false
60 | t.integer "tag_id", null: false
61 | t.datetime "created_at", null: false
62 | t.datetime "updated_at", null: false
63 | end
64 |
65 | create_table "tags", force: :cascade do |t|
66 | t.string "title", null: false
67 | t.integer "user_id", null: false
68 | t.datetime "created_at", null: false
69 | t.datetime "updated_at", null: false
70 | end
71 |
72 | create_table "users", force: :cascade do |t|
73 | t.string "email", null: false
74 | t.string "session_token", null: false
75 | t.string "password_digest", null: false
76 | t.datetime "created_at", null: false
77 | t.datetime "updated_at", null: false
78 | t.index ["email"], name: "index_users_on_email", unique: true
79 | t.index ["session_token"], name: "index_users_on_session_token"
80 | end
81 |
82 | end
83 |
--------------------------------------------------------------------------------
/frontend/components/entity_forms/create_form.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Modal from 'react-modal';
3 | import { merge } from 'lodash';
4 |
5 | class CreateForm extends React.Component {
6 |
7 | constructor(props) {
8 | super(props);
9 | this.state = {title: ""};
10 | this.handleSubmit = this.handleSubmit.bind(this);
11 | this.handleCancel = this.handleCancel.bind(this);
12 | this.handleChange = this.handleChange.bind(this);
13 | this.closeModal = this.closeModal.bind(this);
14 | this.handleEnter = this.handleEnter.bind(this);
15 | this.inputField = null;
16 | }
17 |
18 | redirect (id) {
19 | this.props.history.push(`/${this.props.itemType}s/${id}`);
20 | }
21 |
22 | handleSubmit(e) {
23 | e.preventDefault();
24 | const newItem = merge({}, this.state);
25 | this.props.createItem(newItem).then((res) => {
26 | if (this.props.match.params.noteId) {
27 | this.redirect(
28 | res[this.props.itemType].id
29 | );
30 | } else {
31 | this.props.toggleSelectedNotebook(res[this.props.itemType].id);
32 | }
33 | this.closeModal();
34 | });
35 | }
36 |
37 | handleCancel(e) {
38 | e.preventDefault();
39 | this.closeModal();
40 | }
41 |
42 | handleEnter(e) {
43 | if (e.key === 'Enter') {
44 | e.preventDefault();
45 | this.handleSubmit(e);
46 | }
47 | }
48 |
49 | handleChange (e) {
50 | this.setState({ title: e.target.value });
51 | }
52 |
53 | closeModal () {
54 | this.setState({ title: "" });
55 | this.props.clearItemErrors();
56 | if (this.props.notebookDropdownOpen) {
57 | this.props.toggleNotebookDropdown();
58 | }
59 | this.props.toggleCreateForm("");
60 | }
61 |
62 | componentDidUpdate(prevProps) {
63 | if (!prevProps.createFormType && this.props.createFormType) {
64 | this.inputField.focus();
65 | }
66 | }
67 |
68 | render() {
69 | let errors = this.props.errors || [];
70 | errors = errors.map((error) => {error} );
71 | return (
72 |
78 |
103 |
104 | );
105 | }
106 | }
107 |
108 | export default CreateForm;
109 |
--------------------------------------------------------------------------------
/frontend/components/editor/notebook_dropdown.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class NotebookDropdown extends React.Component {
4 | constructor(props) {
5 | super(props);
6 | this.toggleNotebookDropdown = this.toggleNotebookDropdown.bind(this);
7 | }
8 |
9 | componentDidMount() {
10 | if (this.props.match.params.notebookId) {
11 | this.props.toggleSelectedNotebook(this.props.match.params.notebookId, false);
12 | } else if (this.props.match.params.noteId) {
13 | const notebookId = this.props.notes[this.props.match.params.noteId].notebookId;
14 | this.props.toggleSelectedNotebook(notebookId, false);
15 | }
16 | }
17 |
18 | componentWillReceiveProps(newProps) {
19 | const currentNotebookId = this.props.match.params.notebookId;
20 | const currentNoteId = this.props.match.params.noteId;
21 | const newNotebookId = newProps.match.params.notebookId;
22 | const newNoteId = newProps.match.params.noteId
23 |
24 | // toggle to new notebook if visiting a different notebook
25 | if (newNotebookId && currentNotebookId !== newNotebookId) {
26 | this.props.toggleSelectedNotebook(newNotebookId, false);
27 | }
28 | // toggle to new notebook if visiting a different note without :notebookId param
29 | else if (!newNotebookId && newNoteId && currentNoteId !== newNoteId) {
30 | this.props.toggleSelectedNotebook(this.props.notes[newNoteId].notebookId, false);
31 | // toggle to no notebook if going to all notes
32 | } else if (!this.props.location.pathname === "/notes" && newProps.location.pathname === "/notes") {
33 | this.props.toggleSelectedNotebook(null, false);
34 | }
35 | }
36 |
37 | toggleNotebookDropdown (e) {
38 | e.stopPropagation();
39 | this.props.toggleNotebookDropdown();
40 | }
41 |
42 | handleNotebookSelection(notebookId) {
43 | return (e) => {
44 | e.stopPropagation();
45 | this.props.toggleNotebookDropdown();
46 | this.props.toggleSelectedNotebook(notebookId, true);
47 | }
48 | }
49 |
50 | buttonText() {
51 | let buttonText;
52 | if (this.props.selectedNotebook.id) {
53 | const notebook = this.props.allNotebooks.filter((notebook) => (
54 | notebook.id === parseInt(this.props.selectedNotebook.id)
55 | ));
56 | if (notebook.length > 0) {
57 | buttonText = notebook[0].title;
58 | }
59 | } else {
60 | buttonText = "Select Notebook";
61 | }
62 | return buttonText;
63 | }
64 |
65 | render () {
66 | const notebooks = this.props.allNotebooks.map((notebook, i) => (
67 |
71 | {notebook.title}
72 | ));
73 | notebooks.push(
74 |
78 | Add New Notebook
79 |
80 | );
81 | return (
82 |
85 |
86 | ▾ {this.buttonText()}
87 |
88 |
94 |
97 |
98 |
);
99 | }
100 | }
101 |
102 | export default NotebookDropdown;
103 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both threaded web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Attempt to read encrypted secrets from `config/secrets.yml.enc`.
18 | # Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or
19 | # `config/secrets.yml.key`.
20 | config.read_encrypted_secrets = true
21 |
22 | # Disable serving static files from the `/public` folder by default since
23 | # Apache or NGINX already handles this.
24 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
25 |
26 | # Compress JavaScripts and CSS.
27 | config.assets.js_compressor = :uglifier
28 | # config.assets.css_compressor = :sass
29 |
30 | # Do not fallback to assets pipeline if a precompiled asset is missed.
31 | config.assets.compile = false
32 |
33 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
34 |
35 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
36 | # config.action_controller.asset_host = 'http://assets.example.com'
37 |
38 | # Specifies the header that your server uses for sending files.
39 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
40 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
41 |
42 | # Mount Action Cable outside main process or domain
43 | # config.action_cable.mount_path = nil
44 | # config.action_cable.url = 'wss://example.com/cable'
45 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
46 |
47 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
48 | # config.force_ssl = true
49 |
50 | # Use the lowest log level to ensure availability of diagnostic information
51 | # when problems arise.
52 | config.log_level = :debug
53 |
54 | # Prepend all log lines with the following tags.
55 | config.log_tags = [ :request_id ]
56 |
57 | # Use a different cache store in production.
58 | # config.cache_store = :mem_cache_store
59 |
60 | # Use a real queuing backend for Active Job (and separate queues per environment)
61 | # config.active_job.queue_adapter = :resque
62 | # config.active_job.queue_name_prefix = "Omninote_#{Rails.env}"
63 | config.action_mailer.perform_caching = false
64 |
65 | # Ignore bad email addresses and do not raise email delivery errors.
66 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
67 | # config.action_mailer.raise_delivery_errors = false
68 |
69 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
70 | # the I18n.default_locale when a translation cannot be found).
71 | config.i18n.fallbacks = true
72 |
73 | # Send deprecation notices to registered listeners.
74 | config.active_support.deprecation = :notify
75 |
76 | # Use default logging formatter so that PID and timestamp are not suppressed.
77 | config.log_formatter = ::Logger::Formatter.new
78 |
79 | # Use a different logger for distributed setups.
80 | # require 'syslog/logger'
81 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
82 |
83 | if ENV["RAILS_LOG_TO_STDOUT"].present?
84 | logger = ActiveSupport::Logger.new(STDOUT)
85 | logger.formatter = config.log_formatter
86 | config.logger = ActiveSupport::TaggedLogging.new(logger)
87 | end
88 |
89 | # Do not dump schema after migrations.
90 | config.active_record.dump_schema_after_migration = false
91 |
92 | end
93 |
--------------------------------------------------------------------------------
/frontend/components/map_view/map_view.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Map from './map_container';
3 | import LocationSearch from './location_search';
4 | import DeleteForm from '../entity_forms/delete_form_container';
5 |
6 | class MapView extends React.Component {
7 |
8 | constructor(props) {
9 | super(props);
10 | this.setFlagsInRange = this.setFlagsInRange.bind(this);
11 | this.state = {flagsInRange: [], mapCenter: {lat: 40.7128, lng: -74.0060}, mapBounds: null};
12 |
13 | this.setMapCenter = this.setMapCenter.bind(this);
14 | this.getNotesInRange = this.getNotesInRange.bind(this);
15 | this.updateBounds = this.updateBounds.bind(this);
16 | }
17 |
18 | getUserLocation() {
19 | navigator.geolocation.getCurrentPosition(
20 | (pos) => this.setMapCenter(pos.coords.latitude, pos.coords.longitude),
21 | (a) => this.setMapCenter({lat: 40.7128, lng: -74.0060})
22 | );
23 | }
24 |
25 | setMapCenter(lat, lng) {
26 | this.setState({mapCenter: {lat, lng}});
27 | }
28 |
29 | setFlagsInRange(flagsInRange) {
30 | this.setState({flagsInRange});
31 | }
32 |
33 | updateBounds(mapBounds) {
34 | this.setState({mapBounds});
35 | }
36 |
37 | handleClick (flagId) {
38 | return (e) => {
39 | if (e.target.id === "delete") {
40 | this.props.toggleDeleteForm(flagId);
41 | e.stopPropagation();
42 | } else {
43 | this.props.toggleMapView();
44 | const path = `/flags/${flagId}`;
45 | if (this.props.location.pathname !== path) {
46 | this.props.history.push(path);
47 | }
48 | e.stopPropagation();
49 | }
50 | };
51 | }
52 |
53 | getNotesInRange() {
54 | this.props.toggleMapView();
55 | let query;
56 | if (this.state.flagsInRange.length > 0) {
57 | query = this.state.flagsInRange.map((flag) => flag.id).join(",");
58 | } else {
59 | query = "noflags";
60 | }
61 | this.props.history.push(`/searchbylocation/${query}`);
62 | }
63 |
64 | componentDidMount() {
65 | this.getUserLocation();
66 | }
67 |
68 | render() {
69 |
70 | const flagsInRange = this.state.flagsInRange.map((flag, i) => {
71 | const notePluralized = flag.noteIds.length === 1 ? "note" : "notes";
72 | return (
73 |
74 |
75 |
{flag.title}
76 |
{flag.formattedAddress}
77 |
78 |
79 |
{`${flag.noteIds.length} ${notePluralized}`}
80 |
84 |
85 |
86 | );
87 | });
88 |
89 | return (
90 |
92 |
93 |
94 |
95 |
Flags
96 |
97 |
100 | See all notes in this area
101 |
102 |
103 |
104 | {flagsInRange}
105 |
106 |
107 |
108 |
109 |
112 | Close
115 |
116 |
123 |
124 |
125 |
126 | );
127 | }
128 | }
129 |
130 | export default MapView;
131 |
--------------------------------------------------------------------------------
/frontend/components/sidenav/sidenav.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import Sidemenu from '../sidemenu/sidemenu_container';
4 |
5 | class SideNav extends React.Component {
6 |
7 | constructor (props) {
8 | super(props);
9 | this.redirectToAllNotes = this.redirectToAllNotes.bind(this);
10 | this.newNote = this.newNote.bind(this);
11 | this.toggleSidemenu = this.toggleSidemenu.bind(this);
12 | this.toggleLogoutForm = this.props.toggleLogoutForm.bind(this);
13 | this.toggleMapView = this.toggleMapView.bind(this);
14 | }
15 |
16 | redirectToAllNotes() {
17 | if (this.props.sidemenuOpen) {
18 | this.props.toggleSidemenu();
19 | }
20 | if (this.props.location.pathname !== "/notes") {
21 | this.props.history.push('/notes');
22 | }
23 | }
24 |
25 | newNote () {
26 | let path;
27 | if (this.props.match.params.notebookId) {
28 | path = `/notebooks/${this.props.match.params.notebookId}`;
29 | } else {
30 | path = '/notes';
31 | }
32 | if (path !== this.props.location.pathname) {
33 | this.props.history.push(path);
34 | }
35 | if (this.props.sidemenuOpen) {
36 | this.props.toggleSidemenu();
37 | }
38 | this.props.toggleFullEditor();
39 | }
40 |
41 | toggleSidemenu(itemType) {
42 | return (e) => {
43 | if ((itemType === this.props.sidemenuItemType) && (this.props.sidemenuOpen)) {
44 | this.props.toggleSidemenu();
45 | } else if (!this.props.sidemenuOpen) {
46 | this.props.toggleSidemenu();
47 | }
48 | if (itemType === "tag") {
49 | this.props.toggleSidemenuItemType("tag");
50 | } else if (itemType === "notebook"){
51 | this.props.toggleSidemenuItemType("notebook");
52 | }
53 | };
54 | }
55 |
56 | toggleMapView() {
57 | if (this.props.sidemenuOpen) {
58 | this.props.toggleSidemenu();
59 | }
60 | this.props.toggleMapView();
61 | }
62 |
63 |
64 | render () {
65 | return (
66 |
68 |
69 |
70 |
73 |
74 |
75 |
78 |
81 | New Note
82 |
83 |
84 |
85 |
88 |
91 | All Notes
92 |
93 |
94 |
95 |
98 |
101 | Notebooks
102 |
103 |
104 |
105 |
108 |
111 | Tags
112 |
113 |
114 |
115 |
118 |
121 | Flags
122 |
123 |
124 |
125 |
128 |
131 | Log Out: {this.props.currentUser}
132 |
133 |
134 |
135 |
136 | );
137 | }
138 | }
139 |
140 | export default SideNav;
141 |
142 |
143 | // ]
144 |
--------------------------------------------------------------------------------