diff --git a/Gemfile b/Gemfile index 571e4098..120e0895 100644 --- a/Gemfile +++ b/Gemfile @@ -36,6 +36,11 @@ gem 'paranoia' gem 'coffee-rails' gem 'rails-jquery-autocomplete' +# Form enhancements +gem 'redcarpet' #markdown formatting +gem 'acts_as_list' #sortables +gem 'tribute' # @mentions + # SEO gem 'meta-tags' @@ -50,10 +55,6 @@ gem 'medium-editor-rails' gem 'chartkick' gem 'slack-notifier' -# Form enhancements -gem 'redcarpet' #markdown formatting -gem 'acts_as_list' #sortables - # Analytics gem 'mixpanel-ruby' gem 'barnes' diff --git a/Gemfile.lock b/Gemfile.lock index 8f9da3b6..daa92c7c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -34,45 +34,45 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (5.2.2) - actionpack (= 5.2.2) + actioncable (5.2.2.1) + actionpack (= 5.2.2.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailer (5.2.2) - actionpack (= 5.2.2) - actionview (= 5.2.2) - activejob (= 5.2.2) + actionmailer (5.2.2.1) + actionpack (= 5.2.2.1) + actionview (= 5.2.2.1) + activejob (= 5.2.2.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.2.2) - actionview (= 5.2.2) - activesupport (= 5.2.2) + actionpack (5.2.2.1) + actionview (= 5.2.2.1) + activesupport (= 5.2.2.1) rack (~> 2.0) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.2) - activesupport (= 5.2.2) + actionview (5.2.2.1) + activesupport (= 5.2.2.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) active_record_union (1.3.0) activerecord (>= 4.0) - activejob (5.2.2) - activesupport (= 5.2.2) + activejob (5.2.2.1) + activesupport (= 5.2.2.1) globalid (>= 0.3.6) - activemodel (5.2.2) - activesupport (= 5.2.2) - activerecord (5.2.2) - activemodel (= 5.2.2) - activesupport (= 5.2.2) + activemodel (5.2.2.1) + activesupport (= 5.2.2.1) + activerecord (5.2.2.1) + activemodel (= 5.2.2.1) + activesupport (= 5.2.2.1) arel (>= 9.0) - activestorage (5.2.2) - actionpack (= 5.2.2) - activerecord (= 5.2.2) + activestorage (5.2.2.1) + actionpack (= 5.2.2.1) + activerecord (= 5.2.2.1) marcel (~> 0.3.1) - activesupport (5.2.2) + activesupport (5.2.2.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) @@ -85,7 +85,7 @@ GEM ast (2.4.0) authority (3.3.0) activesupport (>= 3.0.0) - autoprefixer-rails (9.4.8) + autoprefixer-rails (9.4.10.2) execjs aws-eventstream (1.0.1) aws-partitions (1.131.0) @@ -150,7 +150,7 @@ GEM coffee-script-source execjs coffee-script-source (1.12.2) - concurrent-ruby (1.1.4) + concurrent-ruby (1.1.5) connection_pool (2.2.2) crack (0.4.3) safe_yaml (~> 1.0.0) @@ -184,7 +184,7 @@ GEM db_text_search (0.3.0) activerecord (>= 4.1.15, < 6.0) debug_inspector (0.0.3) - devise (4.5.0) + devise (4.6.1) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0, < 6.0) @@ -201,7 +201,7 @@ GEM railties (>= 3.0.0) faraday (0.15.4) multipart-post (>= 1.2, < 3) - ffi (1.9.25) + ffi (1.10.0) filesize (0.2.0) flamegraph (0.9.5) font-awesome-rails (4.7.0.4) @@ -236,7 +236,7 @@ GEM activesupport (>= 2) nokogiri (>= 1.4) htmlentities (4.3.4) - i18n (1.5.2) + i18n (1.6.0) concurrent-ruby (~> 1.0) inline_svg (1.3.1) activesupport (>= 3.0) @@ -314,7 +314,7 @@ GEM notiffany (0.1.1) nenv (~> 0.1) shellany (~> 0.0) - onebox (1.8.78) + onebox (1.8.82) htmlentities (~> 4.3) moneta (~> 1.0) multi_json (~> 1.11) @@ -328,16 +328,17 @@ GEM mime-types mimemagic (~> 0.3.0) terrapin (~> 0.6.0) - parallel (1.12.1) + parallel (1.14.0) paranoia (2.4.1) activerecord (>= 4.0, < 5.3) - parser (2.5.3.0) + parser (2.6.0.0) ast (~> 2.4.0) pg (0.21.0) powerpack (0.1.2) pry (0.12.2) coderay (~> 1.1.0) method_source (~> 0.9.0) + psych (3.1.0) public_suffix (3.0.3) puma (3.12.0) puma-heroku (1.0.0) @@ -354,18 +355,18 @@ GEM rack rack-test (1.1.0) rack (>= 1.0, < 3) - rails (5.2.2) - actioncable (= 5.2.2) - actionmailer (= 5.2.2) - actionpack (= 5.2.2) - actionview (= 5.2.2) - activejob (= 5.2.2) - activemodel (= 5.2.2) - activerecord (= 5.2.2) - activestorage (= 5.2.2) - activesupport (= 5.2.2) + rails (5.2.2.1) + actioncable (= 5.2.2.1) + actionmailer (= 5.2.2.1) + actionpack (= 5.2.2.1) + actionview (= 5.2.2.1) + activejob (= 5.2.2.1) + activemodel (= 5.2.2.1) + activerecord (= 5.2.2.1) + activestorage (= 5.2.2.1) + activesupport (= 5.2.2.1) bundler (>= 1.3.0) - railties (= 5.2.2) + railties (= 5.2.2.1) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.4) actionpack (>= 5.0.1.x) @@ -399,9 +400,9 @@ GEM sass-rails (>= 4.0, < 6) rails_serve_static_assets (0.0.5) rails_stdout_logging (0.0.5) - railties (5.2.2) - actionpack (= 5.2.2) - activesupport (= 5.2.2) + railties (5.2.2.1) + actionpack (= 5.2.2.1) + activesupport (= 5.2.2.1) method_source rake (>= 0.8.7) thor (>= 0.19.0, < 2.0) @@ -415,9 +416,9 @@ GEM redis (4.1.0) regexp_parser (1.3.0) remotipart (1.4.2) - responders (2.4.0) - actionpack (>= 4.2.0, < 5.3) - railties (>= 4.2.0, < 5.3) + responders (2.4.1) + actionpack (>= 4.2.0, < 6.0) + railties (>= 4.2.0, < 6.0) rinku (2.0.5) rmagick (2.16.0) rspec (3.8.0) @@ -444,11 +445,12 @@ GEM rspec-mocks (~> 3.8.0) rspec-support (~> 3.8.0) rspec-support (3.8.0) - rubocop (0.62.0) + rubocop (0.65.0) jaro_winkler (~> 1.5.1) parallel (~> 1.10) parser (>= 2.5, != 2.5.1.1) powerpack (~> 0.1) + psych (>= 3.1.0) rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.4.0) @@ -472,8 +474,8 @@ GEM sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) - sassc (2.0.0) - ffi (~> 1.9.6) + sassc (2.0.1) + ffi (~> 1.9) rake sassc-rails (2.1.0) railties (>= 4.0.0) @@ -523,8 +525,30 @@ GEM climate_control (>= 0.0.3, < 1.0) thor (0.20.3) thread_safe (0.3.6) + thredded (0.16.9) + active_record_union (>= 1.3.0) + autoprefixer-rails + db_text_search (~> 0.3.0) + friendly_id + html-pipeline + htmlentities + inline_svg + kaminari + kramdown (>= 2.0.0) + kramdown-parser-gfm + nokogiri + onebox (~> 1.8, >= 1.8.48) + pundit (>= 1.1.0) + rails (>= 4.2.10) + rb-gravatar + rinku + sanitize + sassc-rails (>= 2.0.0) + sprockets-es6 + timeago_js tilt (2.0.9) timeago_js (3.0.2) + tribute (3.6.0.0) tzinfo (1.2.5) thread_safe (~> 0.1) tzinfo-data (1.2018.9) @@ -618,7 +642,8 @@ DEPENDENCIES stackprof stripe stripe_event - thredded! + thredded + tribute tzinfo-data uglifier (>= 1.3.0) web-console diff --git a/README.rdoc b/README.rdoc index 0b13b7d9..3051b7d1 100644 --- a/README.rdoc +++ b/README.rdoc @@ -80,7 +80,7 @@ You should now see a copy of the site running locally at http://localhost:3000/! == Deployment to notebook.ai -Deployment to the live stage will only be done by approved developers, and consists of a deployment of +Deployment to the live production servers at www.Notebook.ai will only be done by approved developers, and consists of a deployment of - deploy github to staging (done only by approved developers) diff --git a/app/assets/images/card-headers/documents.jpg b/app/assets/images/card-headers/documents.jpg new file mode 100644 index 00000000..d5b6584b Binary files /dev/null and b/app/assets/images/card-headers/documents.jpg differ diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 43b08cbe..e1cc8dbc 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -13,4 +13,5 @@ //= require autocomplete-rails //= require cocoon //= require medium-editor +//= require tribute //= require_tree . diff --git a/app/assets/javascripts/content_filtering.js b/app/assets/javascripts/content_filtering.js new file mode 100644 index 00000000..1fa26886 --- /dev/null +++ b/app/assets/javascripts/content_filtering.js @@ -0,0 +1,17 @@ +$(document).ready(function () { + // Override the :contains() selector to make it case-insensitive + $.expr[":"].contains = $.expr.createPseudo(function(arg) { + return function( elem ) { + return $(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0; + }; + }); + + $('#js-content-name-filter').keyup(function (e) { + var search_query = $(this).val(); + var content_list = $('.js-content-cards-list > .js-content-card-container'); + + // Show everything, then hide what doesn't match + content_list.hide(); + content_list.find(".js-content-name:contains(" + search_query + ")").closest('.js-content-card-container').show(); + }); +}); diff --git a/app/assets/javascripts/content_linking.js b/app/assets/javascripts/content_linking.js deleted file mode 100644 index bfa77840..00000000 --- a/app/assets/javascripts/content_linking.js +++ /dev/null @@ -1,76 +0,0 @@ -$(document).ready(function () { - function add_link_bar(link_bar_container) { - $('.content-field-link-bar-container').html(''); - $(link_bar_container).html($('#content-field-link-bar-template').html()) - $(link_bar_container).find('.content-field-link-bar').show(); - $(link_bar_container).find('.dropdown-trigger').dropdown({coverTrigger: false}); - $(link_bar_container).find('.tooltipped').tooltip({ enterDelay: 50 }); - add_link_bar_click_handlers(); - } - - $('.content-field').find('textarea').focus(function (focus_event) { - var focused_text_field = $(focus_event.target); - var parent_content_field = focused_text_field.closest('.content-field'); - - $('.show-when-focused').hide(); - parent_content_field.find('.show-when-focused').show(); - - $('.content-field').removeClass('focused'); - parent_content_field.addClass('focused'); - $('.content-field-link-bar').hide(); - parent_content_field.find('.content-field-link-bar').show(); - - add_link_bar(parent_content_field.find('.content-field-link-bar-container')); - }); - - // um sir excuse me why does this need to be in a setTimeout? It doesn't work - // unless it is... for whatever reason. Here be dragons. - setTimeout(function () { - $('.content-field').find('.chips input').focus(function (focus_event) { - var focused_text_field = $(focus_event.target); - var parent_content_field = focused_text_field.closest('.content-field'); - - $('.show-when-focused').hide(); - parent_content_field.find('.show-when-focused').show(); - - $('.content-field').removeClass('focused'); - parent_content_field.addClass('focused'); - $('.content-field-link-bar').hide(); - parent_content_field.find('.content-field-link-bar').show(); - }); - }, 100); - - function add_link_bar_click_handlers() { - $('.js-content-link-option').click(function (click_event) { - var selected_option = $(click_event.target); - var textarea_to_modify = selected_option - .closest('.content-field') - .find('textarea'); - var current_textarea_value = textarea_to_modify.val(); - - var token_to_insert = [ - '[[', - selected_option.data('content-type'), - '-', - selected_option.data('content-id'), - ']]' - ].join('') - - // If the final character of the textarea is a newline or a space - // character, then we want to just append our token. Otherwise, we want - // to prepend our token with a space so it is properly separated. - var final_letter_presently = current_textarea_value.slice(-1); - if (final_letter_presently != ' ' && final_letter_presently != "\n") { - // Prepend a space - token_to_insert = ' ' + token_to_insert; - } - - // Make the change! - textarea_to_modify.val(current_textarea_value + token_to_insert); - - // Don't jump to the top of the page after clicking! - click_event.stopPropagation(); - return false; - }); - } -}); diff --git a/app/assets/javascripts/field_focusing.js b/app/assets/javascripts/field_focusing.js new file mode 100644 index 00000000..7e3cee57 --- /dev/null +++ b/app/assets/javascripts/field_focusing.js @@ -0,0 +1,31 @@ +$(document).ready(function () { + $('.content-field').find('textarea').focus(function (focus_event) { + var focused_text_field = $(focus_event.target); + var parent_content_field = focused_text_field.closest('.content-field'); + + $('.show-when-focused').hide(); + parent_content_field.find('.show-when-focused').show(); + + $('.content-field').removeClass('focused'); + parent_content_field.addClass('focused'); + $('.content-field-link-bar').hide(); + parent_content_field.find('.content-field-link-bar').show(); + }); + + // um sir excuse me why does this need to be in a setTimeout? It doesn't work + // unless it is... for whatever reason. Here be dragons. + setTimeout(function () { + $('.content-field').find('.chips input').focus(function (focus_event) { + var focused_text_field = $(focus_event.target); + var parent_content_field = focused_text_field.closest('.content-field'); + + $('.show-when-focused').hide(); + parent_content_field.find('.show-when-focused').show(); + + $('.content-field').removeClass('focused'); + parent_content_field.addClass('focused'); + $('.content-field-link-bar').hide(); + parent_content_field.find('.content-field-link-bar').show(); + }); + }, 100); +}); diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 9aa7a8b5..e0b1691b 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -13,5 +13,6 @@ *= require font-awesome *= require medium-editor/medium-editor *= require medium-editor/themes/beagle + *= require tribute *= require_tree . */ diff --git a/app/assets/stylesheets/buttons.scss b/app/assets/stylesheets/buttons.scss new file mode 100644 index 00000000..252ddb99 --- /dev/null +++ b/app/assets/stylesheets/buttons.scss @@ -0,0 +1,5 @@ +.btn { + .material-icons { + vertical-align: top; + } +} diff --git a/app/assets/stylesheets/content_linking.scss b/app/assets/stylesheets/content_linking.scss deleted file mode 100644 index 2500e21b..00000000 --- a/app/assets/stylesheets/content_linking.scss +++ /dev/null @@ -1,3 +0,0 @@ -.content-field-link-bar, .dropdown-content { - display: none; -} diff --git a/app/assets/stylesheets/content_linking.scss.erb b/app/assets/stylesheets/content_linking.scss.erb new file mode 100644 index 00000000..e2c4f1b6 --- /dev/null +++ b/app/assets/stylesheets/content_linking.scss.erb @@ -0,0 +1,6 @@ +<% Rails.application.config.content_types[:all].each do |content_type| %> + .<%= content_type.name.downcase %>-link { + color: <%= content_type.hex_color %>; + border-bottom: 1px dashed <%= content_type.hex_color %>; + } +<% end %> \ No newline at end of file diff --git a/app/assets/stylesheets/content_types.scss b/app/assets/stylesheets/content_types.scss index a5460d31..d4ed612c 100644 --- a/app/assets/stylesheets/content_types.scss +++ b/app/assets/stylesheets/content_types.scss @@ -25,3 +25,12 @@ right: 7px; z-index: 3; } + +.centered-card-content { + margin: 0; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -80%); + text-align: center; +} diff --git a/app/assets/stylesheets/layers.scss b/app/assets/stylesheets/layers.scss index d656c7df..2d968824 100644 --- a/app/assets/stylesheets/layers.scss +++ b/app/assets/stylesheets/layers.scss @@ -7,3 +7,8 @@ .slider .slides li.active { z-index: 1 !important; } + +.slider { + height: 140px; + overflow: hidden; +} \ No newline at end of file diff --git a/app/authorizers/document_authorizer.rb b/app/authorizers/document_authorizer.rb index 144e0f39..03d2a952 100644 --- a/app/authorizers/document_authorizer.rb +++ b/app/authorizers/document_authorizer.rb @@ -1,4 +1,8 @@ class DocumentAuthorizer < ApplicationAuthorizer + def self.creatable_by?(user) + true + end + def readable_by?(user) [ resource.user_id == user.id diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1a31a429..377c2ec5 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -69,4 +69,27 @@ class ApplicationController < ActionController::Base 0 end end + + def cache_linkable_content_for_each_content_type + linkable_classes = Rails.application.config.content_types[:all].map(&:name) & current_user.user_content_type_activators.pluck(:content_type) + linkable_classes -= ["Universe"] + + @linkables_cache = {} + linkable_classes.each do |class_name| + # class_name = "Character" + + @linkables_cache[class_name] = current_user.send("linkable_#{class_name.downcase.pluralize}") + .in_universe(@universe_scope) + + if @content.present? && @content.persisted? + @linkables_cache[class_name] = @linkables_cache[class_name] + .in_universe(@content.universe) + .reject { |content| content.class.name == class_name && content.id == @content.id } + end + + @linkables_cache[class_name] = @linkables_cache[class_name].sort_by(&:name) + .map { |c| [c.name, c.id] } + .compact + end + end end diff --git a/app/controllers/content_controller.rb b/app/controllers/content_controller.rb index 58b4a542..a4070e45 100644 --- a/app/controllers/content_controller.rb +++ b/app/controllers/content_controller.rb @@ -6,7 +6,7 @@ class ContentController < ApplicationController # old content to the new attributes styling. before_action :migrate_old_style_field_values, only: [:show, :edit] - before_action :populate_linkable_content_for_each_content_type, only: [:new, :edit] + before_action :cache_linkable_content_for_each_content_type, only: [:new, :edit] before_action :set_attributes_content_type, only: [:attributes] @@ -23,11 +23,11 @@ class ContentController < ApplicationController @content_type_class.attribute_categories(current_user) if @universe_scope.present? && @content_type_class != Universe - @content = @universe_scope.send(pluralized_content_name) + @content = @universe_scope.send(pluralized_content_name).includes(:page_tags, :image_uploads) else @content = ( - current_user.send(pluralized_content_name).includes(:page_tags) + - current_user.send("contributable_#{pluralized_content_name}").includes(:page_tags) + current_user.send(pluralized_content_name).includes(:page_tags, :image_uploads) + + current_user.send("contributable_#{pluralized_content_name}").includes(:page_tags, :image_uploads) ) unless @content_type_class == Universe @@ -42,12 +42,13 @@ class ContentController < ApplicationController @page_tags = PageTag.where( page_type: @content_type_class.name, page_id: @content.pluck(:id) - ) + ).order(:tag) if params.key?(:slug) - filtered_page_tags = @page_tags.where(slug: params[:slug]) - @content.select! { |content| filtered_page_tags.pluck(:page_id).include?(content.id) } + @filtered_page_tags = @page_tags.where(slug: params[:slug]) + @content.select! { |content| @filtered_page_tags.pluck(:page_id).include?(content.id) } end + @page_tags = @page_tags.uniq(&:tag) @content = @content.sort_by(&:name) respond_to do |format| @@ -182,7 +183,7 @@ class ContentController < ApplicationController upload_files params['image_uploads'], content_type.name, @content.id end - update_page_tags + update_page_tags if @content.respond_to?(:page_tags) successful_response(content_creation_redirect_url, t(:create_success, model_name: @content.try(:name).presence || humanized_model_name)) else @@ -214,7 +215,7 @@ class ContentController < ApplicationController end end - update_page_tags + update_page_tags if @content.respond_to?(:page_tags) if @content.user == current_user # todo this needs some extra validation probably to ensure each attribute is one associated with this page @@ -404,29 +405,6 @@ class ContentController < ApplicationController TemporaryFieldMigrationService.migrate_fields_for_content(content, current_user) if content.present? end - def populate_linkable_content_for_each_content_type - linkable_classes = Rails.application.config.content_types[:all].map(&:name) & current_user.user_content_type_activators.pluck(:content_type) - linkable_classes -= ["Universe"] - - @linkables_cache = {} - linkable_classes.each do |class_name| - # class_name = "Character" - - @linkables_cache[class_name] = current_user.send("linkable_#{class_name.downcase.pluralize}") - .in_universe(@universe_scope) - - if @content.present? && @content.persisted? - @linkables_cache[class_name] = @linkables_cache[class_name] - .in_universe(@content.universe) - .reject { |content| content.class.name == class_name && content.id == @content.id } - end - - @linkables_cache[class_name] = @linkables_cache[class_name].sort_by(&:name) - .map { |c| [c.name, c.id] } - .compact - end - end - def valid_content_types Rails.application.config.content_types[:all] end @@ -522,7 +500,7 @@ class ContentController < ApplicationController @navbar_actions << { label: "New #{content_type.name.downcase}", href: main_app.new_polymorphic_path(content_type) - } + } if user_signed_in? && current_user.can_create?(content_type) discussions_link = ForumsLinkbuilderService.worldbuilding_url(content_type) if discussions_link.present? @@ -550,11 +528,11 @@ class ContentController < ApplicationController href: main_app.polymorphic_path(content_type) } end - # - # @navbar_actions << { - # label: "New #{content_type.name.downcase}", - # href: main_app.new_polymorphic_path(content_type) - # } + + @navbar_actions << { + label: "New #{content_type.name.downcase}", + href: main_app.new_polymorphic_path(content_type) + } end end diff --git a/app/controllers/documents_controller.rb b/app/controllers/documents_controller.rb index 0d41ab04..b07cd8f9 100644 --- a/app/controllers/documents_controller.rb +++ b/app/controllers/documents_controller.rb @@ -6,6 +6,8 @@ class DocumentsController < ApplicationController before_action :set_navbar_actions, except: [:edit] before_action :set_footer_visibility, only: [:edit] + before_action :cache_linkable_content_for_each_content_type, only: [:edit] + layout 'editor', only: [:edit] def index diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 94379ecf..f1612ee3 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -5,8 +5,8 @@ class UsersController < ApplicationController def show @sidenav_expansion = 'my account' - - @user = User.find_by(id: params[:id]) + + @user = User.find_by(user_params) return redirect_to(root_path, notice: 'That user does not exist.') if @user.nil? @content = @user.public_content.select { |type, list| list.any? } @@ -70,4 +70,10 @@ class UsersController < ApplicationController notifier.ping ":bomb: :bomb: :bomb: #{user.email.split('@').first}@ (##{user.id}) just deleted their account." end + + private + + def user_params + params.permit(:id, :username) + end end diff --git a/app/models/concerns/has_image_uploads.rb b/app/models/concerns/has_image_uploads.rb index 113e94fc..129a234e 100644 --- a/app/models/concerns/has_image_uploads.rb +++ b/app/models/concerns/has_image_uploads.rb @@ -4,17 +4,10 @@ module HasImageUploads extend ActiveSupport::Concern included do - has_many :image_uploads + has_many :image_uploads, as: :content # todo: dependent: :destroy # todo: destroy from s3 on destroy - def image_uploads - ImageUpload.where( - content_type: self.class.name, - content_id: self.id - ) - end - def public_image_uploads self.image_uploads.where(privacy: 'public') end diff --git a/app/models/document.rb b/app/models/document.rb index 893473bd..c8d4f69e 100644 --- a/app/models/document.rb +++ b/app/models/document.rb @@ -25,4 +25,15 @@ class Document < ApplicationRecord def universe_field_value #todo when documents belong to a universe end + + def reading_estimate + words = (self.body || "").split(/\s+/).count + minutes = 1 + (words / 200).to_i + + "~#{minutes} minute read" + end + + def description + self.body + end end diff --git a/app/models/image_upload.rb b/app/models/image_upload.rb index d9676e25..6b9044c2 100644 --- a/app/models/image_upload.rb +++ b/app/models/image_upload.rb @@ -1,5 +1,6 @@ class ImageUpload < ApplicationRecord belongs_to :user + belongs_to :content, polymorphic: true has_attached_file :src, path: 'content/uploads/:style/:filename', diff --git a/app/models/user.rb b/app/models/user.rb index 4b05723c..a30c3da3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -11,7 +11,12 @@ class User < ApplicationRecord include HasContent include Authority::UserAbilities - validates_uniqueness_of :username, allow_nil: true, allow_blank: true + validates :username, + uniqueness: true, + allow_nil: true, + allow_blank: true, + length: { maximum: 40 }, + format: /\A[A-Za-z0-9\-_\$\+\!\*]+\z/ has_many :subscriptions, dependent: :destroy has_many :billing_plans, through: :subscriptions @@ -189,6 +194,14 @@ class User < ApplicationRecord found_key.user end + def profile_url + if self.username.present? + Rails.application.routes.url_helpers.profile_by_username_path(username: self.username) + else + Rails.application.routes.url_helpers.user_path(id: self.id) + end + end + private # Attributes that are non-public, and should be blacklisted from any public diff --git a/app/services/content_formatter_service.rb b/app/services/content_formatter_service.rb index 32a79399..9828146e 100644 --- a/app/services/content_formatter_service.rb +++ b/app/services/content_formatter_service.rb @@ -20,7 +20,7 @@ class ContentFormatterService < Service def self.show(text:, viewing_user: User.new) # We want to evaluate markdown first, because the markdown engine also happens # to strip out HTML tags. So: markdown, _then_ insert content links. - formatted_text = markdown.render(text).html_safe + formatted_text = markdown.render(text || '').html_safe substitute_content_links(formatted_text, viewing_user).html_safe end @@ -67,11 +67,11 @@ class ContentFormatterService < Service end def self.link_template(content_model) - chip_template(content_model.class) { link_to(content_model.name, link_for(content_model), class: "content_link #{content_model.class}-link") } + inline_template(content_model.class) { link_to(content_model.name, link_for(content_model), class: "content_link #{content_model.class.name.downcase}-link") } end def self.private_link_template(content_model) - chip_template(content_model.class) { link_to(content_model.name, link_for(content_model), class: 'grey-text content_link disabled') } + inline_template(content_model.class) { link_to(content_model.name, link_for(content_model), class: 'grey-text content_link disabled') } end def self.unknown_link_template(attempted_key) @@ -95,12 +95,20 @@ class ContentFormatterService < Service end end + def self.inline_template(class_model=nil) + content_tag(:span, class: 'inline-link') do + content_tag(:span, class: class_model ? "#{class_model.color}-text" : '') do + yield + end + end + end + # This is a hack until I figure out how to include polymorphic paths in a service model. #todo remove this def self.link_for(content_model) [ Rails.env.production? ? 'https://' : 'http://', - 'www.notebook.ai', # Rails.application.routes.default_url_options[:host], + Rails.env.production? ? 'www.notebook.ai' : 'localhost:3000', # Rails.application.routes.default_url_options[:host]? '/plan/', content_model.class.name.downcase.pluralize, '/', diff --git a/app/services/page_tag_service.rb b/app/services/page_tag_service.rb index 46d07370..cc91a6e2 100644 --- a/app/services/page_tag_service.rb +++ b/app/services/page_tag_service.rb @@ -16,13 +16,13 @@ class PageTagService < Service when Creature.name ["Domesticated", "Wild", "Humanoid"] when Deity.name - ["Good", "Evil"] + ["Good", "Evil", "Neutral"] when Flora.name - ["Floral", "Weed"] + ["Floral", "Weed", "Tree", "Bush"] when Government.name ["Republic", "Democracy", "Monarchy", "Dictatorship"] when Group.name - ["Good", "Evil"] + ["Good", "Evil", "Secret"] when Item.name ["Weapon", "Armor", "Artifact", "Relic"] when Job.name diff --git a/app/services/subscription_service.rb b/app/services/subscription_service.rb index 75282b56..de365cac 100644 --- a/app/services/subscription_service.rb +++ b/app/services/subscription_service.rb @@ -13,7 +13,9 @@ class SubscriptionService < Service # Add any one-time referral bonuses add_any_referral_bonuses(user, plan_id) - user.update(selected_billing_plan_id: related_plan.id) + # We intentionally skip callbacks on this to ensure the billing plan changes even on invalid users + user.update_column(:selected_billing_plan_id, related_plan.id) + user.subscriptions.create( billing_plan: related_plan, start_date: DateTime.now, diff --git a/app/views/cards/ui/_alert.html.erb b/app/views/cards/ui/_alert.html.erb index 01324c87..a60de6da 100644 --- a/app/views/cards/ui/_alert.html.erb +++ b/app/views/cards/ui/_alert.html.erb @@ -1,7 +1,7 @@ <% if alert %> <%= content_for :javascript do %> M.toast({ - html: "<%= alert.gsub('"', '\"').html_safe %>", + html: "<%= alert.gsub('"', '\"').gsub("\n", ' ').html_safe %>", classes: 'rounded' }) <% end %> diff --git a/app/views/cards/ui/_notice.html.erb b/app/views/cards/ui/_notice.html.erb index da6c047e..c376709c 100644 --- a/app/views/cards/ui/_notice.html.erb +++ b/app/views/cards/ui/_notice.html.erb @@ -1,7 +1,7 @@ <% if notice %> <%= content_for :javascript do %> M.toast({ - html: "<%= notice.gsub('"', '\"').html_safe %>", + html: "<%= notice.gsub('"', '\"').gsub("\n", ' ').html_safe %>", classes: 'rounded' }) <% end %> diff --git a/app/views/content/_form.html.erb b/app/views/content/_form.html.erb index 07987257..affa4ae6 100644 --- a/app/views/content/_form.html.erb +++ b/app/views/content/_form.html.erb @@ -18,45 +18,6 @@ Tip: You don't need to save before changing categories (on the right). Just remember to save when you're done! -
+ Tip: You can press @ to insert a link to your other pages. +
diff --git a/app/views/content/index.html.erb b/app/views/content/index.html.erb index d1a78061..afc21000 100644 --- a/app/views/content/index.html.erb +++ b/app/views/content/index.html.erb @@ -6,33 +6,10 @@ <%= render partial: 'content/components/parallax_header', locals: { content_type: content_type, content_class: @content_type_class } %> <% end %> - -<% if @page_tags.any? %> - - -<% end %> - <% if @content.any? %> - <%= render partial: 'content/components/list_filter_bar', locals: { } %> -+ <%= sanitize ContentFormatterService.show(text: truncate(content.description, length: 420, escape: false), viewing_user: current_user) %> +
+ <% end %> + <% if content.respond_to?(:page_tags) %> + + <% end %> + ++ <%= content.created_at == content.updated_at ? 'created' : 'last updated' %> + <%= time_ago_in_words content.updated_at %> + ago +
++ add +
++ <%= content_type.icon %> +
++ An active + <%= link_to 'Notebook.ai Premium', subscription_path, class: 'blue-text text-darken-2' %> subscription + is required to create additional + <%= content_type.name.downcase %> pages, but pages you've already created will always + be available here. +
+