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 |
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 |
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 |
37 |
38 | *Not actually. Evernote rocks.
39 |
40 |
41 | Copyright 2014 BetterNote Corporation. All rights reserved.
42 |
43 |
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 |
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 |
30 |
31 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/assets/templates/static_pages/search_bar.jst.ejs:
--------------------------------------------------------------------------------
1 |
7 |
39 |
40 |
41 |
42 | New Note
43 |
44 |
--------------------------------------------------------------------------------
/app/views/notes/_info.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | Title:
4 | <%= note.title %>
5 |
6 |
7 | Author:
8 | <%= note.author.username %>
9 |
10 |
11 | Created At:
12 | <%= note.created_at.strftime("%b %d, %Y") %>
13 |
14 |
15 | Updated At:
16 | <%= note.updated_at.strftime("%b %d, %Y") %>
17 |
18 |
19 | Notebook:
20 | <%= note.notebook.name %>
21 |
22 |
23 | Tags:
24 |
25 | <% if note.tags.length > 0 %>
26 |
33 | <% else %>
34 | No tags
35 | <% end %>
36 |
37 |
38 |
39 | Likes:
40 |
41 | <%= note.likes.count.to_s + " " + "like".pluralize(note.likes.count) %>
42 |
43 |
44 |
--------------------------------------------------------------------------------
/app/assets/templates/notes/friend_show.jst.ejs:
--------------------------------------------------------------------------------
1 |
25 |
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 | Title:
4 | <%= note.escape("title") %>
5 |
6 |
7 | Author:
8 | <%= note.author.escape("username") %>
9 |
10 |
11 | Created:
12 |
13 | <%= moment(note.escape("created_at")).format('MMM D, YYYY') %>
14 |
15 |
16 |
17 | Updated:
18 |
19 | <%= moment(note.escape("updated_at")).format('MMM D, YYYY') %>
20 |
21 |
22 |
23 | Notebook:
24 | <%= (note.friendNote) ? note.escape("notebook") : note.notebook.escape("name") %>
25 |
26 |
27 | Tags:
28 |
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 |
37 |
38 |
39 | Likes:
40 |
41 | <%= note.likes.length %>
42 |
43 |
44 |
--------------------------------------------------------------------------------
/app/views/notes/_edit.html.erb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 | Title
9 |
11 |
12 |
13 |
14 | Body
15 | <%= @note.body %>
16 |
17 |
18 |
19 | Notebook
20 |
21 | <% current_user.notebooks.each do |notebook| %>
22 | >
24 | <%= notebook.name %>
25 |
26 | <% end %>
27 |
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 |
8 |
--------------------------------------------------------------------------------
/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 |
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 |
6 |
25 |
--------------------------------------------------------------------------------
/app/views/notes/_comments.html.erb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
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 |
35 |
53 |
54 |
55 |
57 |
58 |
59 | <%= note.escape("body") %>
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 |
35 |
--------------------------------------------------------------------------------
/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 | });
--------------------------------------------------------------------------------