├── 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 | 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 | 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 | 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 |
    {props.noteErrors}
15 |
{props.saved ? "Saved!" : ""}
16 | 20 | 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 | ) 29 | ); 30 | return ( 31 | [ 32 |
33 |
36 | Options ▾ 37 |
    38 | {options} 39 |
40 |
41 |
, 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 | 41 | 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 | 20 | 23 |
    24 |
    25 |
    26 | Meet Omninote, your second brain. 27 |
    28 |
    29 |
    30 |
    31 |
    32 |
    33 | 34 |
    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 |
      53 | {notes} 54 |
    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 | 67 | 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 | 80 |

    81 |
      {errors}{authMessage}{authLink}
    82 | 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 |
    51 |
    54 |
    55 |
    57 |
    58 | 59 | 64 |
    65 |
      66 | {this.props.items.length < 1 ? (
      No {this.props.itemType}s yet!
      ) : this.queriedItemsByFirstLetter().map((item, i) => { 67 | if (typeof item !== "string") { 68 | return ( 69 | 73 | ); 74 | } else { 75 | return ( 76 |
    • {item}
    • 77 | ); 78 | } 79 | })} 80 |
    81 |
    82 | 83 |
    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 |
    81 |
    82 |
    Create {this.props.itemType}
    83 | {this.inputField = input; }}/> 89 |
    90 | 93 | 98 |
    99 |
      100 | {errors} 101 |
    102 |
    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 |
    90 |
      91 | {notebooks} 92 |
    93 |
    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 | 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 | 136 | ); 137 | } 138 | } 139 | 140 | export default SideNav; 141 | 142 | 143 | // ] 144 | --------------------------------------------------------------------------------