├── log └── .keep ├── .rspec ├── app ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── concerns │ │ └── .keep │ ├── note_tag.rb │ ├── notebook.rb │ ├── tag.rb │ ├── friend_request.rb │ ├── comment.rb │ ├── like.rb │ ├── note.rb │ ├── friendship.rb │ ├── notification.rb │ └── user.rb ├── assets │ ├── images │ │ ├── .keep │ │ ├── green_icon.png │ │ └── header-background-green.png │ ├── .DS_Store │ ├── javascripts │ │ ├── models │ │ │ ├── filter.js │ │ │ ├── tag.js │ │ │ ├── comment.js │ │ │ ├── user.js │ │ │ ├── note_tag.js │ │ │ ├── like.js │ │ │ ├── notebook.js │ │ │ └── note.js │ │ ├── views │ │ │ ├── tags │ │ │ │ ├── tags_index.js │ │ │ │ ├── tags_show.js │ │ │ │ ├── tag_delete.js │ │ │ │ └── tag_form.js │ │ │ ├── users │ │ │ │ ├── users_index.js │ │ │ │ └── unfriend.js │ │ │ ├── static_pages │ │ │ │ ├── about.js │ │ │ │ ├── sidebar_item.js │ │ │ │ ├── navbar.js │ │ │ │ └── search_bar.js │ │ │ ├── notebooks │ │ │ │ ├── notebooks_show.js │ │ │ │ ├── corgi.js │ │ │ │ ├── notebook_delete.js │ │ │ │ └── notebook_form.js │ │ │ ├── notes │ │ │ │ ├── note_preview.js │ │ │ │ ├── note_delete.js │ │ │ │ ├── note_info.js │ │ │ │ ├── friend_note_show.js │ │ │ │ ├── note_show.js │ │ │ │ └── notes_index.js │ │ │ ├── likes │ │ │ │ └── like_show.js │ │ │ ├── note_tags │ │ │ │ └── note_tags_index.js │ │ │ └── comments │ │ │ │ └── comments_index.js │ │ ├── collections │ │ │ ├── users.js │ │ │ ├── tags.js │ │ │ ├── likes.js │ │ │ ├── note_tags.js │ │ │ ├── notebooks.js │ │ │ ├── comments.js │ │ │ └── notes.js │ │ ├── application.js │ │ ├── better_note.js │ │ └── routers │ │ │ └── notes_router.js │ ├── fonts │ │ └── CaeciliaLTStd-Roman.ttf │ ├── templates │ │ ├── likes │ │ │ └── show.jst.ejs │ │ ├── static_pages │ │ │ ├── corgi.jst.ejs │ │ │ ├── about.jst.ejs │ │ │ ├── navbar.jst.ejs │ │ │ ├── sidebar_item.jst.ejs │ │ │ ├── search_bar.jst.ejs │ │ │ └── sidebar.jst.ejs │ │ ├── notes │ │ │ ├── delete.jst.ejs │ │ │ ├── preview.jst.ejs │ │ │ ├── friend_show.jst.ejs │ │ │ ├── info.jst.ejs │ │ │ ├── index.jst.ejs │ │ │ └── show.jst.ejs │ │ ├── tags │ │ │ ├── delete.jst.ejs │ │ │ ├── form.jst.ejs │ │ │ └── show.jst.ejs │ │ ├── users │ │ │ └── unfriend.jst.ejs │ │ ├── notebooks │ │ │ ├── delete.jst.ejs │ │ │ ├── form.jst.ejs │ │ │ ├── show.jst.ejs │ │ │ └── index.jst.ejs │ │ ├── comments │ │ │ ├── comment_form.jst.ejs │ │ │ └── index.jst.ejs │ │ └── note_tags │ │ │ └── index.jst.ejs │ └── stylesheets │ │ ├── notes_list.css │ │ ├── application.css.scss │ │ ├── modal.css │ │ ├── sidebar.css │ │ └── public.css ├── controllers │ ├── concerns │ │ └── .keep │ ├── api │ │ ├── placeholder.txt │ │ ├── likes_controller.rb │ │ ├── friendships_controller.rb │ │ ├── comments_controller.rb │ │ ├── note_tags_controller.rb │ │ ├── tags_controller.rb │ │ ├── notebooks_controller.rb │ │ └── notes_controller.rb │ ├── .DS_Store │ ├── static_pages_controller.rb │ ├── friendships_controller.rb │ ├── likes_controller.rb │ ├── sessions_controller.rb │ ├── comments_controller.rb │ ├── application_controller.rb │ ├── friend_requests_controller.rb │ ├── tags_controller.rb │ ├── users_controller.rb │ ├── notebooks_controller.rb │ └── notes_controller.rb ├── views │ ├── static_pages │ │ ├── _nav.html.erb │ │ ├── home.html.erb │ │ ├── root.html.erb │ │ ├── _notifications.html.erb │ │ ├── _search_bar.html.erb │ │ └── _sidebar.html.erb │ ├── users │ │ ├── _show.json.jbuilder │ │ ├── _friends.json.jbuilder │ │ ├── _index.json.jbuilder │ │ ├── _friends.html.erb │ │ ├── show.html.erb │ │ ├── new.html.erb │ │ └── index.html.erb │ ├── tags │ │ ├── _show.json.jbuilder │ │ ├── index.json.jbuilder │ │ ├── edit.html.erb │ │ ├── new.html.erb │ │ ├── _form.html.erb │ │ ├── show.html.erb │ │ └── _index.html.erb │ ├── .DS_Store │ ├── notebooks │ │ ├── _show.json.jbuilder │ │ ├── index.json.jbuilder │ │ ├── edit.html.erb │ │ ├── new.html.erb │ │ ├── _form.html.erb │ │ ├── show.html.erb │ │ └── _index.html.erb │ ├── notes │ │ ├── _index.json.jbuilder │ │ ├── index.html.erb │ │ ├── _new.html.erb │ │ ├── _preview.html.erb │ │ ├── _show.html.erb │ │ ├── _index.html.erb │ │ ├── _show.json.jbuilder │ │ ├── _list_footer.html.erb │ │ ├── show.html.erb │ │ ├── _info.html.erb │ │ ├── _edit.html.erb │ │ ├── edit.html.erb │ │ ├── _comments.html.erb │ │ └── _show_header.html.erb │ ├── note_tags │ │ └── _show.json.jbuilder │ ├── comments │ │ ├── new.html.erb │ │ └── _form.html.erb │ ├── sessions │ │ └── new.html.erb │ └── layouts │ │ ├── public.html.erb │ │ └── application.html.erb ├── .DS_Store └── helpers │ └── application_helper.rb ├── lib ├── assets │ └── .keep └── tasks │ ├── .keep │ └── scheduler.rake ├── public ├── favicon.ico ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── test ├── helpers │ ├── .keep │ ├── users_helper_test.rb │ └── sessions_helper_test.rb ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── note_test.rb │ └── user_test.rb ├── controllers │ ├── .keep │ ├── users_controller_test.rb │ └── sessions_controller_test.rb ├── fixtures │ ├── .keep │ ├── notes.yml │ └── users.yml ├── integration │ └── .keep └── test_helper.rb ├── vendor └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep ├── bin ├── rake ├── bundle └── rails ├── spec ├── controllers │ ├── likes_controller_spec.rb │ ├── notes_controller_spec.rb │ ├── tags_controller_spec.rb │ ├── comments_controller_spec.rb │ ├── note_tags_controller_spec.rb │ ├── notebooks_controller_spec.rb │ ├── friendships_controller_spec.rb │ ├── static_pages_controller_spec.rb │ └── friend_requests_controller_spec.rb ├── models │ ├── notification_spec.rb │ ├── friendship_spec.rb │ ├── friend_request_spec.rb │ ├── notebook_spec.rb │ ├── like_spec.rb │ ├── tag_spec.rb │ ├── note_tag_spec.rb │ ├── comment_spec.rb │ ├── note_spec.rb │ └── user_spec.rb ├── factories.rb └── spec_helper.rb ├── config.ru ├── config ├── environment.rb ├── initializers │ ├── session_store.rb │ ├── filter_parameter_logging.rb │ ├── mime_types.rb │ ├── backtrace_silencers.rb │ ├── wrap_parameters.rb │ ├── inflections.rb │ └── secret_token.rb ├── boot.rb ├── locales │ └── en.yml ├── application.rb ├── environments │ ├── development.rb │ ├── test.rb │ └── production.rb ├── routes.rb └── database.yml ├── Rakefile ├── db └── migrate │ ├── 20140421233947_create_notebooks.rb │ ├── 20140422004736_create_tags.rb │ ├── 20140422050255_create_comments.rb │ ├── 20140422212639_create_notifications.rb │ ├── 20140422200726_create_likes.rb │ ├── 20140422020710_create_note_tags.rb │ ├── 20140421180638_create_users.rb │ ├── 20140421200533_create_notes.rb │ ├── 20140422140843_create_friendships.rb │ └── 20140422141140_create_friend_requests.rb ├── betternote.pub ├── .gitignore ├── Gemfile ├── betternote └── README.md /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/.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 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/static_pages/_nav.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/static_pages/home.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/api/placeholder.txt: -------------------------------------------------------------------------------- 1 | Delete -------------------------------------------------------------------------------- /app/views/users/_show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.(user, :id, :username) -------------------------------------------------------------------------------- /app/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shubik22/BetterNote/HEAD/app/.DS_Store -------------------------------------------------------------------------------- /app/views/tags/_show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.(tag, :id, :name, :created_at, :updated_at) -------------------------------------------------------------------------------- /app/views/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shubik22/BetterNote/HEAD/app/views/.DS_Store -------------------------------------------------------------------------------- /app/views/notebooks/_show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.(notebook, :id, :name, :created_at, :updated_at) -------------------------------------------------------------------------------- /app/assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shubik22/BetterNote/HEAD/app/assets/.DS_Store -------------------------------------------------------------------------------- /app/assets/javascripts/models/filter.js: -------------------------------------------------------------------------------- 1 | BetterNote.Models.Filter = Backbone.Model.extend({ 2 | }); -------------------------------------------------------------------------------- /app/controllers/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shubik22/BetterNote/HEAD/app/controllers/.DS_Store -------------------------------------------------------------------------------- /app/views/tags/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! @tags do |tag| 2 | json.partial! "tags/show", tag: tag 3 | end -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /spec/controllers/likes_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe LikesController do 4 | 5 | end 6 | -------------------------------------------------------------------------------- /spec/controllers/notes_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe NotesController do 4 | 5 | end 6 | -------------------------------------------------------------------------------- /spec/controllers/tags_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe TagsController do 4 | 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/images/green_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shubik22/BetterNote/HEAD/app/assets/images/green_icon.png -------------------------------------------------------------------------------- /app/views/notes/_index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array!(notes) do |note| 2 | json.partial! "notes/show.json", note: note 3 | end -------------------------------------------------------------------------------- /spec/controllers/comments_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CommentsController do 4 | 5 | end 6 | -------------------------------------------------------------------------------- /test/helpers/users_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UsersHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /spec/controllers/note_tags_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe NoteTagsController do 4 | 5 | end 6 | -------------------------------------------------------------------------------- /spec/controllers/notebooks_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe NotebooksController do 4 | 5 | end 6 | -------------------------------------------------------------------------------- /test/helpers/sessions_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SessionsHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /app/views/static_pages/root.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
-------------------------------------------------------------------------------- /spec/controllers/friendships_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe FriendshipsController do 4 | 5 | end 6 | -------------------------------------------------------------------------------- /spec/controllers/static_pages_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe StaticPagesController do 4 | 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/fonts/CaeciliaLTStd-Roman.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shubik22/BetterNote/HEAD/app/assets/fonts/CaeciliaLTStd-Roman.ttf -------------------------------------------------------------------------------- /spec/controllers/friend_requests_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe FriendRequestsController do 4 | 5 | end 6 | -------------------------------------------------------------------------------- /app/views/notebooks/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array!(@notebooks) do |notebook| 2 | json.partial! "notebooks/show", notebook: notebook 3 | end -------------------------------------------------------------------------------- /app/assets/images/header-background-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shubik22/BetterNote/HEAD/app/assets/images/header-background-green.png -------------------------------------------------------------------------------- /app/views/tags/edit.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/tags/new.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/views/tags/tags_index.js: -------------------------------------------------------------------------------- 1 | BetterNote.Views.TagsIndex = Backbone.View.extend({ 2 | 3 | template: JST['tags/index'] 4 | 5 | }); 6 | -------------------------------------------------------------------------------- /app/views/notebooks/edit.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/notebooks/new.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/javascripts/collections/users.js: -------------------------------------------------------------------------------- 1 | BetterNote.Collections.Users = Backbone.Collection.extend({ 2 | 3 | model: BetterNote.Models.User 4 | 5 | }); 6 | -------------------------------------------------------------------------------- /app/assets/javascripts/views/users/users_index.js: -------------------------------------------------------------------------------- 1 | BetterNote.Views.UsersIndex = Backbone.View.extend({ 2 | 3 | template: JST['users/index'] 4 | 5 | }); 6 | -------------------------------------------------------------------------------- /spec/models/notification_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Notification do 4 | pending "add some examples to (or delete) #{__FILE__}" 5 | end 6 | -------------------------------------------------------------------------------- /app/controllers/static_pages_controller.rb: -------------------------------------------------------------------------------- 1 | class StaticPagesController < ApplicationController 2 | before_action :require_signed_in! 3 | 4 | def root 5 | end 6 | end -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /test/models/note_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class NoteTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UserTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/assets/javascripts/collections/tags.js: -------------------------------------------------------------------------------- 1 | BetterNote.Collections.Tags = Backbone.Collection.extend({ 2 | 3 | model: BetterNote.Models.Tag, 4 | url: '/api/tags' 5 | 6 | }); -------------------------------------------------------------------------------- /app/assets/javascripts/collections/likes.js: -------------------------------------------------------------------------------- 1 | BetterNote.Collections.Likes = Backbone.Collection.extend({ 2 | 3 | model: BetterNote.Models.Like, 4 | url: '/api/likes' 5 | 6 | }); -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /app/assets/javascripts/collections/note_tags.js: -------------------------------------------------------------------------------- 1 | BetterNote.Collections.NoteTags = Backbone.Collection.extend({ 2 | 3 | model: BetterNote.Models.NoteTag, 4 | url: '/api/note_tags' 5 | 6 | }); -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | BetterNote::Application.initialize! 6 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | BetterNote::Application.config.session_store :cookie_store, key: '_BetterNote_session' 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/collections/notebooks.js: -------------------------------------------------------------------------------- 1 | BetterNote.Collections.Notebooks = Backbone.Collection.extend({ 2 | 3 | model: BetterNote.Models.Notebook, 4 | url: '/api/notebooks' 5 | 6 | }); 7 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | -------------------------------------------------------------------------------- /test/controllers/users_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UsersControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/sessions_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SessionsControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/models/note_tag.rb: -------------------------------------------------------------------------------- 1 | class NoteTag < ActiveRecord::Base 2 | validates :tag, :note, presence: true 3 | validates :tag, uniqueness: { scope: :note } 4 | 5 | belongs_to :note 6 | belongs_to :tag 7 | end 8 | -------------------------------------------------------------------------------- /app/assets/javascripts/models/tag.js: -------------------------------------------------------------------------------- 1 | BetterNote.Models.Tag = Backbone.Model.extend({ 2 | urlRoot: "api/tags", 3 | initialize: function() { 4 | this.notes = this.notes || new BetterNote.Collections.Notes(); 5 | } 6 | }); -------------------------------------------------------------------------------- /app/models/notebook.rb: -------------------------------------------------------------------------------- 1 | class Notebook < ActiveRecord::Base 2 | validates :name, :owner, presence: true 3 | 4 | has_many :notes, inverse_of: :notebook, dependent: :destroy 5 | belongs_to :owner, class_name: "User" 6 | end -------------------------------------------------------------------------------- /app/views/note_tags/_show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.id note_tag.id 2 | json.note_id note_tag.note_id 3 | json.tag_id note_tag.tag_id 4 | json.tag_name note_tag.tag.name 5 | json.created_at note_tag.created_at 6 | json.updated_at note_tag.updated_at -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /lib/tasks/scheduler.rake: -------------------------------------------------------------------------------- 1 | task :reset_database => :environment do 2 | models = [User, Note, Notebook, Tag, Comment, Like, NoteTag, 3 | Friendship, FriendRequest, Notification] 4 | models.each { |model| model.delete_all } 5 | Rails.application.load_seed 6 | end -------------------------------------------------------------------------------- /app/assets/templates/likes/show.jst.ejs: -------------------------------------------------------------------------------- 1 |
2 | <% if (note.currentUserLike()) { %> 3 | REMOVE LIKE 4 | 5 | <% } else { %> 6 | LIKE 7 | 8 | <% } %> 9 |
-------------------------------------------------------------------------------- /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 File.expand_path('../config/application', __FILE__) 5 | 6 | BetterNote::Application.load_tasks 7 | -------------------------------------------------------------------------------- /spec/models/friendship_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Friendship do 4 | it { should validate_presence_of(:in_friend) } 5 | it { should validate_presence_of(:out_friend) } 6 | 7 | it { should belong_to(:in_friend) } 8 | it { should belong_to(:out_friend) } 9 | end -------------------------------------------------------------------------------- /spec/models/friend_request_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe FriendRequest do 4 | it { should validate_presence_of(:in_friend) } 5 | it { should validate_presence_of(:out_friend) } 6 | 7 | it { should belong_to(:in_friend) } 8 | it { should belong_to(:out_friend) } 9 | end 10 | -------------------------------------------------------------------------------- /app/assets/javascripts/views/static_pages/about.js: -------------------------------------------------------------------------------- 1 | BetterNote.Views.About = Backbone.View.extend({ 2 | template: JST['static_pages/about'], 3 | 4 | render: function() { 5 | var renderedContent = this.template(); 6 | 7 | this.$el.html(renderedContent); 8 | return this; 9 | } 10 | }); -------------------------------------------------------------------------------- /app/views/users/_friends.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array!(friends) do |friend| 2 | json.username friend.username 3 | json.id friend.id 4 | json.notes do 5 | json.array!(friend.notes) do |note| 6 | json.partial! "notes/show.json", note: note 7 | json.note_type "friend" 8 | end 9 | end 10 | end -------------------------------------------------------------------------------- /app/assets/javascripts/models/comment.js: -------------------------------------------------------------------------------- 1 | BetterNote.Models.Comment = Backbone.Model.extend({ 2 | parse: function(jsonComment) { 3 | if (jsonComment.author) { 4 | this.author = new BetterNote.Models.User(jsonComment.author); 5 | delete jsonComment.author; 6 | } 7 | return jsonComment; 8 | } 9 | }); -------------------------------------------------------------------------------- /app/views/notes/index.html.erb: -------------------------------------------------------------------------------- 1 | <% if params[:list_view] == "notebook" %> 2 | <%= render "notebooks/show" %> 3 | <% elsif params[:list_view] == "tag" %> 4 | <%= render "tags/show" %> 5 | <% elsif params[:list_view] == "friend" %> 6 | <%= render "users/show" %> 7 | <% else %> 8 | <%= render "notes/index" %> 9 | <% end %> -------------------------------------------------------------------------------- /app/views/notes/_new.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= auth_token %> 3 | 4 | 5 | 6 | 10 |
-------------------------------------------------------------------------------- /app/assets/javascripts/views/tags/tags_show.js: -------------------------------------------------------------------------------- 1 | BetterNote.Views.TagsShow = Backbone.View.extend({ 2 | template: JST['tags/show'], 3 | 4 | render: function() { 5 | var renderedContent = this.template({ 6 | tag: this.model 7 | }); 8 | 9 | this.$el.html(renderedContent); 10 | return this; 11 | } 12 | }); -------------------------------------------------------------------------------- /db/migrate/20140421233947_create_notebooks.rb: -------------------------------------------------------------------------------- 1 | class CreateNotebooks < ActiveRecord::Migration 2 | def change 3 | create_table :notebooks do |t| 4 | t.text :name, null: false 5 | t.integer :owner_id, null: false 6 | 7 | t.timestamps 8 | end 9 | 10 | add_index :notebooks, :owner_id 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/views/notes/_preview.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= note.title %> 4 |
5 |
6 | 7 | <%= time_ago_in_words(note.created_at) %> ago 8 | 9 | <%= note.body %> 10 |
11 |
-------------------------------------------------------------------------------- /app/assets/javascripts/views/notebooks/notebooks_show.js: -------------------------------------------------------------------------------- 1 | BetterNote.Views.NotebooksShow = Backbone.View.extend({ 2 | template: JST['notebooks/show'], 3 | 4 | render: function() { 5 | var renderedContent = this.template({ 6 | notebook: this.model 7 | }); 8 | 9 | this.$el.html(renderedContent); 10 | return this; 11 | } 12 | }); -------------------------------------------------------------------------------- /app/assets/javascripts/models/user.js: -------------------------------------------------------------------------------- 1 | BetterNote.Models.User = Backbone.Model.extend({ 2 | urlRoot: 'api/friendships', 3 | 4 | parse: function(jsonUser) { 5 | if (jsonUser.notes) { 6 | this.notes = new BetterNote.Collections.Notes(jsonUser.notes, { parse: true }) 7 | delete jsonUser.notes 8 | } 9 | return jsonUser; 10 | } 11 | }); -------------------------------------------------------------------------------- /db/migrate/20140422004736_create_tags.rb: -------------------------------------------------------------------------------- 1 | class CreateTags < ActiveRecord::Migration 2 | def change 3 | create_table :tags do |t| 4 | t.text :name, null: false 5 | t.integer :owner_id, null: false 6 | 7 | t.timestamps 8 | end 9 | 10 | add_index :tags, :owner_id 11 | add_index :tags, [:owner_id, :name], unique: true 12 | end 13 | end -------------------------------------------------------------------------------- /app/models/tag.rb: -------------------------------------------------------------------------------- 1 | class Tag < ActiveRecord::Base 2 | validates :owner, :name, presence: true 3 | validates :name, uniqueness: { scope: :owner } 4 | 5 | belongs_to( 6 | :owner, 7 | class_name: "User", 8 | foreign_key: :owner_id 9 | ) 10 | 11 | has_many :note_tags, inverse_of: :tag, dependent: :destroy 12 | has_many :notes, through: :note_tags, source: :note 13 | end -------------------------------------------------------------------------------- /app/assets/javascripts/models/note_tag.js: -------------------------------------------------------------------------------- 1 | BetterNote.Models.NoteTag = Backbone.Model.extend({ 2 | urlRoot: '/api/note_tags', 3 | 4 | parse: function(jsonNoteTag) { 5 | var tag = BetterNote.tags.get(jsonNoteTag.tag_id); 6 | var note = BetterNote.notes.get(jsonNoteTag.note_id); 7 | tag.notes.add(note); 8 | note.noteTags.add(this); 9 | return jsonNoteTag; 10 | } 11 | }); -------------------------------------------------------------------------------- /spec/models/notebook_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Notebook do 4 | it { should validate_presence_of(:owner) } 5 | it { should validate_presence_of(:name) } 6 | 7 | it "associates with the correct user before save via inverse of" do 8 | user = FactoryGirl.build(:user) 9 | notebook = user.notebooks.new 10 | expect(notebook.owner).to eq(user) 11 | end 12 | end -------------------------------------------------------------------------------- /db/migrate/20140422050255_create_comments.rb: -------------------------------------------------------------------------------- 1 | class CreateComments < ActiveRecord::Migration 2 | def change 3 | create_table :comments do |t| 4 | t.integer :note_id, null: false 5 | t.integer :author_id, null: false 6 | t.text :body, null: false 7 | 8 | t.timestamps 9 | end 10 | 11 | add_index :comments, :note_id 12 | add_index :comments, :author_id 13 | end 14 | end -------------------------------------------------------------------------------- /betternote.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCjzo4AHV8xRWqRZkVQoYz4AHsUPcCWTuxAReC7S14JM1B2nnplK2N7V+61uiyMcXDcPZu7gZM0xiPXEeYuB/MbcOQkeGAakeAUzS9LEOBrKJxZSm3h2O3zHv2pHqF0cfIDbe4xNI0nOdtd+x2qYYAHNtS5TBFn1usewXHc1KboPF0nP5hIjjmkBz5v7WgmspapAHH0ybqkpY4ZazQPRQFpiWqw01GptBlCAw9nVuM+mDhfUJhvC6puIHYavqFv45nkOL+d81SbIsf8nCIlq/99NqbtRtzFdQF7rlsxVZOCeCZXhzeDlLO7Jj6UTVG5EeoVWgCIU25AkKngYZynQaWJ appacademy@Heemstede-91.local 2 | -------------------------------------------------------------------------------- /db/migrate/20140422212639_create_notifications.rb: -------------------------------------------------------------------------------- 1 | class CreateNotifications < ActiveRecord::Migration 2 | def change 3 | create_table :notifications do |t| 4 | t.integer :notifiable_id 5 | t.string :notifiable_type 6 | t.integer :user_id 7 | 8 | t.timestamps 9 | end 10 | 11 | add_index :notifications, :notifiable_id 12 | add_index :notifications, :user_id 13 | end 14 | end -------------------------------------------------------------------------------- /db/migrate/20140422200726_create_likes.rb: -------------------------------------------------------------------------------- 1 | class CreateLikes < ActiveRecord::Migration 2 | def change 3 | create_table :likes do |t| 4 | t.integer :owner_id, null: false 5 | t.integer :note_id, null: false 6 | 7 | t.timestamps 8 | end 9 | 10 | add_index :likes, :owner_id 11 | add_index :likes, :note_id 12 | add_index :likes, [:owner_id, :note_id], unique: true 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/fixtures/notes.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /app/models/friend_request.rb: -------------------------------------------------------------------------------- 1 | class FriendRequest < ActiveRecord::Base 2 | validates :in_friend, :out_friend, presence: true 3 | validates :in_friend, uniqueness: { scope: :out_friend } 4 | 5 | belongs_to( 6 | :in_friend, 7 | class_name: "User", 8 | foreign_key: :in_friend_id 9 | ) 10 | 11 | belongs_to( 12 | :out_friend, 13 | class_name: "User", 14 | foreign_key: :out_friend_id 15 | ) 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20140422020710_create_note_tags.rb: -------------------------------------------------------------------------------- 1 | class CreateNoteTags < ActiveRecord::Migration 2 | def change 3 | create_table :note_tags do |t| 4 | t.integer :tag_id, null: false 5 | t.integer :note_id, null: false 6 | 7 | t.timestamps 8 | end 9 | 10 | add_index :note_tags, :tag_id 11 | add_index :note_tags, :note_id 12 | add_index :note_tags, [:tag_id, :note_id], unique: true 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/models/like_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Like do 4 | it { should validate_presence_of(:owner) } 5 | it { should validate_presence_of(:note) } 6 | it { should validate_uniqueness_of(:owner).scoped_to(:note) } 7 | 8 | it "associates with the correct user before save via inverse of" do 9 | user = FactoryGirl.build(:user) 10 | like = user.likes.new 11 | expect(like.owner).to eq(user) 12 | end 13 | end -------------------------------------------------------------------------------- /app/views/users/_index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array!(users) do |user| 2 | json.id user.id 3 | json.username user.username 4 | if current_user.friendships.find_by({in_friend: user}) 5 | json.status "friend" 6 | elsif current_user.friend_requests.find_by({in_friend: user}) 7 | json.status "waiting" 8 | elsif current_user.friend_requests.find_by({out_friend: user}) 9 | json.status "pending" 10 | else 11 | json.status "none" 12 | end 13 | end -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /db/migrate/20140421180638_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 2 | def change 3 | create_table :users do |t| 4 | t.string :username, null: false 5 | t.string :email, null: false 6 | t.string :password_digest, null: false 7 | t.string :session_token 8 | 9 | t.timestamps 10 | end 11 | 12 | add_index :users, :username, unique: true 13 | add_index :users, :email, unique: true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20140421200533_create_notes.rb: -------------------------------------------------------------------------------- 1 | class CreateNotes < ActiveRecord::Migration 2 | def change 3 | create_table :notes do |t| 4 | t.string :title, default: "Untitled Note" 5 | t.text :body 6 | t.integer :author_id, null: false 7 | t.integer :notebook_id, null: false 8 | 9 | t.timestamps 10 | end 11 | 12 | add_index :notes, :author_id 13 | add_index :notes, :notebook_id 14 | add_index :notes, :title 15 | end 16 | end -------------------------------------------------------------------------------- /db/migrate/20140422140843_create_friendships.rb: -------------------------------------------------------------------------------- 1 | class CreateFriendships < ActiveRecord::Migration 2 | def change 3 | create_table :friendships do |t| 4 | t.integer :in_friend_id, null: false 5 | t.integer :out_friend_id, null: false 6 | 7 | t.timestamps 8 | end 9 | 10 | add_index :friendships, :out_friend_id 11 | add_index :friendships, :in_friend_id 12 | add_index :friendships, [:in_friend_id, :out_friend_id], unique: true 13 | end 14 | end -------------------------------------------------------------------------------- /app/controllers/friendships_controller.rb: -------------------------------------------------------------------------------- 1 | class FriendshipsController < ApplicationController 2 | before_action :require_signed_in! 3 | 4 | def destroy 5 | @friendship = Friendship.find(params[:id]) 6 | @opposite_friendship = Friendship.find_by({ 7 | in_friend_id: @friendship.out_friend_id, 8 | out_friend_id: @friendship.in_friend.id 9 | }) 10 | 11 | @friendship.destroy 12 | @opposite_friendship.destroy 13 | redirect_to :back 14 | end 15 | end -------------------------------------------------------------------------------- /app/models/comment.rb: -------------------------------------------------------------------------------- 1 | class Comment < ActiveRecord::Base 2 | validates :author, :note, presence: true 3 | after_save :create_notification 4 | 5 | belongs_to( 6 | :author, 7 | class_name: "User", 8 | foreign_key: :author_id 9 | ) 10 | 11 | belongs_to :note 12 | 13 | has_many :notifications, as: :notifiable, dependent: :destroy 14 | 15 | private 16 | def create_notification 17 | self.notifications.create({user_id: self.note.author_id}) 18 | end 19 | end -------------------------------------------------------------------------------- /app/views/static_pages/_notifications.html.erb: -------------------------------------------------------------------------------- 1 |
  • 2 | <% count = current_user.notifications.count %> 3 | 4 |   5 | Notifications (<%= count %>) 6 | 7 | 14 |
  • 15 | -------------------------------------------------------------------------------- /app/assets/javascripts/models/like.js: -------------------------------------------------------------------------------- 1 | BetterNote.Models.Like = Backbone.Model.extend({ 2 | initialize: function() { 3 | this.owner = this.owner || BetterNote.currentUser; 4 | }, 5 | 6 | urlRoot: 'api/likes', 7 | 8 | parse: function(jsonLike) { 9 | if (jsonLike.owner) { 10 | this.owner = new BetterNote.Models.User(jsonLike.owner); 11 | delete jsonLike.owner; 12 | } else { 13 | this.owner = BetterNote.currentUser; 14 | } 15 | return jsonLike; 16 | } 17 | }); -------------------------------------------------------------------------------- /app/assets/templates/static_pages/corgi.jst.ejs: -------------------------------------------------------------------------------- 1 | × 2 | 3 |

    4 | Corgi? 5 |

    6 | 7 |
    8 | 9 |
    10 | 11 |
    12 | 13 |
    14 | 15 |
    16 | 17 |
    18 |
    19 |
    -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | def auth_token 3 | <<-HTML.html_safe 4 | 7 | HTML 8 | end 9 | 10 | def update_url_with_note(url, note) 11 | url = url.dup 12 | if url.include?("?") 13 | url.gsub!(/note_id=[0-9]+/, "") 14 | else 15 | url += "?" 16 | end 17 | return url + "note_id=#{note.id}" 18 | end 19 | end -------------------------------------------------------------------------------- /app/assets/templates/notes/delete.jst.ejs: -------------------------------------------------------------------------------- 1 | × 2 | 3 |

    4 | Delete Note 5 |

    6 | 7 |
    8 | 9 |
    10 | Are you sure you want to delete this note? 11 |
    12 | 13 |
    14 |
    15 | Cancel 16 |
    17 | 18 |
    19 | 20 |
    21 |
    22 |
    -------------------------------------------------------------------------------- /db/migrate/20140422141140_create_friend_requests.rb: -------------------------------------------------------------------------------- 1 | class CreateFriendRequests < ActiveRecord::Migration 2 | def change 3 | create_table :friend_requests do |t| 4 | t.integer :in_friend_id, null: false 5 | t.integer :out_friend_id, null: false 6 | 7 | t.timestamps 8 | end 9 | 10 | add_index :friend_requests, :out_friend_id 11 | add_index :friend_requests, :in_friend_id 12 | add_index :friend_requests, [:in_friend_id, :out_friend_id], unique: true 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/views/comments/new.html.erb: -------------------------------------------------------------------------------- 1 |

    Post New Comment

    2 |
    3 | <%= auth_token %> 4 | 5 | 6 | 7 |
    8 | 9 | 10 |
    11 | 12 |
    13 | 14 |
    15 |
    -------------------------------------------------------------------------------- /app/views/comments/_form.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |   3 | New Comment 4 |
    5 |
    6 |
    7 | <%= auth_token %> 8 | 9 | 10 | 11 | 12 | 13 | 16 |
    17 |
    -------------------------------------------------------------------------------- /spec/models/tag_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tag do 4 | it { should validate_presence_of(:owner) } 5 | it { should validate_presence_of(:name) } 6 | it { should validate_uniqueness_of(:name).scoped_to(:owner) } 7 | 8 | it { should have_many(:note_tags) } 9 | it { should have_many(:notes) } 10 | 11 | it "associates with the correct user before save via inverse of" do 12 | user = FactoryGirl.build(:user) 13 | tag = user.tags.new 14 | expect(tag.owner).to eq(user) 15 | end 16 | end -------------------------------------------------------------------------------- /app/models/like.rb: -------------------------------------------------------------------------------- 1 | class Like < ActiveRecord::Base 2 | validates :owner, :note, presence: true 3 | validates :owner, uniqueness: { scope: :note } 4 | after_save :create_notification 5 | 6 | belongs_to( 7 | :owner, 8 | class_name: "User", 9 | foreign_key: :owner_id 10 | ) 11 | 12 | belongs_to :note 13 | has_many :notifications, as: :notifiable, dependent: :destroy 14 | 15 | private 16 | def create_notification 17 | self.notifications.create({user_id: self.note.author_id}) 18 | end 19 | end -------------------------------------------------------------------------------- /app/assets/templates/tags/delete.jst.ejs: -------------------------------------------------------------------------------- 1 | × 2 | 3 |

    4 | Delete Tag 5 |

    6 | 7 |
    8 | 9 |
    10 | 11 |
    12 | 13 |
    14 |
    15 | Cancel 16 |
    17 | 18 |
    19 | 20 |
    21 |
    22 |
    -------------------------------------------------------------------------------- /app/assets/templates/users/unfriend.jst.ejs: -------------------------------------------------------------------------------- 1 | × 2 | 3 |

    4 | Remove Friend 5 |

    6 | 7 |
    8 | 9 |
    10 | Are you sure you want to unfriend <%= friend.get("username") %>? 11 |
    12 | 13 |
    14 |
    15 | Cancel 16 |
    17 | 18 |
    19 | 20 |
    21 |
    22 |
    -------------------------------------------------------------------------------- /app/controllers/api/likes_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::LikesController < ApplicationController 2 | before_action :require_signed_in! 3 | 4 | def create 5 | @like = current_user.likes.new({note_id: params[:note_id]}) 6 | if @like.save 7 | render json: @like 8 | else 9 | render json: @comment.errors.full_messages, status: :unprocessable_entity 10 | end 11 | end 12 | 13 | def destroy 14 | @like = current_user.likes.find(params[:id]) 15 | @like.destroy 16 | render json: {} 17 | end 18 | end -------------------------------------------------------------------------------- /app/controllers/api/friendships_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::FriendshipsController < ApplicationController 2 | before_action :require_signed_in! 3 | 4 | def destroy 5 | @friendship = current_user.friendships.find_by({ 6 | in_friend_id: params[:id] 7 | }) 8 | @opposite_friendship = Friendship.find_by({ 9 | in_friend_id: @friendship.out_friend_id, 10 | out_friend_id: @friendship.in_friend.id 11 | }) 12 | 13 | @friendship.destroy 14 | @opposite_friendship.destroy 15 | render json: {} 16 | end 17 | end -------------------------------------------------------------------------------- /app/assets/templates/notebooks/delete.jst.ejs: -------------------------------------------------------------------------------- 1 | × 2 | 3 |

    4 | Delete Notebook 5 |

    6 | 7 |
    8 | 9 |
    10 | 11 |
    12 | 13 |
    14 |
    15 | Cancel 16 |
    17 | 18 |
    19 | 20 |
    21 |
    22 |
    -------------------------------------------------------------------------------- /app/assets/templates/notes/preview.jst.ejs: -------------------------------------------------------------------------------- 1 | "> 2 |
    "> 4 |
    5 | <%= note.escape("title") %> 6 |
    7 |
    8 | 9 | <%= moment(note.escape("created_at")).fromNow() %> 10 | 11 | <%= note.escape("body") %> 12 |
    13 |
    14 |
    -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] ||= "test" 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | 5 | class ActiveSupport::TestCase 6 | ActiveRecord::Migration.check_pending! 7 | 8 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 9 | # 10 | # Note: You'll currently still have to declare fixtures explicitly in integration tests 11 | # -- they do not yet inherit this setting 12 | fixtures :all 13 | 14 | # Add more helper methods to be used by all tests here... 15 | end 16 | -------------------------------------------------------------------------------- /.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/*.log 16 | /tmp 17 | 18 | /.DS_Store 19 | /.database.yml -------------------------------------------------------------------------------- /app/assets/templates/comments/comment_form.jst.ejs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |   4 | New Comment 5 |
    6 |
    7 | "> 8 | 9 | 10 | 11 | 14 | 17 |
    18 |
    -------------------------------------------------------------------------------- /spec/models/note_tag_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe NoteTag do 4 | it { should validate_presence_of(:note) } 5 | it { should validate_presence_of(:tag) } 6 | 7 | it "associates with the correct note before save via inverse of" do 8 | note = FactoryGirl.build(:note) 9 | note_tag = note.note_tags.new 10 | expect(note_tag.note).to eq(note) 11 | end 12 | 13 | it "associates with the correct tag before save via inverse of" do 14 | tag = FactoryGirl.build(:tag) 15 | note_tag = tag.note_tags.new 16 | expect(note_tag.tag).to eq(tag) 17 | end 18 | end -------------------------------------------------------------------------------- /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] if respond_to?(:wrap_parameters) 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 | -------------------------------------------------------------------------------- /app/assets/javascripts/collections/comments.js: -------------------------------------------------------------------------------- 1 | BetterNote.Collections.Comments = Backbone.Collection.extend({ 2 | 3 | model: BetterNote.Models.Comment, 4 | url: '/api/comments', 5 | 6 | comparator: function(comment1, comment2) { 7 | if (comment1.isNew()) { 8 | return 1 9 | } else if (comment2.isNew()) { 10 | return -1 11 | }; 12 | 13 | if (comment1.get("created_at") > comment2.get("created_at")) { 14 | return 1; 15 | } else if (comment1.get("created_at") < comment2.get("created_at")) { 16 | return -1; 17 | } else { 18 | return 0; 19 | } 20 | } 21 | }); -------------------------------------------------------------------------------- /app/assets/templates/tags/form.jst.ejs: -------------------------------------------------------------------------------- 1 | × 2 | 3 |

    4 | <%= (tag.isNew()) ? "Create Tag" : "Rename Tag" %> 5 |

    6 | 7 |
    8 | 9 |
    10 | 11 | "> 13 |
    14 | 15 |
    16 |
    17 | Cancel 18 |
    19 | 20 |
    21 | 22 |
    23 |
    24 |
    -------------------------------------------------------------------------------- /app/views/notes/_show.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | <%= render partial: "notes/show_header", locals: { note: note } %> 3 | <% if params[:edit] %> 4 |
    5 | <%= render partial: "notes/edit", locals: { note: note } %> 6 |
    7 | <% else %> 8 |
    9 |
    10 | <%= note.title %> 11 |
    12 |
    13 | <%= note.body %> 14 |
    15 |
    16 | <% end %> 17 | <%= render partial: "notes/comments", 18 | locals: { comments: note.comments, note: note } %> 19 |
    -------------------------------------------------------------------------------- /app/controllers/api/comments_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::CommentsController < ApplicationController 2 | before_action :require_signed_in! 3 | 4 | def create 5 | @comment = current_user.comments.new(comment_params) 6 | if @comment.save 7 | render json: @comment 8 | else 9 | render json: @comment.errors.full_messages, status: :unprocessable_entity 10 | end 11 | end 12 | 13 | def destroy 14 | @comment = current_user.comments.find(params[:id]) 15 | @comment.destroy 16 | render json: {} 17 | end 18 | 19 | private 20 | def comment_params 21 | params.require(:comment).permit(:body, :note_id) 22 | end 23 | end -------------------------------------------------------------------------------- /app/assets/javascripts/views/notebooks/corgi.js: -------------------------------------------------------------------------------- 1 | BetterNote.Views.Corgi = Backbone.View.extend({ 2 | initialize: function(options) { 3 | this.$modal = options["$modal"] 4 | }, 5 | 6 | template: JST['static_pages/corgi'], 7 | 8 | events: { 9 | "click input[type='submit']": "closeModal" 10 | }, 11 | 12 | render: function() { 13 | var renderedContent = this.template({ 14 | notebook: this.model 15 | }); 16 | 17 | this.$el.html(renderedContent); 18 | return this; 19 | }, 20 | 21 | closeModal: function(event) { 22 | event.preventDefault(); 23 | 24 | this.$modal.addClass("hidden"); 25 | this.remove(); 26 | } 27 | }); -------------------------------------------------------------------------------- /spec/models/comment_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Comment do 4 | it { should validate_presence_of(:author) } 5 | it { should validate_presence_of(:note) } 6 | 7 | it { should belong_to(:author) } 8 | it { should belong_to(:note) } 9 | 10 | it "associates with the correct author before save via inverse of" do 11 | user = FactoryGirl.build(:user) 12 | comment = user.comments.new 13 | expect(comment.author).to eq(user) 14 | end 15 | 16 | it "associates with the correct note before save via inverse of" do 17 | note = FactoryGirl.build(:note) 18 | comment = note.comments.new 19 | expect(comment.note).to eq(note) 20 | end 21 | end -------------------------------------------------------------------------------- /app/assets/javascripts/models/notebook.js: -------------------------------------------------------------------------------- 1 | BetterNote.Models.Notebook = Backbone.Model.extend({ 2 | initialize: function() { 3 | this.notes = this.notes || new BetterNote.Collections.Notes(); 4 | }, 5 | 6 | parse: function(jsonResp) { 7 | if (jsonResp.notes) { 8 | var that = this; 9 | that.notes = new BetterNote.Collections.Notes(jsonResp.notes); 10 | that.notes.each(function(note) { 11 | note.notebook = that; 12 | }) 13 | delete jsonResp.notes; 14 | } 15 | if (jsonResp.owner) { 16 | this.owner = new BetterNote.Models.User(jsonResp.owner); 17 | delete jsonResp.owner; 18 | } 19 | return jsonResp; 20 | } 21 | }); -------------------------------------------------------------------------------- /app/assets/templates/notebooks/form.jst.ejs: -------------------------------------------------------------------------------- 1 | × 2 | 3 |

    4 | <%= (notebook.isNew()) ? "Create Notebook" : "Rename Notebook" %> 5 |

    6 | 7 |
    8 | 9 |
    10 | 11 | "> 13 |
    14 | 15 |
    16 |
    17 | Cancel 18 |
    19 | 20 |
    21 | 22 |
    23 |
    24 |
    -------------------------------------------------------------------------------- /app/controllers/api/note_tags_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::NoteTagsController < ApplicationController 2 | before_action :require_signed_in! 3 | 4 | def create 5 | @note_tag = NoteTag.new(note_tag_params) 6 | if @note_tag.save 7 | render partial: "note_tags/show", locals: { note_tag: @note_tag } 8 | else 9 | render json: @note_tag.errors.full_messages, status: :unprocessable_entity 10 | end 11 | end 12 | 13 | def destroy 14 | @note_tag = NoteTag.find(params[:id]) 15 | @note_tag.destroy 16 | render json: {} 17 | end 18 | 19 | private 20 | def note_tag_params 21 | params.require(:note_tag).permit(:note_id, :tag_id) 22 | end 23 | end -------------------------------------------------------------------------------- /app/models/note.rb: -------------------------------------------------------------------------------- 1 | class Note < ActiveRecord::Base 2 | validates :author, :notebook, presence: true 3 | 4 | include PgSearch 5 | 6 | pg_search_scope :search_by_title_and_body, against: [:title, :body] 7 | 8 | belongs_to( 9 | :author, 10 | class_name: User, 11 | foreign_key: :author_id, 12 | primary_key: :id 13 | ) 14 | 15 | belongs_to :notebook 16 | has_many :comments, inverse_of: :note, dependent: :destroy 17 | has_many :note_tags, inverse_of: :note, dependent: :destroy 18 | has_many :tags, through: :note_tags, source: :tag 19 | has_many :likes, inverse_of: :note, dependent: :destroy 20 | has_many :likers, through: :likes, source: :owner 21 | end -------------------------------------------------------------------------------- /app/views/static_pages/_search_bar.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/tags/_form.html.erb: -------------------------------------------------------------------------------- 1 | <% action = (@tag.new_record?) ? 2 | tags_url : tag_url(@tag) %> 3 | <% button_value = (@tag.new_record?) ? 4 | "Create New Tag" : "Update Tag" %> 5 | 6 |
    7 | <%= auth_token %> 8 | 9 | <% unless @tag.new_record? %> 10 | 11 | <% end %> 12 | 13 | 14 |
    15 | 16 | 18 |
    19 | 20 |
    21 | 22 |
    23 |
    -------------------------------------------------------------------------------- /app/assets/javascripts/views/static_pages/sidebar_item.js: -------------------------------------------------------------------------------- 1 | BetterNote.Views.SidebarItem = Backbone.View.extend({ 2 | initialize: function(options) { 3 | this.listenTo(this.model, "add change remove", this.render); 4 | this.listenTo(this.model.notes, "add remove", this.render); 5 | }, 6 | 7 | tagName: "li", 8 | className: "group dropdown-parent", 9 | template: JST['static_pages/sidebar_item'], 10 | 11 | render: function() { 12 | var renderedContent = this.template({ 13 | notebook: this.model 14 | }); 15 | 16 | if (BetterNote.featuredNotes.id === this.model.get("id")) { 17 | this.$el.addClass("selected"); 18 | } 19 | 20 | this.$el.html(renderedContent); 21 | return this; 22 | } 23 | }); -------------------------------------------------------------------------------- /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/initializers/secret_token.rb: -------------------------------------------------------------------------------- 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 `rake secret` to generate a secure secret key. 9 | 10 | # Make sure your secret_key_base is kept private 11 | # if you're sharing your code publicly. 12 | BetterNote::Application.config.secret_key_base = 'acb44a5c99e076bc58b7bb3ba7f6d033030fbeb6dd5665c202bb680c530ac01b6bebe55b91c88171a716245b6729310f843496c82a5320db173fab28e4a89736' 13 | -------------------------------------------------------------------------------- /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 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /app/views/notebooks/_form.html.erb: -------------------------------------------------------------------------------- 1 | <% action = (@notebook.new_record?) ? 2 | notebooks_url : 3 | notebook_url(@notebook) %> 4 | <% button_value = (@notebook.new_record?) ? 5 | "Create New Notebook" : "Save" %> 6 | 7 |
    8 | <%= auth_token %> 9 | 10 | <% unless @notebook.new_record? %> 11 | 12 | <% end %> 13 | 14 |
    15 | 16 | 18 |
    19 | 20 |
    21 | 22 |
    23 |
    -------------------------------------------------------------------------------- /app/assets/javascripts/views/tags/tag_delete.js: -------------------------------------------------------------------------------- 1 | BetterNote.Views.TagDelete = Backbone.View.extend({ 2 | initialize: function(options) { 3 | this.$modal = options["$modal"] 4 | }, 5 | 6 | template: JST['tags/delete'], 7 | 8 | events: { 9 | "click input[type='submit']": "deleteTag" 10 | }, 11 | 12 | render: function() { 13 | var renderedContent = this.template({ 14 | notebook: this.model 15 | }); 16 | 17 | this.$el.html(renderedContent); 18 | return this; 19 | }, 20 | 21 | deleteTag: function(event) { 22 | event.preventDefault(); 23 | 24 | var that = this; 25 | this.model.destroy({ 26 | success: function() { 27 | that.$modal.addClass("hidden"); 28 | that.remove(); 29 | } 30 | }) 31 | } 32 | }); -------------------------------------------------------------------------------- /app/views/users/_friends.html.erb: -------------------------------------------------------------------------------- 1 | 23 | 24 | -------------------------------------------------------------------------------- /app/controllers/likes_controller.rb: -------------------------------------------------------------------------------- 1 | class LikesController < ApplicationController 2 | before_action :require_signed_in! 3 | before_action :user_owns_like?, only: [:destroy] 4 | 5 | def create 6 | @like = current_user.likes.new(like_params) 7 | if @like.save 8 | redirect_to :back 9 | else 10 | render json: @comment.errors.full_messages, status: :unprocessable_entity 11 | end 12 | end 13 | 14 | def destroy 15 | @like = Like.find(params[:id]) 16 | @like.destroy 17 | render json: {} 18 | end 19 | 20 | private 21 | def comment_params 22 | params.require(:like).permit(:note_id) 23 | end 24 | 25 | def user_owns_like? 26 | @like = Like.find(params[:id]) 27 | redirect_to root_url unless @like.owner == current_user 28 | end 29 | end -------------------------------------------------------------------------------- /app/views/notes/_index.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |   4 | All Notes 5 |
    6 | 16 | <%= render partial: "notes/list_footer" %> 17 |
    18 | 19 | <%# closes "left-absolute div" %> 20 | 21 | <% if @note || notes.first %> 22 | <%= render partial: "notes/show", 23 | locals: { note: @note || notes.first } %> 24 | <% end %> -------------------------------------------------------------------------------- /app/views/tags/show.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 | <%= @tag.name %> 5 |
    6 | 16 | <%= render partial: "notes/list_footer" %> 17 |
    18 | 19 | <%# closes "left-absolute div" %> 20 | 21 | <% if @note || notes.first %> 22 | <%= render partial: "notes/show", 23 | locals: { note: @note || notes.first } %> 24 | <% end %> -------------------------------------------------------------------------------- /app/views/notebooks/show.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 | <%= @notebook.name %> 5 |
    6 | 16 | <%= render partial: "notes/list_footer" %> 17 |
    18 | 19 | <%# closes "left-absolute div" %> 20 | 21 | <% if @note || notes.first %> 22 | <%= render partial: "notes/show", 23 | locals: { note: @note || notes.first } %> 24 | <% end %> -------------------------------------------------------------------------------- /app/views/users/show.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |   4 | <%= @user.username %> 5 |
    6 | 16 | <%= render partial: "notes/list_footer" %> 17 |
    18 | 19 | 20 | 21 | <% if @note || notes.first %> 22 | <%= render partial: "notes/show", 23 | locals: { note: @note || notes.first } %> 24 | <% end %> -------------------------------------------------------------------------------- /app/models/friendship.rb: -------------------------------------------------------------------------------- 1 | class Friendship < ActiveRecord::Base 2 | validates :in_friend, :out_friend, presence: true 3 | validates :in_friend, uniqueness: { scope: :out_friend } 4 | after_save :create_notification 5 | 6 | belongs_to( 7 | :in_friend, 8 | class_name: "User", 9 | foreign_key: :in_friend_id 10 | ) 11 | 12 | belongs_to( 13 | :out_friend, 14 | class_name: "User", 15 | foreign_key: :out_friend_id 16 | ) 17 | 18 | has_many :notifications, as: :notifiable, dependent: :destroy 19 | 20 | private 21 | def create_notification 22 | friendship = Friendship.find_by({ 23 | in_friend_id: self.out_friend_id, 24 | out_friend_id: self.in_friend_id 25 | }) 26 | self.notifications.create({user_id: self.out_friend_id}) unless friendship 27 | end 28 | end -------------------------------------------------------------------------------- /app/assets/templates/note_tags/index.jst.ejs: -------------------------------------------------------------------------------- 1 | <% noteTags.each(function(noteTag) { %> 2 |
  • 3 | 4 | <%= noteTag.get("tag_name") %> 5 | "> 7 | × 8 | 9 |
  • 10 | <% }) %> 11 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 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 BetterNote 10 | class Application < Rails::Application 11 | config.initialize_on_precompile = false 12 | config.assets.paths << Rails.root.join("app", "assets", "fonts") 13 | config.generators do |g| 14 | g.test_framework :rspec, 15 | :fixtures => true, 16 | :view_specs => false, 17 | :helper_specs => false, 18 | :routing_specs => false, 19 | :controller_specs => true, 20 | :request_specs => true 21 | g.fixture_replacement :factory_girl, :dir => "spec/factories" 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/assets/javascripts/views/users/unfriend.js: -------------------------------------------------------------------------------- 1 | BetterNote.Views.Unfriend = Backbone.View.extend({ 2 | initialize: function(options) { 3 | this.$modal = options["$modal"]; 4 | }, 5 | 6 | template: JST['users/unfriend'], 7 | 8 | events: { 9 | "click input[type='submit']": "removeFriend" 10 | }, 11 | 12 | render: function() { 13 | var renderedContent = this.template({ 14 | friend: this.model 15 | }); 16 | 17 | this.$el.html(renderedContent); 18 | return this; 19 | }, 20 | 21 | removeFriend: function(event) { 22 | event.preventDefault(); 23 | 24 | var that = this; 25 | this.model.destroy({ 26 | success: function() { 27 | that.$modal.addClass("hidden"); 28 | that.remove(); 29 | BetterNote.router.navigate("#/notes"); 30 | } 31 | }) 32 | } 33 | }); -------------------------------------------------------------------------------- /app/assets/javascripts/views/notes/note_preview.js: -------------------------------------------------------------------------------- 1 | BetterNote.Views.NotePreview = Backbone.View.extend({ 2 | initialize: function(options) { 3 | this.listenTo(this.model, "change", this.render); 4 | this.listenTo(this.model, "destroy", this.removeNote); 5 | }, 6 | 7 | template: JST['notes/preview'], 8 | tagName: "li", 9 | 10 | render: function() { 11 | var renderedContent = this.template({ 12 | note: this.model 13 | }); 14 | 15 | this.$el.html(renderedContent); 16 | return this; 17 | }, 18 | 19 | removeNote: function() { 20 | if (this.$el.next().length > 0) { 21 | this.$el.next().find("article").addClass("selected"); 22 | } else if (this.$el.prev().length > 0) { 23 | this.$el.prev().find("article").addClass("selected"); 24 | } 25 | 26 | this.remove(); 27 | } 28 | }); -------------------------------------------------------------------------------- /app/views/tags/_index.html.erb: -------------------------------------------------------------------------------- 1 | 28 | 29 | -------------------------------------------------------------------------------- /app/assets/templates/static_pages/about.jst.ejs: -------------------------------------------------------------------------------- 1 | × 2 | 3 |

    4 | About BetterNote 5 |

    6 | 7 |
    8 |

    9 | BetterNote is a clone of Evernote, the popular note-taking app, that was created by Sam Sweeney in Spring 2014. 10 |

    11 |

    12 | BetterNote was built using a Ruby on Rails back-end, a Backbone.js front-end, and a whole lot of love. Much gratitude and admiration to the team at Evernote for making an awesome app that was also a blast to clone. 13 |

    14 |

    15 | You can view BetterNote's source code here. Feel free to send feedback or get in touch here. 16 |

    17 |

    18 |
    19 | -------------------------------------------------------------------------------- /app/controllers/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class SessionsController < ApplicationController 2 | before_action :require_signed_out!, only: [:new, :create] 3 | before_action :require_signed_in!, only: [:destroy] 4 | layout "public" 5 | 6 | def create 7 | @user = User.find_by_credentials( 8 | params[:user][:email], 9 | params[:user][:password]) 10 | if @user 11 | @user = User.find_by_credentials( 12 | params[:user][:email], 13 | params[:user][:password]) 14 | log_in(@user) 15 | redirect_to root_url 16 | else 17 | flash[:errors] = ["Invalid credentials. Please try again."] 18 | redirect_to new_session_url 19 | end 20 | end 21 | 22 | def new 23 | @user = User.new 24 | render :new 25 | end 26 | 27 | def destroy 28 | logout_current_user 29 | redirect_to new_session_url 30 | end 31 | end -------------------------------------------------------------------------------- /app/views/users/new.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | <%= auth_token %> 3 | 4 |
    5 |

    Sign Up

    6 | 9 |
    10 | 11 |
    12 | 13 | 14 |
    15 | 16 |
    17 | 18 | 19 |
    20 | 21 |
    22 | 23 | 24 |
    25 | 26 |
    27 | 28 |
    29 |
    -------------------------------------------------------------------------------- /spec/models/note_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Note do 4 | it { should validate_presence_of(:author) } 5 | it { should validate_presence_of(:notebook) } 6 | it { should belong_to(:author) } 7 | it { should belong_to(:notebook) } 8 | 9 | it { should have_many(:note_tags) } 10 | it { should have_many(:tags) } 11 | it { should have_many(:comments) } 12 | it { should have_many(:likes) } 13 | it { should have_many(:likers) } 14 | 15 | it "associates with the correct notebook before save via inverse of" do 16 | notebook = FactoryGirl.build(:notebook) 17 | note = notebook.notes.new 18 | expect(note.notebook).to eq(notebook) 19 | end 20 | 21 | it "associates with the correct author before save via inverse of" do 22 | user = FactoryGirl.build(:user) 23 | note = user.notes.new 24 | expect(note.author).to eq(user) 25 | end 26 | end -------------------------------------------------------------------------------- /app/controllers/comments_controller.rb: -------------------------------------------------------------------------------- 1 | class CommentsController < ApplicationController 2 | before_action :require_signed_in! 3 | before_action :user_owns_comment?, only: [:destroy] 4 | 5 | def create 6 | @comment = current_user.comments.new(comment_params) 7 | if @comment.save 8 | redirect_to request.env["HTTP_REFERER"].gsub("&new_comment=true", "") 9 | else 10 | flash.now[:errors] = @comment.errors.full_messages 11 | render :new 12 | end 13 | end 14 | 15 | def destroy 16 | @comment = Comment.find(params[:id]) 17 | @comment.destroy 18 | redirect_to :back 19 | end 20 | 21 | private 22 | def comment_params 23 | params.require(:comment).permit(:body, :note_id) 24 | end 25 | 26 | def user_owns_comment? 27 | @comment = Comment.find(params[:id]) 28 | redirect_to root_url unless @comment.author == current_user 29 | end 30 | end -------------------------------------------------------------------------------- /app/assets/templates/static_pages/navbar.jst.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/notes/_show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.(note, :id, :title, :body, :notebook_id, :created_at, :updated_at) 2 | json.notebook note.notebook.name 3 | json.author do 4 | json.id note.author.id 5 | json.username note.author.username 6 | end 7 | json.comments do 8 | json.array!(note.comments) do |comment| 9 | json.id comment.id 10 | json.body comment.body 11 | json.created_at comment.created_at 12 | json.updated_at comment.updated_at 13 | json.author do 14 | json.id comment.author.id 15 | json.username comment.author.username 16 | end 17 | end 18 | end 19 | json.note_tags do 20 | json.array!(note.note_tags) do |note_tag| 21 | json.partial! "note_tags/show.json", note_tag: note_tag 22 | end 23 | end 24 | json.likes do 25 | json.array!(note.likes) do |like| 26 | json.id like.id 27 | json.owner do 28 | json.id like.owner.id 29 | json.name like.owner.username 30 | end 31 | end 32 | end -------------------------------------------------------------------------------- /app/views/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | <%= auth_token %> 3 | 4 |
    5 |

    Sign In

    6 | 9 |
    10 | 11 | <% if flash[:errors] %> 12 |
    13 | <%= flash[:errors].join("
    ").html_safe %> 14 |
    15 | <% end %> 16 | 17 |
    18 | 19 | 21 |
    22 | 23 |
    24 | 25 | 27 |
    28 | 29 |
    30 | 31 |
    32 |
    33 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | 6 | helper_method :current_user, :signed_in? 7 | 8 | private 9 | def current_user 10 | return nil if session[:session_token].nil? 11 | @current_user ||= User.find_by_session_token(session[:session_token]) 12 | end 13 | 14 | def log_in(user) 15 | session[:session_token] = user.reset_session_token! 16 | user.save! 17 | end 18 | 19 | def signed_in? 20 | !!current_user 21 | end 22 | 23 | def logout_current_user 24 | current_user.reset_session_token! 25 | session[:session_token] = nil 26 | end 27 | 28 | def require_signed_in! 29 | redirect_to new_session_url unless signed_in? 30 | end 31 | 32 | def require_signed_out! 33 | redirect_to root_url if signed_in? 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/assets/templates/static_pages/sidebar_item.jst.ejs: -------------------------------------------------------------------------------- 1 |
    2 | "> 3 |
    4 |   5 | <%= notebook.escape("name") %>  6 |
    7 | 8 | (<%= notebook.notes.length %>) 9 | 10 |
    11 | 27 |
    28 |
    29 | 30 |
    -------------------------------------------------------------------------------- /app/assets/javascripts/views/notebooks/notebook_delete.js: -------------------------------------------------------------------------------- 1 | BetterNote.Views.NotebookDelete = Backbone.View.extend({ 2 | initialize: function(options) { 3 | this.$modal = options["$modal"] 4 | }, 5 | 6 | template: JST['notebooks/delete'], 7 | 8 | events: { 9 | "click input[type='submit']": "deleteNotebook" 10 | }, 11 | 12 | render: function() { 13 | var renderedContent = this.template({ 14 | notebook: this.model 15 | }); 16 | 17 | this.$el.html(renderedContent); 18 | return this; 19 | }, 20 | 21 | deleteNotebook: function(event) { 22 | event.preventDefault(); 23 | 24 | if (BetterNote.featuredNotes.id === this.model.get("id")) { 25 | console.log("here") 26 | BetterNote.featuredNotes = BetterNote.notes; 27 | BetterNote.featuredNotes.id = "all"; 28 | } 29 | 30 | var that = this; 31 | this.model.destroy({ 32 | success: function() { 33 | that.$modal.addClass("hidden"); 34 | that.remove(); 35 | } 36 | }) 37 | } 38 | }); -------------------------------------------------------------------------------- /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, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, 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. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require underscore 16 | //= require backbone 17 | //= require jquery.serializeJSON 18 | //= require moment-with-langs 19 | //= require better_note 20 | //= require_tree ../templates 21 | //= require_tree ./models 22 | //= require_tree ./collections 23 | //= require_tree ./views 24 | //= require_tree ./routers 25 | //= require_tree . 26 | -------------------------------------------------------------------------------- /app/assets/javascripts/views/notebooks/notebook_form.js: -------------------------------------------------------------------------------- 1 | BetterNote.Views.NotebookForm = Backbone.View.extend({ 2 | initialize: function(options) { 3 | this.$modal = options["$modal"] 4 | }, 5 | 6 | template: JST['notebooks/form'], 7 | 8 | events: { 9 | "click input[type='submit']": "submitNotebook" 10 | }, 11 | 12 | render: function() { 13 | var renderedContent = this.template({ 14 | notebook: this.model 15 | }); 16 | 17 | this.$el.html(renderedContent); 18 | return this; 19 | }, 20 | 21 | submitNotebook: function(event) { 22 | event.preventDefault(); 23 | 24 | var that = this; 25 | var attrs = $(event.target.form).serializeJSON(); 26 | var success = function() { 27 | that.$modal.addClass("hidden"); 28 | that.remove(); 29 | }; 30 | 31 | this.model.set(attrs); 32 | if (this.model.isNew()) { 33 | BetterNote.notebooks.create(this.model, { 34 | success: success 35 | }); 36 | } else { 37 | this.model.save({}, { 38 | success: success 39 | }) 40 | } 41 | } 42 | }); -------------------------------------------------------------------------------- /app/assets/javascripts/views/tags/tag_form.js: -------------------------------------------------------------------------------- 1 | BetterNote.Views.TagForm = Backbone.View.extend({ 2 | initialize: function(options) { 3 | this.$modal = options["$modal"] 4 | }, 5 | 6 | template: JST['tags/form'], 7 | 8 | events: { 9 | "click input[type='submit']": "submitTag" 10 | }, 11 | 12 | render: function() { 13 | var renderedContent = this.template({ 14 | tag: this.model 15 | }); 16 | 17 | this.$el.html(renderedContent); 18 | return this; 19 | }, 20 | 21 | submitTag: function(event) { 22 | event.preventDefault(); 23 | 24 | var that = this; 25 | var attrs = $(event.target.form).serializeJSON(); 26 | var success = function() { 27 | that.$modal.addClass("hidden"); 28 | that.remove(); 29 | }; 30 | 31 | this.model.set(attrs); 32 | if (this.model.isNew()) { 33 | BetterNote.tags.create(this.model, { 34 | wait: true, 35 | success: success 36 | }); 37 | } else { 38 | this.model.save({}, { 39 | wait: true, 40 | success: success 41 | }) 42 | } 43 | } 44 | }); -------------------------------------------------------------------------------- /app/views/notes/_list_footer.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/api/tags_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::TagsController < ApplicationController 2 | before_action :require_signed_in! 3 | before_action :user_owns_tag?, only: [:edit, :update, :destroy] 4 | 5 | def index 6 | @tags = current_user.tags 7 | render "tags/index" 8 | end 9 | 10 | def create 11 | @tag = current_user.tags.new(tag_params) 12 | if @tag.save 13 | render json: @tag 14 | else 15 | render json: @tag.errors.full_messages, status: :unprocessable_entity 16 | end 17 | end 18 | 19 | def update 20 | @tag = Tag.find(params[:id]) 21 | if @tag.update_attributes(tag_params) 22 | render json: @tag 23 | else 24 | render json: @tag.errors.full_messages, status: :unprocessable_entity 25 | end 26 | end 27 | 28 | def destroy 29 | @tag = Tag.find(params[:id]) 30 | @tag.destroy 31 | render json: {} 32 | end 33 | 34 | private 35 | def user_owns_tag? 36 | @tag = Tag.find(params[:id]) 37 | redirect_to :back unless @tag.owner == current_user 38 | end 39 | 40 | def tag_params 41 | params.require(:tag).permit(:name) 42 | end 43 | end -------------------------------------------------------------------------------- /app/assets/javascripts/views/likes/like_show.js: -------------------------------------------------------------------------------- 1 | BetterNote.Views.LikeShow = Backbone.View.extend({ 2 | initialize: function(options) { 3 | this.note = options["note"]; 4 | this.listenTo(this.collection, "add remove", this.render); 5 | }, 6 | 7 | template: JST['likes/show'], 8 | 9 | className: "note-show-button", 10 | 11 | events: { 12 | "click .like-note": "likeNote", 13 | "click .unlike-note": "unlikeNote" 14 | }, 15 | 16 | render: function() { 17 | var renderedContent = this.template({ 18 | note: this.note 19 | }); 20 | 21 | this.$el.html(renderedContent); 22 | 23 | var buttonClass = (this.note.currentUserLike()) ? "unlike-note" : "like-note"; 24 | var $button = this.$el.find(".like-button") 25 | $button.removeClass("unlike-note like-note"); 26 | $button.addClass(buttonClass); 27 | 28 | return this; 29 | }, 30 | 31 | likeNote: function(event) { 32 | event.preventDefault(); 33 | 34 | this.collection.create({ 35 | note_id: this.note.get("id") 36 | }); 37 | }, 38 | 39 | unlikeNote: function(event) { 40 | event.preventDefault(); 41 | this.note.currentUserLike().destroy(); 42 | } 43 | }); -------------------------------------------------------------------------------- /app/views/notebooks/_index.html.erb: -------------------------------------------------------------------------------- 1 | 36 | -------------------------------------------------------------------------------- /app/views/notes/show.html.erb: -------------------------------------------------------------------------------- 1 |

    Note Show Page

    2 | 3 |
    4 |
    5 | Title: <%= @note.title %> 6 |
    7 |
    8 | Body: <%= @note.body %> 9 |

    10 | 11 |
    12 |

    Note Information:

    13 | <%= render partial: "info_old" %> 14 |
    15 | 16 |
    17 | <% if @note.likers.include?(current_user) %> 18 | <%= button_to "Remove Like", 19 | user_note_like_url(@note.author, @note, @note.likes.find_by( 20 | {owner_id: current_user.id})), method: :delete %> 21 | <% else %> 22 | <%= button_to "Like Note", 23 | user_note_likes_url(@note.author, @note), method: :post %> 24 | <% end %> 25 | <% if current_user == @note.author %> 26 | <%= button_to "Edit Note", edit_user_note_url(current_user, @note), method: :get %> 27 | <%= button_to "Delete Note", user_note_url(current_user, @note), method: :delete %> 28 | <% end %> 29 |
    30 | 31 |
    32 |

    Comments:

    33 | <%= render partial: "comments", locals: { comments: @note.comments, note: @note } %> 34 |
    35 |
    -------------------------------------------------------------------------------- /app/models/notification.rb: -------------------------------------------------------------------------------- 1 | class Notification < ActiveRecord::Base 2 | validates :user, :notifiable_id, :notifiable_type, presence: true 3 | 4 | belongs_to :user 5 | belongs_to :notifiable, polymorphic: true 6 | 7 | include Rails.application.routes.url_helpers 8 | 9 | def text 10 | if self.notifiable_type == "Comment" 11 | return "#{notifiable.author.username} commented on one of your notes!" 12 | elsif self.notifiable_type == "Like" 13 | return "#{notifiable.owner.username} liked one of your notes!" 14 | elsif self.notifiable_type == "Friendship" 15 | return "#{notifiable.in_friend.username} accepted your friend request!" 16 | end 17 | end 18 | 19 | def url 20 | # if self.notifiable_type == "Comment" 21 | # return note_url(notifiable.note) 22 | # elsif self.notifiable_type == "Like" 23 | # return note_url(notifiable.note) 24 | # elsif self.notifiable_type == "Friendship" 25 | # return user_url(notifiable.in_friend) 26 | # end 27 | end 28 | 29 | def default_url_options 30 | options = {}; 31 | options[:host] = (Rails.env == "development") ? 32 | "localhost:3000" : "http://infinite-bayou-9601.herokuapp.com/" 33 | options 34 | end 35 | end -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | BetterNote::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 and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | end 30 | -------------------------------------------------------------------------------- /app/assets/javascripts/views/notes/note_delete.js: -------------------------------------------------------------------------------- 1 | BetterNote.Views.NoteDelete = Backbone.View.extend({ 2 | initialize: function(options) { 3 | this.$modal = options["$modal"]; 4 | this.noteShowView = options["noteShowView"]; 5 | }, 6 | 7 | template: JST['notes/delete'], 8 | 9 | events: { 10 | "click input[type='submit']": "deleteNote" 11 | }, 12 | 13 | render: function() { 14 | var renderedContent = this.template({ 15 | note: this.model 16 | }); 17 | 18 | this.$el.html(renderedContent); 19 | return this; 20 | }, 21 | 22 | deleteNote: function(event) { 23 | event.preventDefault(); 24 | 25 | if (BetterNote.featuredNotes.length > 1) { 26 | var nextNoteId = BetterNote.featuredNotes.nextNote(this.model).get("id"); 27 | var nextNote = BetterNote.notes.get(nextNoteId); 28 | BetterNote.featuredNote = nextNote; 29 | } 30 | 31 | var that = this; 32 | this.model.destroy({ 33 | success: function() { 34 | that.$modal.addClass("hidden"); 35 | that.remove(); 36 | if (nextNoteId) { 37 | BetterNote.router.navigate("#/notes/" + nextNoteId); 38 | } else { 39 | that.noteShowView.remove(); 40 | } 41 | } 42 | }) 43 | } 44 | }); -------------------------------------------------------------------------------- /app/assets/javascripts/views/notes/note_info.js: -------------------------------------------------------------------------------- 1 | BetterNote.Views.NoteInfo = Backbone.View.extend({ 2 | initialize: function(options) { 3 | this.listenTo(this.model.likes, "add remove", this.render); 4 | 5 | if (!this.model.friendNote) { 6 | this.listenTo(this.model.noteTags, "add remove", this.render); 7 | } 8 | }, 9 | 10 | template: JST['notes/info'], 11 | tagName: "table", 12 | className: "note-info options-dropdown hidden", 13 | 14 | events: { 15 | "click .dropdown-parent": "showDropdown", 16 | "click .options-dropdown": "stopPropagation" 17 | }, 18 | 19 | render: function() { 20 | var renderedContent = this.template({ 21 | note: this.model, 22 | notebooks: BetterNote.notebooks 23 | }); 24 | 25 | this.$el.html(renderedContent); 26 | return this; 27 | }, 28 | 29 | showDropdown: function(event) { 30 | event.preventDefault(); 31 | this.hideDropdowns(); 32 | 33 | var $dropdown = $(event.currentTarget).find(".options-dropdown"); 34 | $dropdown.removeClass("hidden"); 35 | }, 36 | 37 | hideDropdowns: function(event) { 38 | $(".options-dropdown").not("hidden").addClass("hidden"); 39 | }, 40 | 41 | stopPropagation: function(event) { 42 | event.stopPropagation(); 43 | }, 44 | }); -------------------------------------------------------------------------------- /app/views/layouts/public.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | BetterNote 5 | <%= stylesheet_link_tag "public" %> 6 | <%= javascript_include_tag "application" %> 7 | 8 | <%= csrf_meta_tags %> 9 | 10 | 11 | 12 | 13 |
    14 | 22 |
    23 | 24 |

    Welcome to BetterNote

    25 |

    Like Evernote. But Better*.

    26 | 27 |
    28 | Demo User (Employers Click Here) 29 |
    30 | 31 |
    32 | <%= yield %> 33 |
    34 | 35 | 36 | 44 | 45 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | BetterNote::Application.routes.draw do 2 | root to: "static_pages#root" 3 | resources :users, only: [:create, :new, :show, :index]# do 4 | # resources :friend_requests, only: [:create] do 5 | # post "accept", to: "friend_requests#accept" 6 | # post "deny", to: "friend_requests#deny" 7 | # end 8 | # resources :friendships, only: [:destroy] 9 | # end 10 | # resources :likes, only: [:destroy] 11 | # resources :notes, only: [:index, :show, :edit, :update, :destroy] do 12 | # resources :comments, only: [:create] 13 | # resources :likes, only: [:create] 14 | # end 15 | # resources :comments, only: [:destroy] 16 | # resources :tags 17 | # resources :notebooks do 18 | # resources :notes, only: [:create] 19 | # end 20 | resource :session, only: [:create, :new, :destroy] 21 | namespace :api, defaults: { format: :json } do 22 | resources :notes, only: [:create, :show, :index, :update, :destroy] 23 | resources :notebooks, only: [:create, :show, :index, :update, :destroy] 24 | resources :tags, only: [:index, :create, :update, :destroy] 25 | resources :comments, only: [:create, :destroy] 26 | resources :likes, only: [:create, :destroy] 27 | resources :note_tags, only: [:create, :destroy] 28 | resources :friendships, only: [:destroy] 29 | end 30 | end -------------------------------------------------------------------------------- /app/controllers/friend_requests_controller.rb: -------------------------------------------------------------------------------- 1 | class FriendRequestsController < ApplicationController 2 | before_action :require_signed_in! 3 | before_action :authorized?, only: [:accept, :deny] 4 | 5 | def create 6 | @friend_request = current_user.friend_requests.new({ 7 | in_friend_id: params[:user_id] 8 | }) 9 | if @friend_request.save 10 | redirect_to users_url 11 | else 12 | flash[:errors] = @friend_request.errors.full_messages 13 | redirect_to users_url 14 | end 15 | end 16 | 17 | def deny 18 | @friend_request = FriendRequest.find(params[:friend_request_id]) 19 | @friend_request.destroy 20 | redirect_to users_url 21 | end 22 | 23 | def accept 24 | @friend_request = FriendRequest.find(params[:friend_request_id]) 25 | create_friendships(@friend_request) 26 | @friend_request.destroy 27 | redirect_to users_url 28 | end 29 | 30 | private 31 | def authorized? 32 | current_user.id == params[:user_id] 33 | end 34 | 35 | def create_friendships(friend_request) 36 | Friendship.create!({ 37 | in_friend_id: friend_request.in_friend_id, 38 | out_friend_id: friend_request.out_friend_id 39 | }) 40 | Friendship.create!({ 41 | in_friend_id: friend_request.out_friend_id, 42 | out_friend_id: friend_request.in_friend_id 43 | }) 44 | end 45 | end -------------------------------------------------------------------------------- /app/views/users/index.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |

    All Users

    3 | 4 | 27 |
    28 | 29 |
  • 30 | 31 |
    32 |   33 | Add New Notebook 34 |
    35 |
    36 |
  • -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | BetterNote 5 | <%= stylesheet_link_tag "application" %> 6 | <%= javascript_include_tag "application" %> 7 | 8 | <%= csrf_meta_tags %> 9 | 10 | 11 | 12 | 20 | 21 | <%= render partial: "static_pages/nav" %> 22 | 23 |
    24 | <%= flash[:errors].join("
    ").html_safe if flash[:errors] %> 25 |
    26 | 27 |
    28 | <%= yield %> 29 |
    30 | 31 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/assets/templates/static_pages/search_bar.jst.ejs: -------------------------------------------------------------------------------- 1 |
    2 | 6 |
    7 | 39 |
    40 | 44 |
    -------------------------------------------------------------------------------- /app/views/notes/_info.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 37 | 38 | 39 | 40 | 43 | 44 |
    Title:<%= note.title %>
    Author:<%= note.author.username %>
    Created At:<%= note.created_at.strftime("%b %d, %Y") %>
    Updated At:<%= note.updated_at.strftime("%b %d, %Y") %>
    Notebook:<%= note.notebook.name %>
    Tags: 25 | <% if note.tags.length > 0 %> 26 |
      27 | <% note.tags.each do |tag| %> 28 |
    • 29 | <%= tag.name %> 30 |
    • 31 | <% end %> 32 |
    33 | <% else %> 34 | No tags 35 | <% end %> 36 |
    Likes: 41 | <%= note.likes.count.to_s + " " + "like".pluralize(note.likes.count) %> 42 |
    -------------------------------------------------------------------------------- /app/assets/templates/notes/friend_show.jst.ejs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    5 | 6 | <%= note.get("notebook") %> 7 |
    8 |
    9 | 17 |
    18 | 24 |
    25 |
    26 |
    27 | 35 |
    36 |
    37 |
    38 |
    39 |
    40 |
    41 | <%= note.get('title') %> 42 |
    43 |
    44 | <%= note.get("body") %> 45 |
    46 |
    -------------------------------------------------------------------------------- /app/controllers/api/notebooks_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::NotebooksController < ApplicationController 2 | before_action :require_signed_in! 3 | before_action :user_owns_notebook?, only: [:edit, :update, :destroy, :show] 4 | 5 | def create 6 | @notebook = current_user.notebooks.new(notebook_params) 7 | if @notebook.save 8 | render json: @notebook 9 | else 10 | render json: @notebook.errors.full_messages, status: :unprocessable_entity 11 | end 12 | end 13 | 14 | def index 15 | @notebooks = current_user.notebooks 16 | render "notebooks/index" 17 | end 18 | 19 | def show 20 | @notebook = Notebook.includes(:notes).find(params[:id]) 21 | render "notebooks/show" 22 | end 23 | 24 | def update 25 | @notebook = Notebook.includes(:notes).find(params[:id]) 26 | if @notebook.update_attributes(notebook_params) 27 | render json: @notebook.to_json(include: :notes) 28 | else 29 | render json: @notebook.errors.full_messages, status: :unprocessable_entity 30 | end 31 | end 32 | 33 | def destroy 34 | @notebook = Notebook.find(params[:id]) 35 | @notebook.destroy 36 | render json: {} 37 | end 38 | 39 | private 40 | def notebook_params 41 | params.require(:notebook).permit(:name) 42 | end 43 | 44 | def user_owns_notebook? 45 | @notebook = Notebook.find(params[:id]) 46 | redirect_to root_url unless @notebook.owner == current_user 47 | end 48 | end -------------------------------------------------------------------------------- /app/assets/templates/notes/info.jst.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 37 | 38 | 39 | 40 | 43 | 44 |
    Title:<%= note.escape("title") %>
    Author:<%= note.author.escape("username") %>
    Created: 13 | <%= moment(note.escape("created_at")).format('MMM D, YYYY') %> 14 |
    Updated: 19 | <%= moment(note.escape("updated_at")).format('MMM D, YYYY') %> 20 |
    Notebook:<%= (note.friendNote) ? note.escape("notebook") : note.notebook.escape("name") %>
    Tags: 29 | <% if (note.noteTags.length > 0) { %> 30 | <%= note.noteTags.map(function(noteTag) { 31 | return noteTag.escape("tag_name") 32 | }).join(", ") %> 33 | <% } else { %> 34 | No tags 35 | <% } %> 36 |
    Likes: 41 | <%= note.likes.length %> 42 |
    -------------------------------------------------------------------------------- /app/views/notes/_edit.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | <%= auth_token %> 3 | 4 | 5 | 6 |
    7 | 9 |
    10 | 11 |
    12 | 13 |
    14 | 15 |
    16 | 17 | 25 |
    26 | 27 |
    28 | Tags 29 | 30 | <% current_user.tags.each_with_index do |tag, i| %> 31 | > 35 | <%= tag.name %> 36 | 37 |
    38 | <% end %> 39 |
    40 | 41 |
    42 | 43 |
    44 |
    -------------------------------------------------------------------------------- /app/assets/javascripts/collections/notes.js: -------------------------------------------------------------------------------- 1 | BetterNote.Collections.Notes = Backbone.Collection.extend({ 2 | 3 | model: BetterNote.Models.Note, 4 | 5 | url: "/api/notes", 6 | 7 | nextNote: function(note) { 8 | var noteIndex = this.indexOf(note); 9 | var nextIndex = (noteIndex < this.length - 1) ? noteIndex + 1 : noteIndex - 1 10 | return this.at(nextIndex); 11 | }, 12 | 13 | comparator: function(note1, note2) { 14 | if (note1.isNew()) { 15 | return -1; 16 | } else if (note2.isNew()) { 17 | return 1; 18 | }; 19 | 20 | if (note1.get("created_at") > note2.get("created_at")) { 21 | return -1; 22 | } else if (note1.get("created_at") < note2.get("created_at")) { 23 | return 1; 24 | } else { 25 | return 0; 26 | } 27 | }, 28 | 29 | setComparator: function(sortField, sortToggle) { 30 | this.comparator = function(note1, note2) { 31 | var field1 = note1.get(sortField); 32 | var field2 = note2.get(sortField); 33 | 34 | if (note1.isNew()) { 35 | return -1 36 | } else if (note2.isNew()) { 37 | return 1 38 | }; 39 | 40 | if (sortField === "title") { 41 | field1 = field1.toLowerCase(); 42 | field2 = field2.toLowerCase(); 43 | } 44 | 45 | if (field1 > field2) { 46 | return 1 * sortToggle; 47 | } else if (field1 < field2) { 48 | return -1 * sortToggle; 49 | } else { 50 | return 0; 51 | } 52 | } 53 | } 54 | }); -------------------------------------------------------------------------------- /spec/factories.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :user do 3 | email { Faker::Internet.safe_email } 4 | username { Faker::Internet.user_name } 5 | password { Faker::Internet.password } 6 | end 7 | 8 | factory :note do 9 | title { Faker::Lorem.sentence(3) } 10 | body { Faker::Lorem.paragraph } 11 | author_id { Faker::Number.digit } 12 | notebook_id { Faker::Number.digit } 13 | end 14 | 15 | factory :notebook do 16 | name { Faker::Lorem.sentence(5) } 17 | owner_id { Faker::Number.digit } 18 | end 19 | 20 | factory :tag do 21 | name { Faker::Lorem.sentence(2) } 22 | owner_id { Faker::Number.digit } 23 | end 24 | 25 | factory :note_tag do 26 | note_id { Faker::Number.digit } 27 | tag_id { Faker::Number.digit } 28 | end 29 | 30 | factory :comment do 31 | author_id { Faker::Number.digit } 32 | note_id { Faker::Number.digit } 33 | end 34 | 35 | factory :like do 36 | note_id { Faker::Number.digit } 37 | owner_id { Faker::Number.digit } 38 | end 39 | 40 | factory :friendship do 41 | in_friend_id { Faker::Number.digit } 42 | out_friend_id { Faker::Number.digit } 43 | end 44 | 45 | factory :friend_request do 46 | in_friend_id { Faker::Number.digit } 47 | out_friend_id { Faker::Number.digit } 48 | end 49 | 50 | factory :notification do 51 | user_id { Faker::Number.digit } 52 | notifiable_id { Faker::Number.digit } 53 | notifiable_type { Faker::Lorem.sentence(1) } 54 | end 55 | end -------------------------------------------------------------------------------- /app/assets/javascripts/views/static_pages/navbar.js: -------------------------------------------------------------------------------- 1 | BetterNote.Views.NavBar = Backbone.View.extend({ 2 | tagName: "header", 3 | className: "main group", 4 | template: JST['static_pages/navbar'], 5 | 6 | events: { 7 | "click .nav-right-item": "showDropdown", 8 | "click .options-dropdown, .nav-right-item": "stopPropagation", 9 | "click .about": "showModal" 10 | }, 11 | 12 | render: function() { 13 | var renderedContent = this.template(); 14 | this.$el.html(renderedContent); 15 | return this; 16 | }, 17 | 18 | showDropdown: function(event) { 19 | event.preventDefault(); 20 | 21 | var $dropdown = $(event.currentTarget).find(".options-dropdown"); 22 | $dropdown.removeClass("hidden"); 23 | }, 24 | 25 | hideDropdowns: function(event) { 26 | $(".options-dropdown").not("hidden").addClass("hidden"); 27 | }, 28 | 29 | stopPropagation: function(event) { 30 | event.stopPropagation(); 31 | }, 32 | 33 | showModal: function(event) { 34 | event.preventDefault(); 35 | 36 | var view = new BetterNote.Views.About(); 37 | var $modal = $("#modal"); 38 | var $modalContent = $(".modal-content") 39 | 40 | $modal.removeClass("hidden"); 41 | $modalContent.html(view.render().$el); 42 | }, 43 | 44 | closeModal: function(event) { 45 | event.preventDefault(); 46 | 47 | var $modal = $("#modal"); 48 | var $modalContent = $(".modal-content") 49 | 50 | $modal.addClass("hidden"); 51 | $modalContent.html(""); 52 | } 53 | }); -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 48 | 49 | 50 | 51 | 52 |
    53 |

    We're sorry, but something went wrong.

    54 |
    55 |

    If you are the application owner check the logs for more information.

    56 | 57 | 58 | -------------------------------------------------------------------------------- /app/views/notes/edit.html.erb: -------------------------------------------------------------------------------- 1 |

    Edit Note

    2 |
    3 | <%= auth_token %> 4 | 5 | 6 | 7 |
    8 | 9 | 11 |
    12 | 13 |
    14 | 15 | 16 |
    17 | 18 |
    19 | 20 | 28 |
    29 | 30 |
    31 | Tags 32 | 33 | <% current_user.tags.each_with_index do |tag, i| %> 34 | > 38 | <%= tag.name %> 39 | 40 |
    41 | <% end %> 42 |
    43 | 44 |
    45 | 46 |
    47 |
    -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 48 | 49 | 50 | 51 | 52 |
    53 |

    The change you wanted was rejected.

    54 |

    Maybe you tried to change something you didn't have access to.

    55 |
    56 |

    If you are the application owner check the logs for more information.

    57 | 58 | 59 | -------------------------------------------------------------------------------- /app/assets/templates/comments/index.jst.ejs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |   4 | 5 | Comments 6 |
    7 |
    8 |
    9 | <% if (comments.length === 0) { %> 10 |
    11 | No comments 12 |
    13 | <% } %> 14 | 40 |
    41 |
    42 |   43 | New Comment 44 |
    45 |
    46 |
    -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 48 | 49 | 50 | 51 | 52 |
    53 |

    The page you were looking for doesn't exist.

    54 |

    You may have mistyped the address or the page may have moved.

    55 |
    56 |

    If you are the application owner check the logs for more information.

    57 | 58 | 59 | -------------------------------------------------------------------------------- /app/assets/javascripts/views/notes/friend_note_show.js: -------------------------------------------------------------------------------- 1 | BetterNote.Views.FriendNoteShow = Backbone.View.extend({ 2 | template: JST['notes/friend_show'], 3 | 4 | events: { 5 | "click .dropdown-parent": "showDropdown", 6 | "click .options-dropdown, .dropdown-parent": "stopPropagation", 7 | }, 8 | 9 | render: function() { 10 | BetterNote.featuredNote = this.model; 11 | var renderedContent = this.template({ 12 | note: this.model 13 | }); 14 | 15 | this.$el.html(renderedContent); 16 | 17 | var infoView = new BetterNote.Views.NoteInfo({ 18 | model: this.model 19 | }) 20 | 21 | var commentView = new BetterNote.Views.CommentsIndex({ 22 | collection: this.model.comments, 23 | note: this.model 24 | }); 25 | 26 | var likeView = new BetterNote.Views.LikeShow({ 27 | collection: this.model.likes, 28 | note: this.model 29 | }); 30 | 31 | this.$el.append(commentView.render().$el); 32 | this.$el.find(".note-show-header-right.top").append(infoView.render().$el); 33 | this.$el.find(".note-show-header-right.bottom").prepend(likeView.render().$el); 34 | 35 | return this; 36 | }, 37 | 38 | showDropdown: function(event) { 39 | event.preventDefault(); 40 | this.hideDropdowns(); 41 | 42 | var $dropdown = $(event.currentTarget).find(".options-dropdown"); 43 | $dropdown.removeClass("hidden"); 44 | }, 45 | 46 | hideDropdowns: function(event) { 47 | $(".options-dropdown").not("hidden").addClass("hidden"); 48 | }, 49 | 50 | stopPropagation: function(event) { 51 | event.stopPropagation(); 52 | } 53 | }); -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 4 | gem 'rails', '4.0.4' 5 | 6 | ruby '2.1.1' 7 | 8 | # Use postgresql as the database for Active Record 9 | gem 'pg' 10 | 11 | # Use SCSS for stylesheets 12 | gem 'sass-rails', '~> 4.0.2' 13 | 14 | # Use Uglifier as compressor for JavaScript assets 15 | gem 'uglifier', '>= 1.3.0' 16 | 17 | # Use CoffeeScript for .js.coffee assets and views 18 | gem 'coffee-rails', '~> 4.0.0' 19 | 20 | # See https://github.com/sstephenson/execjs#readme for more supported runtimes 21 | # gem 'therubyracer', platforms: :ruby 22 | 23 | # Use jquery as the JavaScript library 24 | gem 'jquery-rails' 25 | 26 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 27 | gem 'jbuilder', '~> 1.2' 28 | 29 | group :doc do 30 | # bundle exec rake doc:rails generates the API under doc/api. 31 | gem 'sdoc', require: false 32 | end 33 | 34 | group :development do 35 | gem 'binding_of_caller' 36 | gem 'better_errors' 37 | end 38 | 39 | group :development, :test do 40 | gem 'rspec-rails' 41 | gem 'factory_girl_rails' 42 | end 43 | 44 | group :test do 45 | gem 'shoulda-matchers' 46 | gem 'capybara' 47 | gem 'guard-rspec' 48 | gem 'launchy' 49 | end 50 | 51 | gem 'bcrypt' 52 | gem 'pg_search' 53 | 54 | gem 'rails_12factor', group: :production 55 | gem 'font-awesome-rails' 56 | gem 'backbone-on-rails' 57 | gem 'faker' 58 | 59 | # Use unicorn as the app server 60 | # gem 'unicorn' 61 | 62 | # Use Capistrano for deployment 63 | # gem 'capistrano', group: :development 64 | 65 | # Use debugger 66 | # gem 'debugger', group: [:development, :test] 67 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | BetterNote::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 static asset server for tests with Cache-Control for performance. 16 | config.serve_static_assets = true 17 | config.static_cache_control = "public, max-age=3600" 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Print deprecation notices to the stderr. 35 | config.active_support.deprecation = :stderr 36 | end 37 | -------------------------------------------------------------------------------- /app/assets/templates/tags/show.jst.ejs: -------------------------------------------------------------------------------- 1 | DELETE 2 |
    3 | 4 | <%= tag.get("name") %> 5 |
    6 | 25 | -------------------------------------------------------------------------------- /app/assets/javascripts/views/note_tags/note_tags_index.js: -------------------------------------------------------------------------------- 1 | BetterNote.Views.NoteTagsIndex = Backbone.View.extend({ 2 | initialize: function(options) { 3 | this.note = options["note"]; 4 | this.listenTo(this.collection, "add remove", this.render); 5 | }, 6 | 7 | template: JST['note_tags/index'], 8 | tagName: "ul", 9 | className: "tags", 10 | 11 | events: { 12 | "click .dropdown-parent": "showDropdown", 13 | "click .options-dropdown, .dropdown-parent": "stopPropagation", 14 | "click .add-tag>ul>li": "addNoteTag", 15 | "click .delete-tag": "deleteNoteTag" 16 | }, 17 | 18 | render: function() { 19 | var renderedContent = this.template({ 20 | noteTags: this.collection 21 | }); 22 | 23 | this.$el.html(renderedContent); 24 | return this; 25 | }, 26 | 27 | showDropdown: function(event) { 28 | event.preventDefault(); 29 | 30 | var $dropdown = $(event.currentTarget).find(".options-dropdown"); 31 | $dropdown.removeClass("hidden"); 32 | }, 33 | 34 | stopPropagation: function(event) { 35 | event.stopPropagation(); 36 | }, 37 | 38 | addNoteTag: function(event) { 39 | event.preventDefault(); 40 | 41 | var tagId = parseInt($(event.currentTarget).attr("data-tag-id")); 42 | var noteTag = new BetterNote.Models.NoteTag({ 43 | note_id: this.note.get("id"), 44 | tag_id: tagId, 45 | tag_name: BetterNote.tags.get(tagId).get("name") 46 | }); 47 | 48 | noteTag.save(); 49 | }, 50 | 51 | deleteNoteTag: function(event) { 52 | event.preventDefault(); 53 | 54 | var tagId = parseInt($(event.currentTarget).attr("data-tag-id")); 55 | var noteTag = this.collection.findWhere({ 56 | tag_id: tagId 57 | }); 58 | 59 | noteTag.destroy(); 60 | } 61 | }); -------------------------------------------------------------------------------- /app/assets/javascripts/better_note.js: -------------------------------------------------------------------------------- 1 | window.BetterNote = { 2 | Models: {}, 3 | Collections: {}, 4 | Views: {}, 5 | Routers: {}, 6 | initialize: function() { 7 | var $content = $('.content'); 8 | var $noteShowEl = $('.note-show'); 9 | var $notesListEl = $('.notes-list'); 10 | var data = JSON.parse($('#bootstrapped-data-json').html()); 11 | 12 | this.currentUser = new BetterNote.Models.User(data.user); 13 | this.notebooks = new BetterNote.Collections.Notebooks(data.notebooks); 14 | this.tags = new BetterNote.Collections.Tags(data.tags); 15 | this.notes = new BetterNote.Collections.Notes(data.notes, { parse: true }); 16 | this.friendNotes = new BetterNote.Collections.Notes(); 17 | this.friends = new BetterNote.Collections.Users(data.friends, { parse: true }); 18 | this.notebooks.each(function(notebook) { 19 | notebook.notes.sort(); 20 | }); 21 | 22 | this.featuredNote = this.notes.at(0); 23 | this.featuredNotes = this.notes; 24 | this.featuredNotes.id = "all"; 25 | this.featuredNotebook = this.notebooks.at(0); 26 | this.filter = new BetterNote.Models.Filter({ 27 | text: "", 28 | tag: null 29 | }); 30 | 31 | this.router = new BetterNote.Routers.Notes($notesListEl, $noteShowEl); 32 | 33 | var sideBarView = new BetterNote.Views.Sidebar(); 34 | var searchBarView = new BetterNote.Views.SearchBar(); 35 | var navBarView = new BetterNote.Views.NavBar(); 36 | $content.prepend(navBarView.render().$el); 37 | $content.prepend(sideBarView.render().$el); 38 | $content.prepend(searchBarView.render().$el); 39 | Backbone.history.start(); 40 | this.router.navigate("#/"); 41 | } 42 | }; 43 | 44 | $(document).ready(function(){ 45 | BetterNote.initialize(); 46 | }); -------------------------------------------------------------------------------- /app/assets/templates/notebooks/show.jst.ejs: -------------------------------------------------------------------------------- 1 | DELETE 2 |
    3 | 4 | <%= notebook.get("name") %> 5 |
    6 | 25 | -------------------------------------------------------------------------------- /app/views/notes/_comments.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |   5 | Comments 6 |
    7 |
    8 | <% if comments.count == 0 && !params[:new_comment] %> 9 |
    10 | No comments 11 |
    12 | <% end %> 13 | 40 | 41 | <% if params[:new_comment] %> 42 | <%= render partial: "comments/form", locals: { note: note } %> 43 | <% else %> 44 |
    45 | "> 46 |   47 | New Comment 48 | 49 |
    50 | <% end %> 51 |
    -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | ENV["RAILS_ENV"] ||= 'test' 3 | require File.expand_path("../../config/environment", __FILE__) 4 | require 'rspec/rails' 5 | require 'rspec/autorun' 6 | 7 | # Requires supporting ruby files with custom matchers and macros, etc, 8 | # in spec/support/ and its subdirectories. 9 | Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } 10 | 11 | # Checks for pending migrations before tests are run. 12 | # If you are not using ActiveRecord, you can remove this line. 13 | ActiveRecord::Migration.check_pending! if defined?(ActiveRecord::Migration) 14 | 15 | RSpec.configure do |config| 16 | # ## Mock Framework 17 | # 18 | # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line: 19 | # 20 | # config.mock_with :mocha 21 | # config.mock_with :flexmock 22 | # config.mock_with :rr 23 | 24 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 25 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 26 | 27 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 28 | # examples within a transaction, remove the following line or assign false 29 | # instead of true. 30 | config.use_transactional_fixtures = true 31 | 32 | # If true, the base class of anonymous controllers will be inferred 33 | # automatically. This will be the default behavior in future versions of 34 | # rspec-rails. 35 | config.infer_base_class_for_anonymous_controllers = false 36 | 37 | # Run specs in random order to surface order dependencies. If you find an 38 | # order dependency and want to debug it, you can fix the order by providing 39 | # the seed, which is printed after each run. 40 | # --seed 1234 41 | config.order = "random" 42 | end 43 | -------------------------------------------------------------------------------- /app/controllers/api/notes_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::NotesController < ApplicationController 2 | before_action :require_signed_in! 3 | before_action :user_owns_note?, only: [:edit, :update, :destroy] 4 | before_action :authorized?, only: [:show] 5 | 6 | def index 7 | @notes = current_user.notes.includes(:comments, :notebook, :tags) 8 | render partial: "notes/index", locals: { notes: @notes } 9 | end 10 | 11 | def create 12 | @note = current_user.notes.new(note_params) 13 | if @note.save 14 | render json: @note 15 | else 16 | render json: @note.errors.full_messages, status: :unprocessable_entity 17 | end 18 | end 19 | 20 | def edit 21 | @note = Note.find(params[:id]) 22 | render json: @note 23 | end 24 | 25 | def update 26 | @note = Note.find(params[:id]) 27 | @note.assign_attributes(note_params) 28 | 29 | if @note.save 30 | render partial: "notes/show", locals: { note: @note } 31 | else 32 | render json: @note.errors.full_messages, status: :unprocessable_entity 33 | end 34 | end 35 | 36 | def show 37 | @note = Note.includes(:comments, :notebook, :tags).find(params[:id]) 38 | render partial: "notes/show", locals: { note: @note } 39 | end 40 | 41 | def destroy 42 | @note = Note.find(params[:id]) 43 | @note.destroy 44 | render json: {} 45 | end 46 | 47 | private 48 | def user_owns_note? 49 | @note = Note.find(params[:id]) 50 | redirect_to root_url unless @note.author == current_user 51 | end 52 | 53 | def authorized? 54 | author = Note.find(params[:id]).author 55 | unless (current_user == author || user.find_friendship(current_user)) 56 | redirect_to root_url 57 | end 58 | end 59 | 60 | def note_params 61 | params.require(:note).permit(:title, :body, :notebook_id) 62 | end 63 | end -------------------------------------------------------------------------------- /app/assets/templates/notes/index.jst.ejs: -------------------------------------------------------------------------------- 1 |
    2 | <% if (type === "all") { %> 3 | 4 | All Notes 5 | <% } else if (type === "notebook") { %> 6 | 7 | <%= model.get("name") %> 8 | <% } else if (type === "friend") { %> 9 | 10 | <%= model.get("username") %> 11 | <% } %> 12 |
    13 | 15 | -------------------------------------------------------------------------------- /betternote: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: AES-128-CBC,6D5845B1BB06D5A11B0722FEDB06C944 4 | 5 | Nh/SSFY077+dQPSr/VJHmlUphf+Vp4A7Hj7u0LvKfxQlSxefIFH/y0YLwaGH/saZ 6 | 7Qd1Dwe+gw6Kk+R9REWKXhTlxaYM4PZa8l7OnyuCJdMlbTkRu1ml+nhbHBA87O7j 7 | Mzta2qkYXgxV2DOUpGCwsifMoZkAYa3jGWty5rj5XhardTy4jblJGDXjrS+yV1zO 8 | rRfhyihFF/ZYx0Omip1Wstj4H1nW71tLhXxiyW3cNZtXfL9VOqy4sfL1HmUefB3Z 9 | j0tJuwGz+BLrJ0ZLQN9/cmHqhR+g2QFQ5HV6lb8HRUiycqRwg64xBXsRtfu9O2AW 10 | FPtrurdVd1SxEervng1Rc2KK4aLppZ5ZPZN6D0f3JvekGHenMiQWXsNmmddDYw3B 11 | QMTCScgPdE+phpJgziFKf/1kJxp44A+Y8cAtyyjLadLMfURFbFEKw1n8d2t3ei9V 12 | eL7fD8uUXTwiKIjnU/Mg0x1N8Im7Nh3HYkUbB8ECxYspeMtHQaJig/zufxLGThTQ 13 | 8EbBI9qeNd8tRflDVPQej+fmKfP3Tur3IKmuXj47nlQ9f+umfU+xQed3OQ57eAIU 14 | uKaIh9ByhLQ/3R6BCkpY+o3d4eyWQ41disAL9X8/ogm5vikUdq9v68XlPR2LoPqq 15 | oGV1Cx5TSFt1s3irKP8ePAQkKvIPcKoDWFbdKvIMoTQ7EozwDSr6hYTiK2iyEEAJ 16 | /zl3Me+fdRXmSRQ+IzHD5BjyhImXcVgXJwOeDb9Oyr4wPa8gCEm0zv7YXKxinolk 17 | bapFupOptxG8Kbv8yluLaF/3kPMdJ6xwi5jz7Pta9diCEJPPSDMm2bXZ3MXwLolz 18 | i66iRiVW0dVxB0JkvZNfoY8yQ/1NxJC6PiUOupKWDmBcwjyeoY2xBQy9OAJTXWFv 19 | DRnrj31PGki8qw9dvErI7ubCteN/oRY+ghFi06WECexX//6D6gLZ8DAJuS4pFcrW 20 | QDx35kK6JGJREIGG4mypVWFicLBy/Pa6OIzEyCTNROSJviVP/ex2V1ddg7nNWSi1 21 | XTXL5k0YmwiA6M6W4ozGNhAPEF7RSOyxa3v7EIy74D40YobImnU/TjXmLSgkBgr5 22 | XfieY4/TNFFUgsW03QXy8B9aPhN0mVXQbxsRp065z4GBCZi3+M2SPeyTTI1SjnCT 23 | TM/LrqwyuKjHYGvwwx0FANstPAwll55nXODFpFTAA2iEgZ+cdQ+U4ubgp6DpolxZ 24 | RpjnGDqs1/DIQbkkYsws8ejhHXKIAOdvg1YSpn+5uRXdhl4KRCALtYOq5TswElfJ 25 | 3hRSlBdLxyy00sdoXjAHPSIrEjmrbFWd0spBnHKCiCVlla5JkN2vo/kpA1nt6uqy 26 | gTtXBqcw23qZRSDIzl8ou/Um/vhtN9dPfKqbGpgwciUF+2QCCQwPlaopIKVVpsQN 27 | JXEMpiBzWXTkhs98tqc1s4o9SMOIVjYJWYzV2D7lzeFeb6lRVXqkHqCla3nQO3p7 28 | TMEd8Jp4FbAVpLxI3Py71Ioa3NRdMwvtzrm0C8Fx98Qzx531n3yNbZDqytB7Jm9W 29 | Rj4I8tN+hlapqrglDtS46iXcd7o8m/aTqq/NVLSKIniWVaE2ixlQa5rcdkeGoFxx 30 | -----END RSA PRIVATE KEY----- 31 | -------------------------------------------------------------------------------- /app/controllers/tags_controller.rb: -------------------------------------------------------------------------------- 1 | class TagsController < ApplicationController 2 | before_action :require_signed_in! 3 | before_action :user_owns_tag?, only: [:edit, :update, :destroy] 4 | 5 | def new 6 | @tag = current_user.tags.new 7 | render :new 8 | end 9 | 10 | def create 11 | @tag = current_user.tags.new(tag_params) 12 | if @tag.save 13 | redirect_to root_url 14 | else 15 | flash.now[:errors] = @tag.errors.full_messages 16 | render :new 17 | end 18 | end 19 | 20 | def index 21 | @tags = current_user.tags 22 | render :index 23 | end 24 | 25 | def show 26 | @tag = Tag.includes(:notes).find(params[:id]) 27 | @notebooks = Notebook.all 28 | 29 | if params[:note_id] && @note = Note.where(id: params[:note_id]).first 30 | else 31 | redirect_to tag_url(@tag, note_id: 32 | @tag.notes.sort_by { |n| n.created_at }.last.id) 33 | return 34 | end 35 | 36 | if params[:query] && params[:query] != "" 37 | @notes = @tag.notes.search_by_title_and_body(params[:query]) 38 | render :show 39 | return 40 | else 41 | @notes = @tag.notes 42 | render :show 43 | end 44 | end 45 | 46 | def edit 47 | @tag = Tag.find(params[:id]) 48 | render :edit 49 | end 50 | 51 | def update 52 | @tag = Tag.find(params[:id]) 53 | if @tag.update_attributes(tag_params) 54 | redirect_to 55 | else 56 | flash.now[:errors] = @tag.errors.full_messages 57 | render :edit 58 | end 59 | end 60 | 61 | def destroy 62 | @tag = Tag.find(params[:id]) 63 | @tag.destroy 64 | redirect_to :back 65 | end 66 | 67 | private 68 | def user_owns_tag? 69 | @tag = Tag.find(params[:id]) 70 | redirect_to :back unless @tag.owner == current_user 71 | end 72 | 73 | def tag_params 74 | params.require(:tag).permit(:name) 75 | end 76 | end -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # PostgreSQL. Versions 8.2 and up are supported. 2 | # 3 | # Install the pg driver: 4 | # gem install pg 5 | # On OS X with Homebrew: 6 | # gem install pg -- --with-pg-config=/usr/local/bin/pg_config 7 | # On OS X with MacPorts: 8 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config 9 | # On Windows: 10 | # gem install pg 11 | # Choose the win32 build. 12 | # Install PostgreSQL and put its /bin directory on your path. 13 | # 14 | # Configure Using Gemfile 15 | # gem 'pg' 16 | # 17 | development: 18 | adapter: postgresql 19 | encoding: unicode 20 | database: BetterNote_development 21 | pool: 5 22 | username: appacademy 23 | 24 | # Connect on a TCP socket. Omitted by default since the client uses a 25 | # domain socket that doesn't need configuration. Windows does not have 26 | # domain sockets, so uncomment these lines. 27 | #host: localhost 28 | 29 | # The TCP port the server listens on. Defaults to 5432. 30 | # If your server runs on a different port number, change accordingly. 31 | #port: 5432 32 | 33 | # Schema search path. The server defaults to $user,public 34 | #schema_search_path: myapp,sharedapp,public 35 | 36 | # Minimum log levels, in increasing order: 37 | # debug5, debug4, debug3, debug2, debug1, 38 | # log, notice, warning, error, fatal, and panic 39 | # Defaults to warning. 40 | #min_messages: notice 41 | 42 | # Warning: The database defined as "test" will be erased and 43 | # re-generated from your development database when you run "rake". 44 | # Do not set this db to the same as development or production. 45 | test: 46 | adapter: postgresql 47 | encoding: unicode 48 | database: BetterNote_test 49 | pool: 5 50 | username: appacademy 51 | 52 | production: 53 | adapter: postgresql 54 | encoding: unicode 55 | database: BetterNote_production 56 | pool: 5 57 | username: appacademy -------------------------------------------------------------------------------- /app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < ApplicationController 2 | before_action :require_signed_out!, only: [:new, :create] 3 | before_action :authorized?, only: [:show] 4 | 5 | def create 6 | @user = User.new(user_params) 7 | if @user.save 8 | log_in(@user) 9 | @user.notebooks.create({ 10 | name: "First notebook!" 11 | }) 12 | @user.notes.create({ 13 | title: "Welcome to BetterNote!", 14 | body: "", 15 | notebook_id: @user.notebooks.first.id 16 | }) 17 | redirect_to root_url 18 | else 19 | flash.now[:errors] = @user.errors.full_messages 20 | render :new, layout: "public" 21 | end 22 | end 23 | 24 | def new 25 | @user = User.new 26 | render :new, layout: "public" 27 | end 28 | 29 | def index 30 | @users = User.all 31 | @users.delete_at(@users.find_index(current_user)) 32 | render :index 33 | end 34 | 35 | def show 36 | @user = User.includes(:notes).find(params[:id]) 37 | @notebooks = Notebook.all 38 | 39 | if params[:note_id] && @note = Note.where(id: params[:note_id]).first 40 | @note = Note.find(params[:note_id]) 41 | else 42 | redirect_to user_url(@user, note_id: 43 | @user.notes.sort_by { |n| n.created_at }.last.id) 44 | return 45 | end 46 | 47 | if params[:query] && params[:query] != "" 48 | @notes = @user.notes.search_by_title_and_body(params[:query]) 49 | render :show 50 | return 51 | else 52 | @notes = @user.notes 53 | render :show 54 | end 55 | end 56 | 57 | private 58 | def authorized? 59 | user = User.find(params[:id]) 60 | unless (current_user == user || user.find_friendship(current_user)) 61 | redirect_to root_url 62 | end 63 | end 64 | 65 | def user_params 66 | params.require(:user).permit(:username, :email, :password) 67 | end 68 | end -------------------------------------------------------------------------------- /app/views/static_pages/_sidebar.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/javascripts/models/note.js: -------------------------------------------------------------------------------- 1 | BetterNote.Models.Note = Backbone.Model.extend({ 2 | urlRoot: "/api/notes", 3 | 4 | parse: function(jsonNote) { 5 | var that = this; 6 | this.friendNote = (jsonNote.note_type === "friend") 7 | 8 | if (jsonNote.author) { 9 | this.author = new BetterNote.Models.User(jsonNote.author); 10 | delete jsonNote.author; 11 | } else { 12 | this.author = BetterNote.currentUser; 13 | } 14 | 15 | if (jsonNote.notebook_id && (!that.friendNote)) { 16 | var notebook = BetterNote.notebooks.get(jsonNote.notebook_id); 17 | this.notebook = notebook; 18 | notebook.notes.add(this); 19 | delete jsonNote.notebook; 20 | } 21 | 22 | if (jsonNote.comments) { 23 | this.comments = new BetterNote.Collections.Comments(jsonNote.comments, { parse: true }); 24 | delete jsonNote.comments; 25 | } else { 26 | this.comments = new BetterNote.Collections.Comments(); 27 | } 28 | 29 | if (jsonNote.note_tags) { 30 | this.noteTags = new BetterNote.Collections.NoteTags(jsonNote.note_tags); 31 | if (!that.friendNote) { 32 | this.noteTags.each(function(noteTag) { 33 | var tag = BetterNote.tags.get(noteTag.get("tag_id")); 34 | tag.notes.add(that); 35 | }) 36 | } 37 | delete jsonNote.note_tags; 38 | } else { 39 | this.noteTags = new BetterNote.Collections.NoteTags(); 40 | } 41 | 42 | if (jsonNote.likes) { 43 | this.likes = new BetterNote.Collections.Likes(jsonNote.likes, { parse: true }); 44 | delete jsonNote.likes; 45 | } else { 46 | this.likes = new BetterNote.Collections.Likes(); 47 | } 48 | 49 | if (this.friendNote) BetterNote.friendNotes.add(this); 50 | 51 | return jsonNote; 52 | }, 53 | 54 | currentUserLike: function() { 55 | return this.likes.find(function(like) { 56 | return (like.owner.get("id") === BetterNote.currentUser.get("id")); 57 | }); 58 | } 59 | }); -------------------------------------------------------------------------------- /app/assets/stylesheets/notes_list.css: -------------------------------------------------------------------------------- 1 | .notes-list { 2 | width: 360px; 3 | background: white; 4 | border-right: 1px solid #b3b3b3; 5 | position: fixed; 6 | top: 69px; 7 | left: 221px; 8 | bottom: 39px; 9 | overflow: auto; 10 | } 11 | 12 | .notes-list-header { 13 | font-size: 16px; 14 | line-height: 36px; 15 | color: #404040; 16 | border-bottom: 1px solid #b5b5b5; 17 | padding-left: 4px; 18 | text-overflow: ellipsis; 19 | overflow: hidden; 20 | white-space: nowrap; 21 | } 22 | 23 | .notes-list-header > i { 24 | color: #ABABAB; 25 | } 26 | 27 | .note-preview { 28 | background: white; 29 | border-bottom: 1px solid #e6e6e6; 30 | padding: 7px 2px 4px 10px; 31 | color: #5C5C5C; 32 | font-size: 12px; 33 | overflow: hidden; 34 | } 35 | 36 | .note-preview.selected { 37 | background: #E4E4E4; 38 | } 39 | 40 | .note-preview:hover { 41 | background: #d1d4d7; 42 | } 43 | 44 | .note-preview-body { 45 | height: 48px; 46 | padding-top: 1px; 47 | font-size: 11px; 48 | line-height: 16px; 49 | text-overflow: ellipsis; 50 | word-wrap: break-word; 51 | overflow: hidden; 52 | } 53 | 54 | .note-preview-title { 55 | font-weight: bold; 56 | font-size: 12px; 57 | color: black; 58 | padding-bottom: 2px; 59 | text-overflow: ellipsis; 60 | overflow: hidden; 61 | white-space: nowrap; 62 | } 63 | 64 | .note-preview-time { 65 | float: left; 66 | display: inline-block; 67 | padding-right: 6px; 68 | color: #4a8db8; 69 | font-weight: bold; 70 | font-size: 11px; 71 | line-height: 16px; 72 | } 73 | 74 | .notes-list-footer { 75 | position: fixed; 76 | background: white; 77 | width: 360px; 78 | left: 221px; 79 | bottom: 0; 80 | border-top: 1px solid #b3b3b3; 81 | border-right: 1px solid #b3b3b3; 82 | } 83 | 84 | .notes-list-footer-left { 85 | padding: 12px; 86 | float: left; 87 | font-size: 12px; 88 | font-weight: bold; 89 | cursor: pointer; 90 | } 91 | 92 | .notes-list-footer-right { 93 | float: right; 94 | font-size: 12px; 95 | padding: 12px; 96 | } -------------------------------------------------------------------------------- /app/controllers/notebooks_controller.rb: -------------------------------------------------------------------------------- 1 | class NotebooksController < ApplicationController 2 | before_action :require_signed_in! 3 | before_action :user_owns_notebook?, only: [:edit, :update, :destroy, :show] 4 | 5 | def new 6 | @notebook = current_user.notebooks.new 7 | render :new 8 | end 9 | 10 | def create 11 | @notebook = current_user.notebooks.new(notebook_params) 12 | if @notebook.save 13 | redirect_to root_url 14 | else 15 | flash.now[:errors] = @notebook.errors.full_messages 16 | render :new 17 | end 18 | end 19 | 20 | def index 21 | @notebooks = current_user.notebooks 22 | render :index 23 | end 24 | 25 | def show 26 | @notebooks = Notebook.all 27 | @notebook = Notebook.includes(:notes).find(params[:id]) 28 | 29 | if params[:note_id] && @note = Note.find_by(id: params[:note_id]) 30 | else 31 | redirect_to notebook_url(@notebook, note_id: 32 | @notebook.notes.sort_by { |n| n.created_at }.last.id) 33 | return 34 | end 35 | 36 | if params[:query] && params[:query] != "" 37 | @notes = @notebook.notes.search_by_title_and_body(params[:query]) 38 | render :show 39 | return 40 | else 41 | @notes = @notebook.notes 42 | render :show 43 | end 44 | end 45 | 46 | def edit 47 | @notebook = Notebook.find(params[:id]) 48 | render :edit 49 | end 50 | 51 | def update 52 | @notebook = Notebook.find(params[:id]) 53 | if @notebook.update_attributes(notebook_params) 54 | redirect_to 55 | else 56 | flash.now[:errors] = @notebook.errors.full_messages 57 | render :edit 58 | end 59 | end 60 | 61 | def destroy 62 | @notebook = Notebook.find(params[:id]) 63 | @notebook.destroy 64 | redirect_to root_url 65 | end 66 | 67 | private 68 | def notebook_params 69 | params.require(:notebook).permit(:name) 70 | end 71 | 72 | def user_owns_notebook? 73 | @notebook = Notebook.find(params[:id]) 74 | redirect_to root_url unless @notebook.owner == current_user 75 | end 76 | end -------------------------------------------------------------------------------- /app/assets/javascripts/views/static_pages/search_bar.js: -------------------------------------------------------------------------------- 1 | BetterNote.Views.SearchBar = Backbone.View.extend({ 2 | initialize: function() { 3 | this.listenTo(BetterNote.filter, "change", this.render); 4 | }, 5 | 6 | tagName: "header", 7 | className: "search group", 8 | template: JST['static_pages/search_bar'], 9 | 10 | events: { 11 | "click button.create-new-note": "createNote", 12 | "click .submit": "applySearchFilter", 13 | "click .clear-search": "clearSearch", 14 | "click .delete-tag": "deleteTag" 15 | }, 16 | 17 | render: function() { 18 | if (BetterNote.filter.get("tag") && BetterNote.featuredNotes.id != "friend") { 19 | var tag = BetterNote.filter.get("tag") 20 | } else { 21 | var tag = false; 22 | } 23 | 24 | var renderedContent = this.template({ 25 | tag: tag 26 | }); 27 | this.$el.html(renderedContent); 28 | return this; 29 | }, 30 | 31 | createNote: function(event) { 32 | event.preventDefault(); 33 | 34 | var note = new BetterNote.Models.Note({ 35 | title: "Untitled", 36 | notebook_id: BetterNote.featuredNotebook.get("id") 37 | }); 38 | BetterNote.featuredNote = note; 39 | 40 | var that = this; 41 | note.save({}, { 42 | success: function(note) { 43 | that.render(); 44 | BetterNote.notes.add(note); 45 | BetterNote.featuredNotebook.notes.add(note); 46 | BetterNote.router.navigate("#/notes/" + note.get("id")); 47 | } 48 | }); 49 | }, 50 | 51 | applySearchFilter: function(event) { 52 | event.preventDefault(); 53 | 54 | var searchText = $(event.currentTarget).closest("form").serializeJSON().query; 55 | BetterNote.filter.set({ 56 | text: searchText 57 | }); 58 | }, 59 | 60 | clearSearch: function(event) { 61 | event.preventDefault(); 62 | BetterNote.filter.set({ 63 | text: "", 64 | tag: null 65 | }); 66 | }, 67 | 68 | deleteTag: function(event) { 69 | event.preventDefault(); 70 | 71 | BetterNote.filter.set({ 72 | tag: null 73 | }) 74 | } 75 | }); -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css.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, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, 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 top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require_self 12 | *= require font-awesome 13 | *= require navbar 14 | *= require sidebar 15 | *= require notes_list 16 | *= require note_show 17 | *= require modal 18 | */ 19 | 20 | @font-face { 21 | font-family: "Caecilia LT Std"; 22 | src: asset-url("CaeciliaLTStd-Roman.ttf") format("truetype"); 23 | } 24 | 25 | html, body, header, section, div, span, h1, h2, h3, a, form, ul, li, i { 26 | padding: 0; 27 | margin: 0; 28 | border: 0; 29 | font: inherit; 30 | vertical-align: baseline; 31 | } 32 | 33 | input[type="text"], 34 | input[type="textarea"], 35 | input[type="submit"] { 36 | -webkit-appearance: none; 37 | box-sizing: content-box; 38 | display: inline-block; 39 | margin: 0; 40 | padding: 0; 41 | border: 0; 42 | outline: 0; 43 | font: inherit; 44 | vertical-align: baseline; 45 | width: auto; 46 | background: transparent; 47 | color: inherit; 48 | } 49 | 50 | input[type="submit"] { 51 | display: inline; 52 | cursor: pointer; 53 | } 54 | 55 | .hidden { 56 | display: none; 57 | } 58 | 59 | button:focus { 60 | outline: none; 61 | } 62 | 63 | .group:after { 64 | content: ""; 65 | clear: both; 66 | display: block; 67 | } 68 | 69 | body { 70 | font-family: sans-serif; 71 | background: #F0F2F4; 72 | } 73 | 74 | a { 75 | text-decoration: none; 76 | } 77 | 78 | h1 { 79 | font-size: 40px; 80 | font-weight: bold; 81 | } 82 | 83 | ul { 84 | list-style: none; 85 | } 86 | 87 | h2 { 88 | font-size: 30px; 89 | font-weight: bold; 90 | } 91 | 92 | img.corgi-pic { 93 | width: 100%; 94 | height: auto; 95 | } -------------------------------------------------------------------------------- /app/assets/templates/notes/show.jst.ejs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 27 |
    28 | 34 |
    35 |
    36 |
    37 | 45 |
    46 |
    47 |
    48 | DELETE 49 | 50 |
    51 |
    52 |
    53 |
    54 |
    55 | 57 |
    58 |
    59 | 60 |
    61 |
    -------------------------------------------------------------------------------- /app/controllers/notes_controller.rb: -------------------------------------------------------------------------------- 1 | class NotesController < ApplicationController 2 | before_action :require_signed_in! 3 | before_action :user_owns_note?, only: [:edit, :update, :destroy] 4 | before_action :authorized?, only: [:show] 5 | 6 | def create 7 | @note = current_user.notes.new(note_params) 8 | if @note.save 9 | redirect_to notebook_url(@note.notebook, note_id: @note.id) 10 | else 11 | flash.now[:errors] = @note.errors.full_messages 12 | render :new 13 | end 14 | end 15 | 16 | def index 17 | @notebooks = Notebook.all 18 | 19 | if params[:note_id] && @note = Note.where(id: params[:note_id]).first 20 | @note = Note.find(params[:note_id]) 21 | else 22 | redirect_to notes_url(note_id: 23 | Note.all.sort_by { |n| n.created_at }.last.id) 24 | return 25 | end 26 | 27 | if params[:query] && params[:query] != "" 28 | @notes = current_user.notes.search_by_title_and_body(params[:query]) 29 | render :index 30 | return 31 | else 32 | @notes = current_user.notes 33 | render :index 34 | end 35 | end 36 | 37 | def update 38 | @note = Note.find(params[:id]) 39 | 40 | @note.assign_attributes(note_params) 41 | @note.tag_ids = note_tag_params[:tag_ids] 42 | 43 | if @note.save 44 | redirect_to request.env["HTTP_REFERER"].gsub("&edit=true", "") 45 | else 46 | flash[:errors] = @note.errors.full_messages 47 | redirect_to :back 48 | end 49 | end 50 | 51 | def show 52 | @note = Note.find(params[:id]) 53 | render :show 54 | end 55 | 56 | def destroy 57 | @note = Note.find(params[:id]) 58 | @note.destroy 59 | redirect_to :back 60 | end 61 | 62 | private 63 | def user_owns_note? 64 | @note = Note.find(params[:id]) 65 | redirect_to root_url unless @note.author == current_user 66 | end 67 | 68 | def authorized? 69 | author = Note.find(params[:id]).author 70 | unless (current_user == author || user.find_friendship(current_user)) 71 | redirect_to root_url 72 | end 73 | end 74 | 75 | def note_params 76 | params.require(:note).permit(:title, :body, :notebook_id) 77 | end 78 | 79 | def note_tag_params 80 | params.require(:note_tags).permit(tag_ids: []) 81 | end 82 | end -------------------------------------------------------------------------------- /app/assets/javascripts/routers/notes_router.js: -------------------------------------------------------------------------------- 1 | BetterNote.Routers.Notes = Backbone.Router.extend({ 2 | initialize: function($notesListEl, $noteShowEl) { 3 | this.$notesListEl = $notesListEl; 4 | this.$noteShowEl = $noteShowEl; 5 | }, 6 | 7 | routes: { 8 | '': "notesIndex", 9 | 'notes/:id': 'showNote', 10 | 'notes': "notesIndex", 11 | 'notebooks/:id': 'showNotebook', 12 | 'users/:id': 'showFriend' 13 | }, 14 | 15 | showNote: function(id) { 16 | if (BetterNote.notes.get(id)) { 17 | var view = new BetterNote.Views.NoteShow({ 18 | model: BetterNote.notes.get(id) 19 | }); 20 | } else { 21 | var view = new BetterNote.Views.FriendNoteShow({ 22 | model: BetterNote.friendNotes.findWhere({ id: parseInt(id) }) 23 | }) 24 | } 25 | 26 | this._swapShowView(view); 27 | }, 28 | 29 | notesIndex: function() { 30 | var listView = new BetterNote.Views.NotesIndex({ 31 | type: "all", 32 | collection: BetterNote.notes, 33 | $noteShowEl: this.$noteShowEl 34 | }); 35 | 36 | BetterNote.featuredNotebook = BetterNote.notebooks.at(0); 37 | 38 | this._swapListView(listView); 39 | }, 40 | 41 | showNotebook: function(id) { 42 | var notebook = BetterNote.notebooks.get(id); 43 | BetterNote.featuredNotebook = notebook; 44 | 45 | var listView = new BetterNote.Views.NotesIndex({ 46 | type: "notebook", 47 | model: notebook, 48 | collection: notebook.notes, 49 | $noteShowEl: this.$noteShowEl 50 | }); 51 | 52 | this._swapListView(listView); 53 | }, 54 | 55 | showFriend: function(id) { 56 | var friend = BetterNote.friends.get(id); 57 | 58 | var listView = new BetterNote.Views.NotesIndex({ 59 | type: "friend", 60 | model: friend, 61 | collection: friend.notes, 62 | $noteShowEl: this.$noteShowEl 63 | }) 64 | 65 | this._swapListView(listView); 66 | }, 67 | 68 | _swapShowView: function(view) { 69 | this._currentShowView && this._currentShowView.remove(); 70 | this._currentShowView = view; 71 | this.$noteShowEl.html(view.render().$el); 72 | }, 73 | 74 | _swapListView: function(view) { 75 | this._currentListView && this._currentListView.remove(); 76 | this._currentListView = view; 77 | this.$notesListEl.html(view.render().$el); 78 | } 79 | }); -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe User do 4 | subject(:user) do 5 | FactoryGirl.build(:user, 6 | username: "Bob", 7 | email: "bob@example.com", 8 | password: "good_password") 9 | end 10 | 11 | it { should validate_presence_of(:username) } 12 | it { should validate_presence_of(:email) } 13 | it { should validate_presence_of(:password_digest) } 14 | it { should ensure_length_of(:password).is_at_least(6) } 15 | 16 | it { should have_many(:notes) } 17 | it { should have_many(:notebooks) } 18 | it { should have_many(:tags) } 19 | it { should have_many(:comments) } 20 | it { should have_many(:likes) } 21 | it { should have_many(:friends) } 22 | it { should have_many(:friendships) } 23 | it { should have_many(:friend_requests) } 24 | it { should have_many(:pending_friends) } 25 | 26 | it "creates a password digest when a password is given" do 27 | expect(user.password_digest).to_not be_nil 28 | end 29 | 30 | it "creates a session token before validation" do 31 | user.valid? 32 | expect(user.session_token).to_not be_nil 33 | end 34 | 35 | describe "#reset_session_token!" do 36 | it "gives the user a new session token" do 37 | user.valid? 38 | old_session_token = user.session_token 39 | user.reset_session_token! 40 | expect(user.session_token).to_not eq(old_session_token) 41 | end 42 | 43 | it "returns the session token" do 44 | expect(user.reset_session_token!).to eq(user.session_token) 45 | end 46 | end 47 | 48 | describe "#is_password?" do 49 | it "matches the correct password" do 50 | expect(user.is_password?("good_password")).to be_true 51 | end 52 | 53 | it "does not match an incorrect password" do 54 | expect(user.is_password?("bad_password")).to be_false 55 | end 56 | end 57 | 58 | describe "find_by_credentials" do 59 | before { user.save! } 60 | 61 | it "returns the user for the correct email/password pair" do 62 | expect(User.find_by_credentials("bob@example.com", 63 | "good_password")).to eq(user) 64 | end 65 | 66 | it "returns nil when the email is incorrect" do 67 | expect(User.find_by_credentials("b@example.com", "")).to be_nil 68 | end 69 | 70 | it "returns nil if the password is incorrect" do 71 | expect(User.find_by_credentials("bob@example.com", 72 | "bad_password")).to be_nil 73 | end 74 | end 75 | end -------------------------------------------------------------------------------- /app/assets/javascripts/views/comments/comments_index.js: -------------------------------------------------------------------------------- 1 | BetterNote.Views.CommentsIndex = Backbone.View.extend({ 2 | 3 | initialize: function(options) { 4 | this.note = options["note"]; 5 | this.listenTo(this.collection, "add change remove", this.render); 6 | }, 7 | 8 | tagName: "section", 9 | className: "comments", 10 | template: JST['comments/index'], 11 | formTemplate: JST['comments/comment_form'], 12 | 13 | events: { 14 | "click .comment-button.new": "addCommentForm", 15 | "click .comment-button.submit": "submitComment", 16 | "click .delete-comment": "deleteComment", 17 | "click .comment-button.cancel": "addButton", 18 | "click .comments-header>.note-show-header-left": "toggleCommentsList" 19 | }, 20 | 21 | render: function() { 22 | var renderedContent = this.template({ 23 | comments: this.collection 24 | }); 25 | 26 | this.$el.html(renderedContent); 27 | return this; 28 | }, 29 | 30 | addButton: function(event) { 31 | if (event) event.preventDefault(); 32 | this.render(); 33 | }, 34 | 35 | addCommentForm: function(event) { 36 | event.preventDefault(); 37 | 38 | var renderedContent = this.formTemplate({ 39 | note: this.note 40 | }); 41 | 42 | $(".comment-button").remove(); 43 | this.$el.append(renderedContent); 44 | }, 45 | 46 | submitComment: function(event) { 47 | event.preventDefault(); 48 | 49 | var attrs = $(event.target.form).serializeJSON(); 50 | var comment = new BetterNote.Models.Comment(attrs); 51 | comment.author = BetterNote.currentUser; 52 | 53 | this.collection.create(comment, {}); 54 | }, 55 | 56 | deleteComment: function(event) { 57 | event.preventDefault(); 58 | 59 | var commentId = $(event.currentTarget).find("button").attr("data-comment-id"); 60 | var comment = this.collection.get(commentId); 61 | comment.destroy(); 62 | }, 63 | 64 | toggleCommentsList: function(event) { 65 | event.preventDefault(); 66 | var $commentsContent = $(event.currentTarget).closest("section.comments") 67 | .find(".comments-content"); 68 | var $icon = $(event.currentTarget).find("i:first-of-type"); 69 | 70 | if ($commentsContent.hasClass("hidden")) { 71 | $commentsContent.removeClass("hidden"); 72 | $icon.removeClass('fa-caret-right').addClass('fa-caret-down') 73 | } else { 74 | $commentsContent.addClass("hidden") 75 | $icon.removeClass('fa-caret-down').addClass('fa-caret-right') 76 | } 77 | } 78 | }); -------------------------------------------------------------------------------- /app/assets/stylesheets/modal.css: -------------------------------------------------------------------------------- 1 | .modal-content { 2 | position: absolute; 3 | left: 50%; 4 | top: 50%; 5 | 6 | margin-left: -218px; 7 | margin-top: -200px; 8 | 9 | width: 400px; 10 | min-height: 100px; 11 | padding: 18px; 12 | z-index: 1000; 13 | 14 | background: white; 15 | border-radius: 3px; 16 | -webkit-box-shadow: 0 0 5px 5px rgba(0, 0, 0, 0.25); 17 | } 18 | 19 | .modal-screen { 20 | position: fixed; 21 | top: 0; 22 | left: 0; 23 | 24 | width: 100%; 25 | height: 100%; 26 | 27 | background: rgba(0, 0, 0, 0.5); 28 | z-index: 999; 29 | } 30 | 31 | .modal-x { 32 | display: block; 33 | position: absolute; 34 | right: 10px; 35 | top: 3px; 36 | font-size: 20px; 37 | font-weight: bold; 38 | cursor: pointer; 39 | 40 | color: #999; 41 | } 42 | 43 | .modal-x:hover { 44 | color: #000; 45 | } 46 | 47 | .modal-content h3 { 48 | font-size: 20; 49 | font-family: "Caecilia LT Std"; 50 | color: rgb(59, 59, 59); 51 | border-bottom: 1px solid #b3b3b3; 52 | padding-bottom: 8px; 53 | margin-bottom: 10px; 54 | } 55 | 56 | .modal-content label { 57 | display: block; 58 | font-size: 14px; 59 | } 60 | 61 | .modal-content .input { 62 | margin: 15px 0 25px 0; 63 | } 64 | 65 | .modal-content .input > label { 66 | display: block; 67 | color: #606060; 68 | margin-bottom: 4px; 69 | } 70 | 71 | .modal-content .input > input { 72 | border: 2px solid rgb(202, 202, 202); 73 | padding: 6px; 74 | width: 382px; 75 | } 76 | 77 | .modal-content .input > input:focus { 78 | border-color: #50AAF2; 79 | border-radius: 3px; 80 | border-opacity: 0.7; 81 | } 82 | 83 | .modal-content .form-right { 84 | float: right; 85 | } 86 | 87 | .form-right > div { 88 | float: left; 89 | display: block; 90 | } 91 | 92 | .form-right > .submit > input, 93 | .form-right > .cancel { 94 | background: #69ad31; 95 | padding: 4px 16px; 96 | border: 1px solid #b3b3b3; 97 | border-radius: 2px; 98 | color: white; 99 | font-size: 14px; 100 | width: 60px; 101 | text-align: center; 102 | } 103 | 104 | .modal-content .cancel { 105 | background: #E6E6E6; 106 | margin-right: 10px; 107 | color: #000; 108 | cursor: pointer; 109 | } 110 | 111 | .modal-content .submit > input:hover { 112 | background: #56A225; 113 | } 114 | 115 | .modal-content .cancel:hover { 116 | background: #CCC; 117 | } 118 | 119 | .modal-content .cancel:active { 120 | background: #B7B7B7; 121 | } 122 | 123 | .modal-content .submit > input:active { 124 | background: #488920; 125 | } -------------------------------------------------------------------------------- /app/views/notes/_show_header.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    5 | <%= note.notebook.name %> 6 |   7 |
    8 |
      9 | <% current_user.notebooks.each do |notebook| %> 10 |
    • 11 | <%= notebook.name %> 12 |
    • 13 | <% end %> 14 |
    15 |
    16 | 26 |
    27 |
    28 |
    29 | INFO  30 | 31 |
    32 | <%= render partial: "notes/info", locals: { note: note }%> 33 |
    34 |
    35 |
    36 |
    37 | 45 |
    46 |
    47 | <% if note.likers.include?(current_user) %> 48 |
    50 | <%= auth_token %> 51 | 52 | 56 |
    57 | <% else %> 58 |
    59 | <%= auth_token %> 60 | 64 |
    65 | <% end %> 66 | <% if current_user == note.author %> 67 | " class="note-show-button"> 68 | EDIT 69 | 70 | 71 |
    72 | <%= auth_token %> 73 | 74 | 78 |
    79 | <% end %> 80 |
    81 |
    -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | attr_reader :password 3 | validates :username, :email, :password_digest, presence: true 4 | validates :username, :email, uniqueness: true 5 | validates :password, length: { minimum: 6, allow_nil: true } 6 | validates :session_token, presence: true 7 | before_validation :ensure_session_token 8 | 9 | has_many( 10 | :notes, 11 | foreign_key: :author_id, 12 | inverse_of: :author, 13 | dependent: :destroy 14 | ) 15 | 16 | has_many( 17 | :notebooks, 18 | foreign_key: :owner_id, 19 | inverse_of: :owner, 20 | dependent: :destroy 21 | ) 22 | 23 | has_many( 24 | :tags, 25 | foreign_key: :owner_id, 26 | inverse_of: :owner, 27 | dependent: :destroy 28 | ) 29 | 30 | has_many( 31 | :comments, 32 | foreign_key: :author_id, 33 | inverse_of: :author, 34 | dependent: :destroy 35 | ) 36 | 37 | has_many( 38 | :likes, 39 | foreign_key: :owner_id, 40 | inverse_of: :owner, 41 | dependent: :destroy 42 | ) 43 | 44 | has_many( 45 | :friendships, 46 | foreign_key: :out_friend_id, 47 | inverse_of: :out_friend, 48 | dependent: :destroy 49 | ) 50 | 51 | has_many :friends, through: :friendships, source: :in_friend 52 | 53 | has_many( 54 | :friend_requests, 55 | foreign_key: :out_friend_id, 56 | inverse_of: :out_friend, 57 | dependent: :destroy 58 | ) 59 | 60 | has_many :notifications, inverse_of: :user, dependent: :destroy 61 | 62 | has_many :pending_friends, through: :friend_requests, source: :in_friend 63 | 64 | def self.generate_random_token 65 | SecureRandom.urlsafe_base64 66 | end 67 | 68 | def self.find_by_credentials(email, password) 69 | user = User.find_by_email(email) 70 | return nil if user.nil? 71 | user.is_password?(password) ? user : nil 72 | end 73 | 74 | def password=(password) 75 | @password = password 76 | self.password_digest = BCrypt::Password.create(password) if password 77 | end 78 | 79 | def is_password?(password) 80 | BCrypt::Password.new(self.password_digest).is_password?(password) 81 | end 82 | 83 | def reset_session_token! 84 | self.session_token = self.class.generate_random_token 85 | self.save! 86 | self.session_token 87 | end 88 | 89 | def friend?(other_user) 90 | self.friends.include?(other_user) 91 | end 92 | 93 | def pending_friend?(other_user) 94 | self.pending_friends.include?(other_user) 95 | end 96 | 97 | def find_friend_request(other_user) 98 | self.friend_requests.where({in_friend: other_user}).first 99 | end 100 | 101 | def find_friendship(other_user) 102 | self.friendships.where({in_friend: other_user}).first 103 | end 104 | 105 | private 106 | def ensure_session_token 107 | self.session_token ||= self.class.generate_random_token 108 | end 109 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BetterNote: an Evernote Clone 2 | 3 | [BetterNote](http://www.betternote.us) is a clone of [EverNote](http://www.evernote.com), the popular note-taking app. It was built by [Sam Sweeney](http://sweeneysam.com) in Spring 2014. You can view it at http://www.betternote.us. 4 | 5 | ## Features 6 | 7 | BetterNote has most of the core features of Evernote with a few additional features sprinkled in. Users can: 8 | 9 | * Create notes, notebooks, and tags 10 | * Organize their notes into notebooks 11 | * Tag their notes with multiple tags 12 | * Search their notes by tag or search term 13 | * View their friends' notes 14 | * Comment on their friends' notes and respond to comments on their notes 15 | * Like notes 16 | * View one Easter Egg (try to find it!) 17 | 18 | ## Technologies 19 | 20 | BetterNote was built using the following technologies: 21 | 22 | * **Back-End**: BetterNote has a Rails 4 back-end with a RESTful API. 23 | * **Front-End**: Betternote has a Backbone.js front-end to provide a smoother user experience and minimize requests to the server. 24 | * **Styling**: BetterNote was styled with custom CSS without the use of any third-party themes or tools. 25 | 26 | ## Todos 27 | 28 | Additional features to implement include: 29 | 30 | * Allow users to send and approve friend requests 31 | * Use SendGrid to authenticate email address and activate upon sign-up 32 | * Allow users to post notes via email or text message 33 | * Add jQuery UI drag/drop functionality for changing a note's notebook 34 | * Allow users to filter notes by multiple tags at once 35 | * Use TinyMCE to allow rich text editing of notes 36 | 37 | ## Frequently Asked Questions 38 | 39 | > Why bother copying a website that already exists? Isn't Evernote good enough? 40 | 41 | Great question! My main goal in creating BetterNote was to hone (and demonstrate) my Rails and Backbone skills and to become a better developer in general. BetterNote is meant as an homage to Evernote, not a substitue. 42 | 43 | > What are the main differences between Evernote and BetterNote? 44 | 45 | Well, their names for starters. BetterNote also has a number of social aspects (liking notes, commenting on notes, viewing friends' notes) that (as far as I know) aren't in Evernote. There just might be a few features in Evernote that are missing from BetterNote as well. 46 | 47 | > What was the most challenging part about building BetterNote? 48 | 49 | Hard to say. Styling BetterNote to look like Evernote definitely took some time, as did setting up the myriad Backbone views (they're everywhere!) so that everything on the page is synced up. 50 | 51 | > Wow, I'm blown away! How can I be a part of BetterNote's next fundraising round? 52 | 53 | Thanks! Unfortunately, we just closed our most recent round, in which we sold 10% of BetterNote for a Diet Coke and bag of SunChips (they were delicious!). We're unlikely to raise additional funding prior to our IPO (currently scheduled for the Fall of 2062), so you'll just have to wait until then. 54 | 55 | ## Praise for BetterNote 56 | 57 | > "Nice app. Love, Mom" 58 | 59 | \- My mom -------------------------------------------------------------------------------- /app/assets/javascripts/views/notes/note_show.js: -------------------------------------------------------------------------------- 1 | BetterNote.Views.NoteShow = Backbone.View.extend({ 2 | initialize: function() { 3 | this.listenTo(this.model, "change", this.render); 4 | this.listenTo(this.model, "destroy", this.remove); 5 | }, 6 | 7 | template: JST['notes/show'], 8 | 9 | events: { 10 | "click .dropdown-parent": "showDropdown", 11 | "click .options-dropdown, .dropdown-parent": "stopPropagation", 12 | "click form.update-notebook": "updateNotebook", 13 | "click .delete-note": "showModal", 14 | "blur form.note-title, form.note-body": "updateNote" 15 | }, 16 | 17 | render: function() { 18 | BetterNote.featuredNote = this.model; 19 | var renderedContent = this.template({ 20 | note: this.model, 21 | notebooks: BetterNote.notebooks 22 | }); 23 | 24 | this.$el.html(renderedContent); 25 | 26 | var infoView = new BetterNote.Views.NoteInfo({ 27 | model: this.model 28 | }) 29 | 30 | var commentView = new BetterNote.Views.CommentsIndex({ 31 | collection: this.model.comments, 32 | note: this.model 33 | }); 34 | 35 | var likeView = new BetterNote.Views.LikeShow({ 36 | collection: this.model.likes, 37 | note: this.model 38 | }); 39 | 40 | var noteTagView = new BetterNote.Views.NoteTagsIndex({ 41 | collection: this.model.noteTags, 42 | note: this.model 43 | }); 44 | 45 | this.$el.append(commentView.render().$el); 46 | this.$el.find(".note-show-header-left.top").append(noteTagView.render().$el); 47 | this.$el.find(".note-show-header-right.top").append(infoView.render().$el); 48 | this.$el.find(".note-show-header-right.bottom").prepend(likeView.render().$el); 49 | 50 | return this; 51 | }, 52 | 53 | showDropdown: function(event) { 54 | event.preventDefault(); 55 | this.hideDropdowns(); 56 | 57 | var $dropdown = $(event.currentTarget).find(".options-dropdown"); 58 | $dropdown.removeClass("hidden"); 59 | }, 60 | 61 | hideDropdowns: function(event) { 62 | $(".options-dropdown").not("hidden").addClass("hidden"); 63 | }, 64 | 65 | stopPropagation: function(event) { 66 | event.stopPropagation(); 67 | }, 68 | 69 | updateNotebook: function(event) { 70 | event.preventDefault(); 71 | 72 | var that = this; 73 | var attrs = $(event.currentTarget).serializeJSON(); 74 | 75 | var oldNotebook = that.model.notebook; 76 | oldNotebook.notes.remove(this.model); 77 | 78 | this.model.save(attrs, { 79 | wait: true 80 | }) 81 | }, 82 | 83 | updateNote: function(event) { 84 | var attrs = $(event.currentTarget).serializeJSON(); 85 | this.model.save(attrs, { 86 | wait: true 87 | }); 88 | }, 89 | 90 | showModal: function(event) { 91 | event.preventDefault(); 92 | 93 | var $modal = $("#modal"); 94 | var $modalContent = $(".modal-content") 95 | var view = new BetterNote.Views.NoteDelete({ 96 | model: this.model, 97 | $modal: $modal, 98 | noteShowView: this 99 | }); 100 | 101 | $modal.removeClass("hidden"); 102 | $modalContent.html(view.render().$el); 103 | } 104 | }); -------------------------------------------------------------------------------- /app/assets/templates/notebooks/index.jst.ejs: -------------------------------------------------------------------------------- 1 | 55 | 56 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | BetterNote::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 thread 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 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. 20 | # config.action_dispatch.rack_cache = true 21 | 22 | # Disable Rails's static asset server (Apache or nginx will already do this). 23 | config.serve_static_assets = true 24 | 25 | # Compress JavaScripts and CSS. 26 | config.assets.js_compressor = :uglifier 27 | # config.assets.css_compressor = :sass 28 | 29 | # Do not fallback to assets pipeline if a precompiled asset is missed. 30 | config.assets.compile = true 31 | 32 | # Generate digests for assets URLs. 33 | config.assets.digest = true 34 | 35 | # Version of your assets, change this if you want to expire all your assets. 36 | config.assets.version = '1.0' 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 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 43 | # config.force_ssl = true 44 | 45 | # Set to :debug to see everything in the log. 46 | config.log_level = :info 47 | 48 | # Prepend all log lines with the following tags. 49 | # config.log_tags = [ :subdomain, :uuid ] 50 | 51 | # Use a different logger for distributed setups. 52 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 53 | 54 | # Use a different cache store in production. 55 | # config.cache_store = :mem_cache_store 56 | 57 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 58 | # config.action_controller.asset_host = "http://assets.example.com" 59 | 60 | # Precompile additional assets. 61 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 62 | config.assets.precompile += %w( public.css ) 63 | 64 | # Ignore bad email addresses and do not raise email delivery errors. 65 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 66 | # config.action_mailer.raise_delivery_errors = false 67 | 68 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 69 | # the I18n.default_locale when a translation can not be found). 70 | config.i18n.fallbacks = true 71 | 72 | # Send deprecation notices to registered listeners. 73 | config.active_support.deprecation = :notify 74 | 75 | # Disable automatic flushing of the log to improve performance. 76 | # config.autoflush_log = false 77 | 78 | # Use default logging formatter so that PID and timestamp are not suppressed. 79 | config.log_formatter = ::Logger::Formatter.new 80 | end 81 | -------------------------------------------------------------------------------- /app/assets/stylesheets/sidebar.css: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | width: 220px; 3 | border-right: 1px solid #b3b3b3; 4 | background: #E4E8EA; 5 | float: left; 6 | position: fixed; 7 | top: 69px; 8 | left: 0; 9 | bottom: 0; 10 | overflow: auto; 11 | } 12 | 13 | .sidebar-header { 14 | font-size: 11px; 15 | line-height: 17px; 16 | font-weight: bold; 17 | border-bottom: 1px solid #ced8df; 18 | margin-bottom: 5px; 19 | padding: 5px; 20 | color: rgb(85, 94, 100); 21 | background: -webkit-linear-gradient(bottom, #dfe5e7 0, #e3e7e9 100%); 22 | position: relative; 23 | } 24 | 25 | .sidebar-header-left { 26 | float: left; 27 | width: 175px; 28 | } 29 | 30 | .sidebar-header-left, 31 | .sidebar-header-right > i, 32 | .sidebar-entry-right > i { 33 | cursor: pointer; 34 | } 35 | 36 | .sidebar-header-right { 37 | float: right; 38 | font-size: 13px; 39 | font-weight: normal; 40 | margin-right: 4px; 41 | } 42 | 43 | .sidebar-list { 44 | padding-top: 8px; 45 | padding-bottom: 16px; 46 | border-bottom: 1px solid #ced8df; 47 | } 48 | 49 | .sidebar-list > li.selected { 50 | background: #c2c9ce; 51 | } 52 | 53 | article.sidebar-item li:hover, 54 | .options-dropdown > li:hover { 55 | background: #d1d4d7; 56 | } 57 | 58 | .sidebar-entry-left { 59 | float: left; 60 | position: relative; 61 | } 62 | 63 | .sidebar-entry-left > a, 64 | .sidebar-entry-left.tag, 65 | .sidebar-entry-left.friend { 66 | font-size: 11px; 67 | line-height: 25px; 68 | float: left; 69 | color: #555e64; 70 | padding-left: 8px; 71 | width: 175px; 72 | height: 25px; 73 | } 74 | 75 | .sidebar-entry-left.tag:hover, 76 | .sidebar-entry-left.friend:hover { 77 | cursor: pointer; 78 | } 79 | 80 | a > .notebook-name, 81 | a > .tag-name { 82 | float: left; 83 | max-width: 150px; 84 | text-overflow: ellipsis; 85 | overflow: hidden; 86 | white-space: nowrap; 87 | } 88 | 89 | a > .notes-count { 90 | float: left; 91 | font-size: 11px; 92 | line-height: 25px; 93 | color: #555e64; 94 | } 95 | 96 | .sidebar-entry-right { 97 | float: right; 98 | padding-right: 8px; 99 | line-height: 25px; 100 | vertical-align: middle; 101 | } 102 | 103 | .sidebar-entry-right > i { 104 | color: #7E7E7E; 105 | padding-bottom: 3px; 106 | font-size: 13px; 107 | display: none; 108 | } 109 | 110 | .sidebar-list > li:hover i { 111 | display: inline-block; 112 | } 113 | 114 | ul.options-dropdown { 115 | position: absolute; 116 | top: 20px; 117 | left: 80px; 118 | z-index: 100; 119 | background: #f4f6f7; 120 | padding: 7px 0; 121 | border: 1px solid rgba(153, 158, 161, 0.9); 122 | border-radius: 5px; 123 | width: 120px; 124 | } 125 | 126 | .options-dropdown > li > a, 127 | .options-dropdown > li > div, 128 | .options-dropdown input, 129 | .options-dropdown > .sort-option { 130 | font-size: 11px; 131 | line-height: 17px; 132 | color: #585858; 133 | padding: 2px 10px; 134 | display: block; 135 | } 136 | 137 | .options-dropdown > li:hover { 138 | cursor: pointer; 139 | } 140 | 141 | .notes-list-footer-left { 142 | position: relative; 143 | } 144 | 145 | .notes-list-footer-left > .options-dropdown { 146 | top: -155px; 147 | bottom: 30px; 148 | left: 0; 149 | right: 0; 150 | width: 160px; 151 | font-weight: normal; 152 | } 153 | 154 | .options-dropdown-header { 155 | margin: 0 10px 2px 10px; 156 | padding: 2px 0; 157 | border-bottom: 1px solid rgba(153, 158, 161, 0.9); 158 | font-size: 11px; 159 | color: rgb(122, 125, 129); 160 | } 161 | 162 | li.options-dropdown-header:hover{ 163 | background: transparent; 164 | } -------------------------------------------------------------------------------- /app/assets/templates/static_pages/sidebar.jst.ejs: -------------------------------------------------------------------------------- 1 | 32 | 33 | 93 | 94 | -------------------------------------------------------------------------------- /app/assets/stylesheets/public.css: -------------------------------------------------------------------------------- 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, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, 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 top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require_self 12 | *= require font-awesome 13 | */ 14 | 15 | @font-face { 16 | font-family: "Caecilia LT Std"; 17 | src: url(/assets/CaeciliaLTStd-Roman.ttf) format("truetype"); 18 | } 19 | 20 | html, body, header, section, div, h1, h2, h3, a, form, ul, li { 21 | padding: 0; 22 | margin: 0; 23 | border: 0; 24 | font: inherit; 25 | vertical-align: baseline; 26 | } 27 | 28 | input[type="text"], 29 | input[type="email"], 30 | input[type="password"], 31 | input[type="submit"] { 32 | -webkit-appearance: none; 33 | box-sizing: content-box; 34 | display: inline-block; 35 | margin: 0; 36 | padding: 0; 37 | border: 0; 38 | outline: 0; 39 | font: inherit; 40 | vertical-align: baseline; 41 | width: auto; 42 | background: transparent; 43 | color: inherit; 44 | } 45 | 46 | input[type="submit"] { 47 | display: inline; 48 | cursor: pointer; 49 | } 50 | 51 | a { 52 | text-decoration: none; 53 | color: #00e; 54 | } 55 | 56 | a:visited { 57 | color: #551a8b; 58 | } 59 | 60 | .group:after { 61 | content: ""; 62 | clear: both; 63 | display: block; 64 | } 65 | 66 | body { 67 | font-family: helvetica, arial, sans-serif; 68 | background: #D6DDE2; 69 | } 70 | 71 | header { 72 | background: url("/assets/header-background-green.png"); 73 | border-bottom: 1px solid grey; 74 | font-size: 14px; 75 | } 76 | 77 | header > .nav-left { 78 | float: left; 79 | display: block; 80 | padding: 3px 5px 0 10px; 81 | } 82 | 83 | header > .nav-left > a { 84 | color: white; 85 | } 86 | 87 | header > .nav-left > a > .logo { 88 | font-family: "Caecilia LT Std"; 89 | padding: 7px; 90 | line-height: 1.0; 91 | float: left; 92 | } 93 | 94 | h1 { 95 | text-align: center; 96 | font-size: 60px; 97 | line-height: 1.0; 98 | padding-top: 40px; 99 | padding-bottom: 15px; 100 | font-family: "Caecilia LT Std"; 101 | } 102 | 103 | h3 { 104 | text-align: center; 105 | color: #858585; 106 | font-size: 20px; 107 | padding-bottom: 40px; 108 | font-family: "Caecilia LT Std"; 109 | } 110 | 111 | form { 112 | width: 340px; 113 | margin: auto; 114 | margin-bottom: 30px; 115 | background: white; 116 | padding: 20px; 117 | border: 1px solid #bdbdbd; 118 | box-shadow: 0 1px 1px 0 grey; 119 | } 120 | 121 | h2 { 122 | font-size: 30px; 123 | float: left; 124 | } 125 | 126 | .new-account { 127 | float: right; 128 | font-size: 14px; 129 | } 130 | 131 | .input { 132 | margin: 15px 0; 133 | } 134 | 135 | .errors { 136 | text-align: center; 137 | color: #ED0B0E; 138 | margin-top: 8px; 139 | border-radius: 3px; 140 | } 141 | 142 | .input > label { 143 | display: block; 144 | color: #606060; 145 | font-size: 16px; 146 | margin-bottom: 4px; 147 | } 148 | 149 | .input > input { 150 | border: 2px solid rgb(202, 202, 202); 151 | padding: 6px; 152 | width: 94%; 153 | } 154 | 155 | .input > input:focus { 156 | border-color: #50AAF2; 157 | border-radius: 3px; 158 | border-opacity: 0.7; 159 | } 160 | 161 | .submit > input { 162 | background: #69ad31; 163 | padding: 8px 16px; 164 | box-shadow: 0 1.5px 0 0 #070; 165 | border-radius: 3px; 166 | color: white; 167 | } 168 | 169 | .submit > input:hover, 170 | .demo-user:hover { 171 | background: #56A225; 172 | } 173 | 174 | .submit > input:active, 175 | .demo-user:active { 176 | top: 3px; 177 | box-shadow: none; 178 | background: #488920; 179 | } 180 | 181 | .submit.sign-in { 182 | display: inline; 183 | margin-right: 8px; 184 | } 185 | 186 | .demo-user { 187 | background: #69ad31; 188 | padding: 8px 16px; 189 | box-shadow: 0 1.5px 0 0 #070; 190 | border-radius: 3px; 191 | color: white; 192 | cursor: pointer; 193 | margin: auto; 194 | margin-bottom: 30px; 195 | width: 300px; 196 | text-align: center; 197 | } 198 | 199 | .demo-user > a { 200 | color: white; 201 | } 202 | 203 | footer { 204 | text-align: center; 205 | padding-top: 80px; 206 | color: #ababab; 207 | font-size: 12px; 208 | } -------------------------------------------------------------------------------- /app/assets/javascripts/views/notes/notes_index.js: -------------------------------------------------------------------------------- 1 | BetterNote.Views.NotesIndex = Backbone.View.extend({ 2 | initialize: function(options) { 3 | this.type = options["type"]; 4 | this.$noteShowEl = options["$noteShowEl"]; 5 | 6 | this.listenTo(this.collection, "sort", this.render); 7 | this.listenTo(BetterNote.filter, "change", this.render); 8 | 9 | if (this.model) { 10 | this.listenTo(this.model, "change", this.render); 11 | this.listenTo(this.model, "destroy", this.refreshNotesIndex); 12 | } 13 | }, 14 | 15 | events: { 16 | "click .notes-list-footer-left-text": "showDropdown", 17 | "click .options-dropdown, .notes-list-footer-left-text": "stopPropagation", 18 | "click .sort-option": "sortNotes", 19 | "click .note-preview": "selectNote" 20 | }, 21 | 22 | template: JST['notes/index'], 23 | 24 | render: function() { 25 | var filteredCollection = this._filteredCollection(); 26 | var note = filteredCollection[0]; 27 | 28 | if (filteredCollection.length > 0 && !note.isNew()) { 29 | BetterNote.featuredNote = note; 30 | BetterNote.router.navigate("#/notes/" + note.get("id"), { 31 | trigger: true 32 | }); 33 | } else { 34 | this.$noteShowEl.html(""); 35 | } 36 | 37 | var renderedContent = this.template({ 38 | model: this.model, 39 | type: this.type 40 | }); 41 | this.$el.html(renderedContent); 42 | 43 | var that = this; 44 | _.each(filteredCollection, function(note) { 45 | var notePreviewView = new BetterNote.Views.NotePreview({ 46 | model: note 47 | }) 48 | 49 | that.$el.find("ul.notes").append(notePreviewView.render().$el) 50 | }) 51 | 52 | BetterNote.featuredNotes = this.collection; 53 | if (this.type === "all") { 54 | BetterNote.featuredNotes.id = "all"; 55 | } else if (this.type === "friend" ) { 56 | BetterNote.featuredNotes.id = "friend"; 57 | } else { 58 | BetterNote.featuredNotes.id = this.model.get("id"); 59 | } 60 | 61 | return this; 62 | }, 63 | 64 | showDropdown: function(event) { 65 | event.preventDefault(); 66 | this.hideDropdowns(); 67 | 68 | var $dropdown = $(event.currentTarget).closest(".dropdown-parent") 69 | .find(".options-dropdown"); 70 | $dropdown.removeClass("hidden"); 71 | }, 72 | 73 | hideDropdowns: function(event) { 74 | $(".options-dropdown").not("hidden").addClass("hidden"); 75 | }, 76 | 77 | stopPropagation: function(event) { 78 | event.stopPropagation(); 79 | }, 80 | 81 | selectNote: function(event) { 82 | $(event.currentTarget).closest(".notes") 83 | .find(".selected").removeClass("selected"); 84 | 85 | var $note = $(event.currentTarget).closest(".note-preview"); 86 | $note.addClass("selected"); 87 | }, 88 | 89 | refreshNotesIndex: function(event) { 90 | this.remove(); 91 | BetterNote.router.navigate("#/notes"); 92 | }, 93 | 94 | sortNotes: function(event) { 95 | event.preventDefault(); 96 | 97 | var $li = $(event.currentTarget); 98 | var sortField = $li.attr("data-sort-field"); 99 | var sortToggle = $li.attr("data-sort-toggle"); 100 | 101 | this.collection.setComparator(sortField, sortToggle); 102 | this.collection.sort(); 103 | 104 | this.hideDropdowns(); 105 | }, 106 | 107 | _filteredCollection: function() { 108 | var that = this; 109 | return this.collection.filter(function(note) { 110 | return that._filterTextMatch(note) && that._filterTagMatch(note); 111 | }) 112 | }, 113 | 114 | _filterTextMatch: function(note) { 115 | var title = note.get("title") ? note.get("title").toLowerCase() : ""; 116 | var body = note.get("body") ? note.get("body").toLowerCase() : ""; 117 | var searchText = BetterNote.filter.get("text"); 118 | 119 | var titleMatch = (title.indexOf(searchText) === -1) ? false : true; 120 | var bodyMatch = (body.indexOf(searchText) === -1) ? false : true; 121 | 122 | return titleMatch || bodyMatch; 123 | }, 124 | 125 | _filterTagMatch: function(note) { 126 | if (this.type === "friend") return true; 127 | 128 | if (BetterNote.filter.get("tag")) { 129 | var tagMatchId = BetterNote.filter.get("tag").get("id") 130 | var noteTagIds = note.noteTags.map(function(noteTag) { 131 | return noteTag.get("tag_id") 132 | }) 133 | 134 | return (noteTagIds.indexOf(tagMatchId) != -1); 135 | } else { 136 | return true; 137 | } 138 | } 139 | }); --------------------------------------------------------------------------------