diff --git a/.travis.yml b/.travis.yml index f3ab4715..515f444d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,12 @@ language: ruby +cache: bundler rvm: 2.1 +services: +- redis-server before_script: - - cp config/database.yml.postgres config/database.yml - - cp config/justask.yml.example config/justask.yml - - bundle exec rake db:drop db:create db:migrate db:test:prepare +- cp config/database.yml.postgres config/database.yml +- cp config/justask.yml.example config/justask.yml +- bundle exec rake db:drop db:create db:migrate db:test:prepare +notifications: + slack: + secure: aXIzsmSiwfwwIjBq759esN2+0jVXHt4XYRmhT6cpvkbtdE4LQapnco7XA3c6hD5LXvR67KZgBQjPcayhaPfbRCWbmmU5IXZyXpH1u+CcGsOZyDsStnGWF+AjYTMhWz3d0t+vbYDk3P4+hqXGvV3gi04a4nqNFI+soohBT919slJt9hqCY/fZYRkpXrn+F+OHSLRwp9R+SSznpwPAxyL0AXcqrRaThHupFtWQCCMTDGDPBfz5oJWzF7cWK+BRar2WIZ3q5lnp7CqTRbOlcpIDnGUMVTFLH7H13h/NZclaqqmgLZ0iCbs6sqN6ZLTVY6HQoUG8qJItEOomFQ6eXMgZ9ZZmINaDINojLjg1b2Y8mrwE6IXtDP9pQ/Hqth0kn1cCW1mQyjvhus+uEJ2N1y4QcZVoHFUCaOj/oEnCb+F8ZGKICtODRNWPEnX1cfLAxK9Hhc6zfkP5GAK9DNxaZNo0Zxvc2eVyS6XnnNzvLFjI7RVju+bJdQarW2nficbawiU8Z0KplKuQCA5yyC2CmKh4wWbBLcNd5y4iqcKIn0pdbc+xCPB89JgOyOfIPemtNpBhev2tffbUCH6hQ2j6C1iDCEZezLW/7oW+SAPGYSySw0uLGeqmB1oLRlDudgJGIITgP+hOhMcOKcQRM7+QwVAgxR2nMGOWAX5HD5aJeCZ+Z3Q= diff --git a/Gemfile b/Gemfile index 3484abaa..e60d4714 100644 --- a/Gemfile +++ b/Gemfile @@ -64,6 +64,9 @@ gem 'redis' group :development do gem 'spring' + # ten thousand raises no more! + gem 'byebug' + gem 'web-console' end group :production do diff --git a/Gemfile.lock b/Gemfile.lock index 171413af..089e06d2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -51,6 +51,8 @@ GEM coderay (>= 1.0.0) erubis (>= 2.6.6) rack (>= 0.9.0) + binding_of_caller (0.7.2) + debug_inspector (>= 0.0.1) bootstrap-sass (3.2.0.2) sass (~> 3.2) bootstrap3-datetimepicker-rails (4.7.14) @@ -60,6 +62,8 @@ GEM railties (>= 3.1) buftok (0.2.0) builder (3.2.2) + byebug (4.0.5) + columnize (= 0.9.0) capybara (2.4.4) mime-types (>= 1.16) nokogiri (>= 1.3.3) @@ -81,10 +85,12 @@ GEM coffee-script-source execjs coffee-script-source (1.9.1.1) + columnize (0.9.0) connection_pool (2.2.0) crass (1.0.2) daemons (1.2.2) database_cleaner (1.4.1) + debug_inspector (0.0.2) delayed_paperclip (2.9.1) paperclip (>= 3.3) devise (3.4.1) @@ -446,6 +452,11 @@ GEM raindrops (~> 0.7) warden (1.2.3) rack (>= 1.0) + web-console (2.1.2) + activemodel (>= 4.0) + binding_of_caller (>= 0.7.2) + railties (>= 4.0) + sprockets-rails (>= 2.0, < 4.0) websocket-driver (0.5.4) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) @@ -465,6 +476,7 @@ DEPENDENCIES bootstrap3-datetimepicker-rails (~> 4.7.14) bootstrap_form bootswatch-rails + byebug capybara coffee-rails (~> 4.1.0) database_cleaner @@ -517,5 +529,6 @@ DEPENDENCIES twitter uglifier (>= 1.3.0) unicorn + web-console will_paginate will_paginate-bootstrap diff --git a/README.md b/README.md index 4a4ca042..d981259f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# Retrospring [![Build Status](https://snap-ci.com/Retrospring/retrospring/branch/master/build_image)](https://snap-ci.com/Retrospring/retrospring/branch/master) +# Retrospring [![Build Status](https://travis-ci.org/Retrospring/retrospring.svg)](https://travis-ci.org/Retrospring/retrospring) [![Bugs](https://badge.waffle.io/retrospring/bugs.svg?label=in+progress&title=In+Progress)](http://waffle.io/retrospring/bugs) + This is the source code that powers Retrospring. Yep, all of it. Including all the branches where we left off. diff --git a/app/assets/javascripts/application.js.erb.coffee b/app/assets/javascripts/application.js.erb.coffee index 5fe872f9..e322e7fb 100644 --- a/app/assets/javascripts/application.js.erb.coffee +++ b/app/assets/javascripts/application.js.erb.coffee @@ -8,6 +8,7 @@ #= require growl #= require cheet #= require jquery.guillotine +#= require jquery.particleground #= require sweet-alert # local requires to be seen by everyone: #= require_tree ./answerbox @@ -44,5 +45,9 @@ _ready = -> if typeof sweetAlertInitialize != "undefined" sweetAlertInitialize() + particleground document.getElementById('particles'), + dotColor: '#5e35b1' + lineColor: '#5e35b1' + $(document).ready _ready $(document).on 'page:load', _ready diff --git a/app/assets/stylesheets/base.css.scss b/app/assets/stylesheets/base.css.scss index 92c82f35..aa31dc4d 100644 --- a/app/assets/stylesheets/base.css.scss +++ b/app/assets/stylesheets/base.css.scss @@ -6,6 +6,7 @@ body { background-color: #fafafa; } +@import "scss/variable"; @import "scss/generic"; @import "scss/answerbox"; @import "scss/comments"; @@ -15,21 +16,12 @@ body { @import "scss/user"; @import "scss/notifications"; @import "scss/groups"; +@import "scss/mobile"; .j2-page { padding-top: 30px; } -.question-page { - padding-top: 100px; -} - -@media(max-width: $screen-xs-max) { - .question-page { - padding-top: 130px; - } -} - .centre { text-align: center; } @@ -69,7 +61,7 @@ body { } .j2-lh { - color: #fff; + color: $main-color; } .about--moderator { padding-left: 0px; @@ -167,3 +159,39 @@ body { border-bottom-left-radius: 2px; border-bottom-right-radius: 2px; } + +.particle-jumbotron { + padding: 0px; + overflow: hidden; + position: relative; + width: 100%; +} + +.particle-content { + position: relative; + top: 0; + padding-top: 48px; + padding-bottom: 48px; + padding-left: 30px; + padding-right: 30px; +} + +#particles { + position: absolute; + width: 100%; + height: 100%; +} + +.icon-showcase { + font-size: 78px; + text-align: center; + display: block; +} + +.heading-showcase { + margin-top: 5px; +} + +.discover { + padding-top: 20px; +} diff --git a/app/assets/stylesheets/scss/comments.scss b/app/assets/stylesheets/scss/comments.scss index 55952077..08b73e2d 100644 --- a/app/assets/stylesheets/scss/comments.scss +++ b/app/assets/stylesheets/scss/comments.scss @@ -6,6 +6,10 @@ list-style-type: none; } +.comments .pull-right { + margin-top: -13px; +} + .comments--box { z-index: 99; } @@ -24,6 +28,10 @@ word-break: normal; } +.comments--content p { + margin-bottom: 0px; +} + .comments--media { overflow: visible !important; } \ No newline at end of file diff --git a/app/assets/stylesheets/scss/mobile.scss b/app/assets/stylesheets/scss/mobile.scss new file mode 100644 index 00000000..8279eb21 --- /dev/null +++ b/app/assets/stylesheets/scss/mobile.scss @@ -0,0 +1,4 @@ +@media (max-width: 768px) { + @import "mobile/settings"; + @import "mobile/profile"; +} diff --git a/app/assets/stylesheets/scss/mobile/profile.scss b/app/assets/stylesheets/scss/mobile/profile.scss new file mode 100644 index 00000000..3799ba3b --- /dev/null +++ b/app/assets/stylesheets/scss/mobile/profile.scss @@ -0,0 +1,76 @@ +.container.headerable:not(.profile--no-header) { + margin-top: 0; + padding-top: 0; +} + +#profile--header:not(.profile--no-header) { + min-width: 0px; + * { + min-width: 0px; + } +} + + +.container.headerable { + #profile-info { + box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.1); + margin-bottom: 15px; + + #profile.panel, #profile-stats.panel { + box-shadow: none; + margin-bottom: 0; + } + + #profile.panel { + font-size: 0; + + .profile--avatar { + width: 64px; + position: relative; + top: -15px; + left: 5px; + display: inline; + } + + .profile--panel-badge { + display: inline-block; + padding: 0 5px; + vertical-align: top; + width: auto; + margin-top: -15px; + &:nth-child(2) { + margin-left: 5px; + } + .fa { + font-size: 15px; + } + } + + .panel-body { + font-size: 15px; + .profile--panel-name { + margin-top: -75px; + margin-left: 60px + } + } + } + + #profile-stats { + border: none; + .panel-heading { + display: none; + } + + .panel-body { + font-size: 0px; + padding: 0; + .row { + width: 50%; + display: inline-block; + font-size: 12px; + margin: 0; + } + } + } + } +} diff --git a/app/assets/stylesheets/scss/mobile/settings.scss b/app/assets/stylesheets/scss/mobile/settings.scss new file mode 100644 index 00000000..a9b4f2cf --- /dev/null +++ b/app/assets/stylesheets/scss/mobile/settings.scss @@ -0,0 +1,11 @@ +#profile-header-media { + clear: both; + display: block; + .pull-left { + float: none !important; + clear: both; + img { + width: 100% + } + } +} diff --git a/app/assets/stylesheets/scss/panel.scss b/app/assets/stylesheets/scss/panel.scss index b54f8e4e..91c39e4f 100644 --- a/app/assets/stylesheets/scss/panel.scss +++ b/app/assets/stylesheets/scss/panel.scss @@ -27,6 +27,13 @@ border-color: #fff; } +.panel-question.question-hidden { + visibility: hidden; + position: relative; + box-shadow: none; + z-index: -1; +} + .answerbox--question-media, .question-media, .question-body { overflow: visible !important; } diff --git a/app/controllers/discover_controller.rb b/app/controllers/discover_controller.rb new file mode 100644 index 00000000..5d1490a5 --- /dev/null +++ b/app/controllers/discover_controller.rb @@ -0,0 +1,29 @@ +class DiscoverController < ApplicationController + before_filter :authenticate_user! + + def index + top_x = 10 # only display the top X items + + @popular_answers = Answer.where("created_at > ?", Time.now.ago(1.week)).order(:smile_count).reverse_order.limit(top_x) + @most_discussed = Answer.where("created_at > ?", Time.now.ago(1.week)).order(:comment_count).reverse_order.limit(top_x) + @popular_questions = Question.where("created_at > ?", Time.now.ago(1.week)).order(:answer_count).reverse_order.limit(top_x) + @new_users = User.where("asked_count > 0").order(:id).reverse_order.limit(top_x) + + # .user = the user + # .question_count = how many questions did the user ask + @users_with_most_questions = Question.select('user_id, COUNT(*) AS question_count'). + where("created_at > ?", Time.now.ago(1.week)). + where(author_is_anonymous: false). + group(:user_id). + order('question_count'). + reverse_order.limit(top_x) + + # .user = the user + # .answer_count = how many questions did the user answer + @users_with_most_answers = Answer.select('user_id, COUNT(*) AS answer_count'). + where("created_at > ?", Time.now.ago(1.week)). + group(:user_id). + order('answer_count'). + reverse_order.limit(top_x) + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 627f3408..438ed29c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -38,9 +38,9 @@ module ApplicationHelper content_tag(:a, body.html_safe, href: path, class: ("list-group-item #{'active ' if current_page? path}#{options[:class]}")) end - + ## - # + # def bootstrap_color c case c when "error", "alert" @@ -93,4 +93,64 @@ module ApplicationHelper return true if user_agent.match /^Mozilla\/\d+\.\d+ \(i(?:Phone|Pad|Pod); CPU(?:.*) like Mac OS X\)(?:.*) Mobile(?:\S*)$/ false end + + def generate_title(name, junction = nil, content = nil, s = false) + if s + if name[-1].downcase != "s" + name = name + "'s" + else + name = name + "'" + end + end + + list = [name] + + list.push junction unless junction.nil? + + unless content.nil? + content = strip_markdown(content) + if content.length > 45 + content = content[0..42] + "..." + end + list.push content + end + list.push "|", APP_CONFIG['site_name'] + + list.join " " + end + + def question_title(question) + name = user_screen_name question.user, question.author_is_anonymous, false + generate_title name, "asked", question.content + end + + def answer_title(answer) + name = user_screen_name answer.user, false, false + generate_title name, "answered", answer.question.content + end + + def user_title(user, junction = nil) + name = user_screen_name user, false, false + generate_title name, junction, nil, !junction.nil? + end + + def questions_title(user) + user_title user, "questions" + end + + def answers_title(user) + user_title user, "answers" + end + + def smiles_title(user) + user_title user, "smiles" + end + + def comments_title(user) + user_title user, "comments" + end + + def group_title(group) + generate_title group.name + end end diff --git a/app/helpers/discover_helper.rb b/app/helpers/discover_helper.rb new file mode 100644 index 00000000..6d7d2ed7 --- /dev/null +++ b/app/helpers/discover_helper.rb @@ -0,0 +1,2 @@ +module DiscoverHelper +end diff --git a/app/views/answer/show.html.haml b/app/views/answer/show.html.haml index 301e8629..9c1b3f1d 100644 --- a/app/views/answer/show.html.haml +++ b/app/views/answer/show.html.haml @@ -1,3 +1,3 @@ -- provide(:title, "#{@answer.user.display_name.blank? ? "@#{@answer.user.screen_name}'s" : "#{@answer.user.display_name}'s"} answer | #{APP_CONFIG['site_name']}") +- provide(:title, answer_title(@answer)) .container.j2-page = render 'shared/answerbox', a: @answer diff --git a/app/views/devise/confirmations/new.html.haml b/app/views/devise/confirmations/new.html.haml index f71e2208..c6a533a1 100644 --- a/app/views/devise/confirmations/new.html.haml +++ b/app/views/devise/confirmations/new.html.haml @@ -1,4 +1,4 @@ -- provide(:title, "Resend confirmation instructions | #{APP_CONFIG['site_name']}") +- provide(:title, generate_title("Resend confirmation instructions")) .container %h1 Resend confirmation instructions = bootstrap_form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml index 8ac67b9a..d773a8c1 100644 --- a/app/views/devise/passwords/edit.html.haml +++ b/app/views/devise/passwords/edit.html.haml @@ -1,4 +1,4 @@ -- provide(:title, "Change your password | #{APP_CONFIG['site_name']}") +- provide(:title, generate_title("Reset Password")) .container %h1 Change your password = bootstrap_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml index 5db5f61d..71045f16 100644 --- a/app/views/devise/passwords/new.html.haml +++ b/app/views/devise/passwords/new.html.haml @@ -1,4 +1,4 @@ -- provide(:title, "Forgot your password? | #{APP_CONFIG['site_name']}") +- provide(:title, generate_title("Forgot your Password?")) .container %h1 Forgot your password? = bootstrap_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| diff --git a/app/views/devise/registrations/new.html.haml b/app/views/devise/registrations/new.html.haml index a5c8dc45..3d5792d3 100644 --- a/app/views/devise/registrations/new.html.haml +++ b/app/views/devise/registrations/new.html.haml @@ -1,4 +1,4 @@ -- provide(:title, "Sign up | #{APP_CONFIG['site_name']}") +- provide(:title, generate_title("Sign Up")) .container %h1 Sign up diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index 26f5893d..e6e4e0d2 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -1,4 +1,4 @@ -- provide(:title, "Sign in | #{APP_CONFIG['site_name']}") +- provide(:title, generate_title("Sign In")) .container %h1 Sign in = render 'layouts/messages' diff --git a/app/views/devise/unlocks/new.html.haml b/app/views/devise/unlocks/new.html.haml index e718da96..f35c61e4 100644 --- a/app/views/devise/unlocks/new.html.haml +++ b/app/views/devise/unlocks/new.html.haml @@ -1,4 +1,4 @@ -- provide(:title, "Unlock | #{APP_CONFIG['site_name']}") +- provide(:title, generate_title("Unlock")) .container %h1 Resend unlock instructions = render 'layouts/messages' diff --git a/app/views/discover/_tab_answers.html.haml b/app/views/discover/_tab_answers.html.haml new file mode 100644 index 00000000..ed290d50 --- /dev/null +++ b/app/views/discover/_tab_answers.html.haml @@ -0,0 +1,3 @@ +.tab-pane.active.fade.in{role: "tabpanel", id: "answers"} + - answers.each do |a| + = render 'shared/answerbox', a: a diff --git a/app/views/discover/_tab_asked.html.haml b/app/views/discover/_tab_asked.html.haml new file mode 100644 index 00000000..95136d6c --- /dev/null +++ b/app/views/discover/_tab_asked.html.haml @@ -0,0 +1,3 @@ +.tab-pane.fade{role: "tabpanel", id: "asked"} + - asked.each do |user| + = render 'discover/userbox', u: user.user, type: "asked", q: user.question_count diff --git a/app/views/discover/_tab_discussed.html.haml b/app/views/discover/_tab_discussed.html.haml new file mode 100644 index 00000000..a6b73b4f --- /dev/null +++ b/app/views/discover/_tab_discussed.html.haml @@ -0,0 +1,3 @@ +.tab-pane.fade{role: "tabpanel", id: "comments"} + - comments.each do |a| + = render 'shared/answerbox', a: a diff --git a/app/views/discover/_tab_most.html.haml b/app/views/discover/_tab_most.html.haml new file mode 100644 index 00000000..3b9a1645 --- /dev/null +++ b/app/views/discover/_tab_most.html.haml @@ -0,0 +1,3 @@ +.tab-pane.fade{role: "tabpanel", id: "answered"} + - answered.each do |user| + = render 'discover/userbox', u: user.user, type: "most", a: user.answer_count diff --git a/app/views/discover/_tab_new.html.haml b/app/views/discover/_tab_new.html.haml new file mode 100644 index 00000000..261f904c --- /dev/null +++ b/app/views/discover/_tab_new.html.haml @@ -0,0 +1,3 @@ +.tab-pane.active.fade.in{role: "tabpanel", id: "new"} + - new.each do |user| + = render 'discover/userbox', u: user, type: "new" diff --git a/app/views/discover/_tab_questions.html.haml b/app/views/discover/_tab_questions.html.haml new file mode 100644 index 00000000..cd14a840 --- /dev/null +++ b/app/views/discover/_tab_questions.html.haml @@ -0,0 +1,3 @@ +.tab-pane.fade{role: "tabpanel", id: "questions"} + - questions.each do |q| + = render 'shared/question', q: q, type: "discover" diff --git a/app/views/discover/_userbox.html.haml b/app/views/discover/_userbox.html.haml new file mode 100644 index 00000000..3637a2a4 --- /dev/null +++ b/app/views/discover/_userbox.html.haml @@ -0,0 +1,26 @@ +.panel.panel-default.questionbox{data: { id: u.id }} + .panel-body + .media + .pull-left + %a{href: show_user_profile_path(u.screen_name)} + %img.answerbox--img{src: u.profile_picture.url(:medium)} + .media-body + %h6.media-heading + - if u.display_name.blank? + %a{href: show_user_profile_path(u.screen_name)} + %span= "@#{u.screen_name}" + - else + %a{href: show_user_profile_path(u.screen_name)} + %span= u.display_name + %span.text-muted= "@#{u.screen_name}" + %p.answerbox--question-text + - if type == "new" + registered + = time_ago_in_words(u.created_at) + ago + - elsif type == "most" + answered + = pluralize(a, "question") + - else + asked + = pluralize(q, "question") diff --git a/app/views/discover/index.html.haml b/app/views/discover/index.html.haml new file mode 100644 index 00000000..18f46562 --- /dev/null +++ b/app/views/discover/index.html.haml @@ -0,0 +1,48 @@ +- provide(:title, generate_title("Discover")) +.jumbotron.j2-jumbo.text-center.particle-jumbotron + #particles + .particle-content + %h1 Discover + %p + The perfect place to find interesting content from the last week on + = succeed '!' do + = APP_CONFIG['site_name'] +.container + .row + .col-md-7.col-sm-6 + %h2 Popular Content + %p Answers with most smiles and most answered questions + %div{role: "tabpanel"} + %ul.nav.nav-tabs{role: "tablist"} + %li.active{role: "presentation"} + %a{href: "#answers", role: "tab", aria: {controls: "answers"}, data: {toggle: "tab"}} + Answers + %li{role: "presentation"} + %a{href: "#questions", role: "tab", aria: {controls: "questions"}, data: {toggle: "tab"}} + Questions + %li{role: "presentation"} + %a{href: "#comments", role: "tab", aria: {controls: "comments"}, data: {toggle: "tab"}} + Most Comments + .tab-content.discover + = render 'discover/tab_answers', answers: @popular_answers + = render 'discover/tab_questions', questions: @popular_questions + = render 'discover/tab_discussed', comments: @most_discussed + .col-md-5.col-sm-6 + %h2 People + %p Newcomers and people who asked the most questions + %div{role: "tabpanel"} + %ul.nav.nav-tabs{role: "tablist"} + %li.active{role: "presentation"} + %a{href: "#new", role: "tab", aria: {controls: "new"}, data: {toggle: "tab"}} + New Users + %li{role: "presentation"} + %a{href: "#asked", role: "tab", aria: {controls: "asked"}, data: {toggle: "tab"}} + Most Asked Questions + %li{role: "presentation"} + %a{href: "#answered", role: "tab", aria: {controls: "answered"}, data: {toggle: "tab"}} + Most Answers + .tab-content.discover + = render 'discover/tab_new', new: @new_users + = render 'discover/tab_asked', asked: @users_with_most_questions + = render 'discover/tab_most', answered: @users_with_most_answers + = render 'shared/links' diff --git a/app/views/group/index.html.haml b/app/views/group/index.html.haml index e95f8c98..1ce3ecac 100644 --- a/app/views/group/index.html.haml +++ b/app/views/group/index.html.haml @@ -1,4 +1,4 @@ -- provide(:title, "#{@group.display_name} | #{APP_CONFIG['site_name']}") +- provide(:title, group_title(@group)) = render 'static/mobile_nav' .container.j2-page .col-md-3.col-sm-3 diff --git a/app/views/inbox/show.html.haml b/app/views/inbox/show.html.haml index f5acd1b9..c597545b 100644 --- a/app/views/inbox/show.html.haml +++ b/app/views/inbox/show.html.haml @@ -1,4 +1,4 @@ -- provide(:title, "Inbox | #{APP_CONFIG['site_name']}") +- provide(:title, generate_title("Inbox")) .container.j2-page .row .col-md-3.col-xs-12.col-sm-3.hidden-xs diff --git a/app/views/layouts/_header.html.haml b/app/views/layouts/_header.html.haml index dbacfbe9..b1fe5ed5 100644 --- a/app/views/layouts/_header.html.haml +++ b/app/views/layouts/_header.html.haml @@ -22,6 +22,7 @@ %ul.nav.navbar-nav = nav_entry "Timeline", root_path = nav_entry "Inbox", "/inbox", badge: inbox_count + = nav_entry "Discover", discover_path %ul.nav.navbar-nav.navbar-right = render "layouts/notifications" %li.hidden-xs{"data-toggle" => "tooltip", "data-placement" => "bottom", title: "Ask a question"} diff --git a/app/views/moderation/_discussion.html.haml b/app/views/moderation/_discussion.html.haml index 39e66a0c..51054f2e 100644 --- a/app/views/moderation/_discussion.html.haml +++ b/app/views/moderation/_discussion.html.haml @@ -8,7 +8,10 @@ .pull-left %img.img-rounded.answerbox--img{src: gravatar_url(comment.user)} .media-body.comments--body - %h6.media-heading.answerbox--question-user= user_screen_name comment.user + %h6.media-heading.answerbox--question-user + = user_screen_name comment.user + %span.text-muted{title: comment.created_at, data: { toggle: :tooltip, placement: :right }} + = "#{time_ago_in_words(comment.created_at)} ago" - if comment.user == current_user .pull-right .btn-group diff --git a/app/views/moderation/index.html.haml b/app/views/moderation/index.html.haml index 672983b3..e1ad35b3 100644 --- a/app/views/moderation/index.html.haml +++ b/app/views/moderation/index.html.haml @@ -1,4 +1,4 @@ -- provide(:title, "Moderation | #{APP_CONFIG['site_name']}") +- provide(:title, generate_title("Moderation")) = render 'moderation/moderation_nav' .container.j2-page .row diff --git a/app/views/notifications/index.html.haml b/app/views/notifications/index.html.haml index b3d3055c..c22e28cf 100644 --- a/app/views/notifications/index.html.haml +++ b/app/views/notifications/index.html.haml @@ -1,4 +1,4 @@ -- provide(:title, "Notifications | #{APP_CONFIG['site_name']}") +- provide(:title, generate_title("Notifications")) = render 'notifications/notification_nav' .container.j2-page = render 'notification_tabs' diff --git a/app/views/public/index.html.haml b/app/views/public/index.html.haml index 59a86c9d..b0f425e5 100644 --- a/app/views/public/index.html.haml +++ b/app/views/public/index.html.haml @@ -1,4 +1,4 @@ -- provide(:title, "Public Timeline | #{APP_CONFIG['site_name']}") +- provide(:title, generate_title("Public Timeline")) = render 'static/mobile_nav' .container.j2-page .col-md-3.col-sm-3 diff --git a/app/views/question/show.html.haml b/app/views/question/show.html.haml index 67ad0eb3..5ccbc6c4 100644 --- a/app/views/question/show.html.haml +++ b/app/views/question/show.html.haml @@ -1,35 +1,6 @@ -- provide(:title, "#{@question.user.display_name.blank? ? "@#{@question.user.screen_name}'s" : "#{@question.user.display_name}'s"} question | #{APP_CONFIG['site_name']}") -.panel.panel-question - .container - .panel-body - .media.question-media - - unless @question.author_is_anonymous - %a.pull-left{href: show_user_profile_path(@question.user.screen_name)} - %img.img-rounded.answerbox--img{src: gravatar_url(@question.user)} - .media-body.question-body - - if user_signed_in? - .pull-right - .btn-group - %button.btn.btn-link.btn-sm.dropdown-toggle{data: { toggle: :dropdown }, aria: { expanded: :false }} - %span.caret - %ul.dropdown-menu.dropdown-menu-right{role: :menu} - - if current_user.mod? or @question.user == current_user - %li.text-danger - %a{href: '#', data: { action: 'ab-question-destroy', q_id: @question.id, redirect: if @question.author_is_anonymous? then "/" else show_user_questions_path(@question.user.screen_name) end }} - %i.fa.fa-trash-o - Delete Question - - unless @question.user == current_user - %li - %a{href: '#', data: { action: 'ab-question-report', q_id: @question.id }} - %i.fa.fa-exclamation-triangle - Report - %h6.text-muted.media-heading.answerbox--question-user - = user_screen_name @question.user, @question.author_is_anonymous - asked - %span{title: @question.created_at, data: { toggle: :tooltip, placement: :bottom }} - = time_ago_in_words(@question.created_at) - ago - %p.answerbox--question-text= @question.content +- provide(:title, question_title(@question)) += render 'shared/question_header', question: @question, hidden: false += render 'shared/question_header', question: @question, hidden: true .container.question-page / TODO: make this pretty (it's currently C-c'd straight from shared/_answerbox) diff --git a/app/views/services/index.html.haml b/app/views/services/index.html.haml index fe04011b..a198bce9 100644 --- a/app/views/services/index.html.haml +++ b/app/views/services/index.html.haml @@ -1,4 +1,4 @@ -- provide(:title, "Service Settings | #{APP_CONFIG['site_name']}") +- provide(:title, generate_title("Service Settings")) .container.j2-page = render 'user/settings_tabs' .col-md-9.col-xs-12.col-sm-9 diff --git a/app/views/shared/_comments.html.haml b/app/views/shared/_comments.html.haml index 07c7971e..d302687b 100644 --- a/app/views/shared/_comments.html.haml +++ b/app/views/shared/_comments.html.haml @@ -9,7 +9,10 @@ .pull-left %img.img-rounded.answerbox--img{src: gravatar_url(comment.user)} .media-body.comments--body - %h6.media-heading.answerbox--question-user= user_screen_name comment.user + %h6.media-heading.answerbox--question-user + = user_screen_name comment.user + %span.text-muted{title: comment.created_at, data: { toggle: :tooltip, placement: :right }} + = "#{time_ago_in_words(comment.created_at)} ago" .pull-right %span.hidden-xs.text-muted - unless user_signed_in? diff --git a/app/views/shared/_question.html.haml b/app/views/shared/_question.html.haml index 2a592f5b..ea90fa2a 100644 --- a/app/views/shared/_question.html.haml +++ b/app/views/shared/_question.html.haml @@ -1,6 +1,10 @@ .panel.panel-default.questionbox{data: { id: q.id }} .panel-body .media + - if type == "discover" + .pull-left + %a{href: show_user_profile_path(q.user.screen_name)} + %img.answerbox--img{src: q.user.profile_picture.url(:medium)} .media-body - if user_signed_in? .pull-right diff --git a/app/views/shared/_question_header.haml b/app/views/shared/_question_header.haml new file mode 100644 index 00000000..0ead95b4 --- /dev/null +++ b/app/views/shared/_question_header.haml @@ -0,0 +1,33 @@ +.panel.panel-question{class: if hidden then 'question-hidden' end, tabindex: if hidden then '-1' end, aria: { hidden: if hidden then :true end }} + .container + .panel-body + .media.question-media + - unless question.author_is_anonymous + %a.pull-left{href: unless hidden then show_user_profile_path(question.user.screen_name) end} + %img.img-rounded.answerbox--img{src: gravatar_url(question.user)} + .media-body.question-body + - if user_signed_in? + .pull-right + .btn-group + %button.btn.btn-link.btn-sm.dropdown-toggle{data: { toggle: :dropdown }, aria: { expanded: :false }} + %span.caret + - unless hidden + %ul.dropdown-menu.dropdown-menu-right{role: :menu} + - if current_user.mod? or question.user == current_user + %li.text-danger + %a{href: '#', data: { action: 'ab-question-destroy', q_id: question.id, redirect: if question.author_is_anonymous? then "/" else show_user_questions_path(question.user.screen_name) end }} + %i.fa.fa-trash-o + Delete Question + - unless question.user == current_user + %li + %a{href: '#', data: { action: 'ab-question-report', q_id: question.id }} + %i.fa.fa-exclamation-triangle + Report + %h6.text-muted.media-heading.answerbox--question-user + = user_screen_name question.user, question.author_is_anonymous, !hidden + - unless hidden + asked + %span{title: question.created_at, data: { toggle: :tooltip, placement: :bottom }} + = time_ago_in_words(question.created_at) + ago + %p.answerbox--question-text= question.content diff --git a/app/views/shared/_sidebar.html.haml b/app/views/shared/_sidebar.html.haml index ec135868..d9304d8d 100644 --- a/app/views/shared/_sidebar.html.haml +++ b/app/views/shared/_sidebar.html.haml @@ -37,4 +37,4 @@ %a{href: show_user_profile_path(member.user.screen_name), title: member.user.screen_name, data: { toggle: :tooltip, placement: :top }} %img.img-rounded.answerbox--img-small{src: member.user.profile_picture.url(:medium)} -.hidden-xs= render 'shared/links' \ No newline at end of file +.hidden-xs= render 'shared/links' diff --git a/app/views/static/_front.html.haml b/app/views/static/_front.html.haml index bcd140d4..5a281a59 100644 --- a/app/views/static/_front.html.haml +++ b/app/views/static/_front.html.haml @@ -1,31 +1,38 @@ -.jumbotron.j2-jumbo.text-center - .container - = render 'layouts/messages' - %h1= APP_CONFIG['site_name'] - %p Ask questions, give answers and learn more about your friends. - %p - %a.btn.btn-primary.btn-lg{href: url_for(new_user_registration_path)} - Register now - %small - Already a member? - = link_to 'Sign in', new_user_session_path +.jumbotron.j2-jumbo.text-center.particle-jumbotron + #particles + .particle-content + .container + = render 'layouts/messages' + %h1= APP_CONFIG['site_name'] + %p Ask questions, give answers and learn more about your friends. + %p + %a.btn.btn-primary.btn-lg{href: url_for(new_user_registration_path)} + Register now + %small + Already a member? + = link_to 'Sign in', new_user_session_path .container-fluid - %h2.text-center Features .row.text-center .col-md-4.col-sm-4.col-xs-12 - %h3 + .icon-showcase + %i.fa.fa-comments + %h3.heading-showcase Ask and answer questions %p With = APP_CONFIG['site_name'] you can ask people questions and answer questions from other users or unregistered people. Want to know something more? Keep the discussion ongoing in the comments! .col-md-4.col-sm-4.col-xs-12 - %h3 + .icon-showcase + %i.fa.fa-users + %h3.heading-showcase Follow users and get followed %p Following users allows you to get a personalized feed of all people you want to know more about. You can also send a question to all your followers at once! .col-md-4.col-sm-4.col-xs-12 - %h3 + .icon-showcase + %i.fa.fa-share-square-o + %h3.heading-showcase Sharing to other networks %p Want to share your answer to a question so that more people read it? With a simple click on the answer button, your answer is shared wherever you want! diff --git a/app/views/static/about.html.haml b/app/views/static/about.html.haml index 03d6a77f..e1a9a97f 100644 --- a/app/views/static/about.html.haml +++ b/app/views/static/about.html.haml @@ -1,7 +1,9 @@ -- provide(:title, "About | #{APP_CONFIG['site_name']}") -.jumbotron.j2-jumbo.text-center - %h1= APP_CONFIG['site_name'] - %p About our service, features and other information +- provide(:title, generate_title("About")) +.jumbotron.j2-jumbo.text-center.particle-jumbotron + #particles + .particle-content + %h1= APP_CONFIG['site_name'] + %p About our service, features and other information .container = render 'layouts/messages' diff --git a/app/views/static/faq.html.haml b/app/views/static/faq.html.haml index 2915de26..56c937b9 100644 --- a/app/views/static/faq.html.haml +++ b/app/views/static/faq.html.haml @@ -1,7 +1,13 @@ -- provide(:title, "Frequently Asked Questions | #{APP_CONFIG['site_name']}") +- provide(:title, generate_title("Frequently Asked Questions")) +.jumbotron.j2-jumbo.text-center.particle-jumbotron + #particles + .particle-content + %h1 Frequently Asked Questions + %p + Everything you want to know about + = succeed '!' do + = APP_CONFIG['site_name'] .container - %h1.text-center Frequently Asked Questions - .panel-group{id: "accordion", role: "tablist", aria: {multiselectable: :true}} .panel.panel-default .panel-heading{id: "faqOne", role: "tab"} diff --git a/app/views/static/privacy_policy.html.haml b/app/views/static/privacy_policy.html.haml index 7ea7c7c5..931a71ad 100644 --- a/app/views/static/privacy_policy.html.haml +++ b/app/views/static/privacy_policy.html.haml @@ -1,4 +1,4 @@ -- provide(:title, "Privacy Policy | #{APP_CONFIG['site_name']}") +- provide(:title, generate_title("Privacy Policy")) .container .panel.panel-default .panel-body diff --git a/app/views/static/terms.html.haml b/app/views/static/terms.html.haml index f918cab0..b31f8da7 100644 --- a/app/views/static/terms.html.haml +++ b/app/views/static/terms.html.haml @@ -1,4 +1,4 @@ -- provide(:title, "Terms of Service | #{APP_CONFIG['site_name']}") +- provide(:title, generate_title("Terms of Service")) .container .panel.panel-default .panel-body diff --git a/app/views/user/_account.html.haml b/app/views/user/_account.html.haml index 0c9eda99..208d4d1f 100644 --- a/app/views/user/_account.html.haml +++ b/app/views/user/_account.html.haml @@ -1,4 +1,4 @@ -- provide(:title, "Account Settings | #{APP_CONFIG['site_name']}") +- provide(:title, generate_title("Account Settings")) .container.j2-page = render 'user/settings_tabs' .col-md-9.col-xs-12.col-sm-9 diff --git a/app/views/user/_profile_info.html.haml b/app/views/user/_profile_info.html.haml index 97cb3076..0d1d866e 100644 --- a/app/views/user/_profile_info.html.haml +++ b/app/views/user/_profile_info.html.haml @@ -28,14 +28,15 @@ .profile--panel-badge.panel-badge-default Follows you .panel-body - - if @user.display_name.blank? - .profile--displayname - = @user.screen_name - - else - .profile--displayname - = @user.display_name - .profile--username - = @user.screen_name + .profile--panel-name + - if @user.display_name.blank? + .profile--displayname + = @user.screen_name + - else + .profile--displayname + = @user.display_name + .profile--username + = @user.screen_name - unless @user.bio.blank? %p.profile--text= markdown @user.bio - unless @user.website.blank? diff --git a/app/views/user/_stats.html.haml b/app/views/user/_stats.html.haml index b6ff4d5d..7ca49232 100644 --- a/app/views/user/_stats.html.haml +++ b/app/views/user/_stats.html.haml @@ -1,4 +1,4 @@ -.panel.panel-default.profile--panel +.panel.panel-default.profile--panel#profile-stats .panel-heading %h3.panel-title Stats .panel-body @@ -19,4 +19,4 @@ %a{href: show_user_profile_path(@user.screen_name)} .col-md-6.col-sm-6.col-xs-6 %h4.entry-text#answered-count= @user.answered_count - %h6.entry-subtext Answers \ No newline at end of file + %h6.entry-subtext Answers diff --git a/app/views/user/edit.html.haml b/app/views/user/edit.html.haml index 9a646467..a2409b3a 100644 --- a/app/views/user/edit.html.haml +++ b/app/views/user/edit.html.haml @@ -1,4 +1,4 @@ -- provide(:title, "Profile Settings | #{APP_CONFIG['site_name']}") +- provide(:title, generate_title("Profile Settings")) .container.j2-page = render 'settings_tabs' .col-md-9.col-xs-12.col-sm-9 @@ -9,7 +9,7 @@ = f.text_field :display_name, label: "Your name" - .media + .media#profile-picture-media .pull-left %img.img-rounded.profile--img{src: current_user.profile_picture.url(:medium)} .media-body @@ -26,7 +26,7 @@ %button#cropper-zoom-in.btn.btn-inverse{type: :button} %i.fa.fa-search-plus - .media + .media#profile-header-media .pull-left %img.img-rounded.header--img{src: current_user.profile_header.url(:mobile)} .media-body diff --git a/app/views/user/edit_privacy.html.haml b/app/views/user/edit_privacy.html.haml index b705cb12..26aad1cc 100644 --- a/app/views/user/edit_privacy.html.haml +++ b/app/views/user/edit_privacy.html.haml @@ -1,4 +1,4 @@ -- provide(:title, "Privacy Settings | #{APP_CONFIG['site_name']}") +- provide(:title, generate_title("Privacy Settings")) .container.j2-page = render 'settings_tabs' .col-md-9.col-xs-12.col-sm-9 diff --git a/app/views/user/questions.html.haml b/app/views/user/questions.html.haml index 884e88b3..e8f5d435 100644 --- a/app/views/user/questions.html.haml +++ b/app/views/user/questions.html.haml @@ -1,4 +1,4 @@ -- provide(:title, "#{@user.display_name.blank? ? "@#{@user.screen_name}'s" : "#{@user.display_name}'s"} questions | #{APP_CONFIG['site_name']}") +- provide(:title, questions_title(@user)) .profile--header .container.j2-page .col-md-3.col-xs-12.col-sm-4.j2-col-reset @@ -9,7 +9,7 @@ %h1.visible-xs= @title #questions - @questions.each do |q| - = render 'shared/question', q: q + = render 'shared/question', q: q, type: nil #pagination= will_paginate @questions, renderer: BootstrapPagination::Rails, page_links: false diff --git a/app/views/user/show.html.haml b/app/views/user/show.html.haml index 47e32e9d..00f5b723 100644 --- a/app/views/user/show.html.haml +++ b/app/views/user/show.html.haml @@ -1,9 +1,9 @@ -- provide(:title, "#{@user.display_name.blank? ? "@#{@user.screen_name}" : "#{@user.display_name} (@#{@user.screen_name})"} | #{APP_CONFIG['site_name']}") +- provide(:title, user_title(@user)) - no_header = unless @user.profile_header.exists? then "profile--no-header" else "" end -#profile--header.hidden-xs{class: no_header} +#profile--header{class: no_header} %img.profile--header-img{src: @user.profile_header.url(:web)} .container.j2-page.headerable{class: no_header} - .col-md-3.col-xs-12.col-sm-4.j2-col-reset + #profile-info.col-md-3.col-xs-12.col-sm-4.j2-col-reset = render 'user/profile_info' .hidden-xs= render 'shared/links' .col-md-9.col-xs-12.col-sm-8.j2-col-reset diff --git a/app/views/user/show_follow.html.haml b/app/views/user/show_follow.html.haml index f1af7740..d1d3878f 100644 --- a/app/views/user/show_follow.html.haml +++ b/app/views/user/show_follow.html.haml @@ -1,4 +1,4 @@ -- provide(:title, "#{@user.display_name.blank? ? "@#{@user.screen_name}'s" : "#{@user.display_name}'s"} friends & followers | #{APP_CONFIG['site_name']}") +- provide(:title, user_title(@user, "friends and followers")) .profile--header .container.j2-page .col-md-3.col-xs-12.col-sm-4.j2-col-reset diff --git a/config/routes.rb b/config/routes.rb index 9b8c648f..8cb2a9ca 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -90,6 +90,7 @@ Rails.application.routes.draw do match '/unsubscribe', to: 'subscription#unsubscribe', via: :post, as: :unsubscribe_answer end + match '/discover', to: 'discover#index', via: :get, as: :discover match '/public', to: 'public#index', via: :get, as: :public_timeline match '/group/:group_name', to: 'group#index', via: :get, as: :group_timeline diff --git a/vendor/assets/javascripts/jquery.particleground.js b/vendor/assets/javascripts/jquery.particleground.js new file mode 100644 index 00000000..31466252 --- /dev/null +++ b/vendor/assets/javascripts/jquery.particleground.js @@ -0,0 +1,453 @@ +/*! + * Particleground + * + * @author Jonathan Nicol - @mrjnicol + * @version 1.1.0 + * @description Creates a canvas based particle system background + * + * Inspired by http://requestlab.fr/ and http://disruptivebydesign.com/ + */ + +;(function(window, document) { + "use strict"; + var pluginName = 'particleground'; + + // http://youmightnotneedjquery.com/#deep_extend + function extend(out) { + out = out || {}; + for (var i = 1; i < arguments.length; i++) { + var obj = arguments[i]; + if (!obj) continue; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + if (typeof obj[key] === 'object') + deepExtend(out[key], obj[key]); + else + out[key] = obj[key]; + } + } + } + return out; + }; + + var $ = window.jQuery; + + function Plugin(element, options) { + var canvasSupport = !!document.createElement('canvas').getContext; + var canvas; + var ctx; + var particles = []; + var raf; + var mouseX = 0; + var mouseY = 0; + var winW; + var winH; + var desktop = !navigator.userAgent.match(/(iPhone|iPod|iPad|Android|BlackBerry|BB10|mobi|tablet|opera mini|nexus 7)/i); + var orientationSupport = !!window.DeviceOrientationEvent; + var tiltX = 0; + var pointerX; + var pointerY; + var tiltY = 0; + var paused = false; + + options = extend({}, window[pluginName].defaults, options); + + /** + * Init + */ + function init() { + if (!canvasSupport) { return; } + + //Create canvas + canvas = document.createElement('canvas'); + canvas.className = 'pg-canvas'; + canvas.style.display = 'block'; + element.insertBefore(canvas, element.firstChild); + ctx = canvas.getContext('2d'); + styleCanvas(); + + // Create particles + var numParticles = Math.round((canvas.width * canvas.height) / options.density); + for (var i = 0; i < numParticles; i++) { + var p = new Particle(); + p.setStackPos(i); + particles.push(p); + }; + + window.addEventListener('resize', function() { + resizeHandler(); + }, false); + + document.addEventListener('mousemove', function(e) { + mouseX = e.pageX; + mouseY = e.pageY; + }, false); + + if (orientationSupport && !desktop) { + window.addEventListener('deviceorientation', function () { + // Contrain tilt range to [-30,30] + tiltY = Math.min(Math.max(-event.beta, -30), 30); + tiltX = Math.min(Math.max(-event.gamma, -30), 30); + }, true); + } + + draw(); + hook('onInit'); + } + + /** + * Style the canvas + */ + function styleCanvas() { + canvas.width = element.offsetWidth; + canvas.height = element.offsetHeight; + ctx.fillStyle = options.dotColor; + ctx.strokeStyle = options.lineColor; + ctx.lineWidth = options.lineWidth; + } + + /** + * Draw particles + */ + function draw() { + if (!canvasSupport) { return; } + + winW = window.innerWidth; + winH = window.innerHeight; + + // Wipe canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Update particle positions + for (var i = 0; i < particles.length; i++) { + particles[i].updatePosition(); + }; + // Draw particles + for (var i = 0; i < particles.length; i++) { + particles[i].draw(); + }; + + // Call this function next time screen is redrawn + if (!paused) { + raf = requestAnimationFrame(draw); + } + } + + /** + * Add/remove particles. + */ + function resizeHandler() { + // Resize the canvas + styleCanvas(); + + var elWidth = element.offsetWidth; + var elHeight = element.offsetHeight; + + // Remove particles that are outside the canvas + for (var i = particles.length - 1; i >= 0; i--) { + if (particles[i].position.x > elWidth || particles[i].position.y > elHeight) { + particles.splice(i, 1); + } + }; + + // Adjust particle density + var numParticles = Math.round((canvas.width * canvas.height) / options.density); + if (numParticles > particles.length) { + while (numParticles > particles.length) { + var p = new Particle(); + particles.push(p); + } + } else if (numParticles < particles.length) { + particles.splice(numParticles); + } + + // Re-index particles + for (i = particles.length - 1; i >= 0; i--) { + particles[i].setStackPos(i); + }; + } + + /** + * Pause particle system + */ + function pause() { + paused = true; + } + + /** + * Start particle system + */ + function start() { + paused = false; + draw(); + } + + /** + * Particle + */ + function Particle() { + this.stackPos; + this.active = true; + this.layer = Math.ceil(Math.random() * 3); + this.parallaxOffsetX = 0; + this.parallaxOffsetY = 0; + // Initial particle position + this.position = { + x: Math.ceil(Math.random() * canvas.width), + y: Math.ceil(Math.random() * canvas.height) + } + // Random particle speed, within min and max values + this.speed = {} + switch (options.directionX) { + case 'left': + this.speed.x = +(-options.maxSpeedX + (Math.random() * options.maxSpeedX) - options.minSpeedX).toFixed(2); + break; + case 'right': + this.speed.x = +((Math.random() * options.maxSpeedX) + options.minSpeedX).toFixed(2); + break; + default: + this.speed.x = +((-options.maxSpeedX / 2) + (Math.random() * options.maxSpeedX)).toFixed(2); + this.speed.x += this.speed.x > 0 ? options.minSpeedX : -options.minSpeedX; + break; + } + switch (options.directionY) { + case 'up': + this.speed.y = +(-options.maxSpeedY + (Math.random() * options.maxSpeedY) - options.minSpeedY).toFixed(2); + break; + case 'down': + this.speed.y = +((Math.random() * options.maxSpeedY) + options.minSpeedY).toFixed(2); + break; + default: + this.speed.y = +((-options.maxSpeedY / 2) + (Math.random() * options.maxSpeedY)).toFixed(2); + this.speed.x += this.speed.y > 0 ? options.minSpeedY : -options.minSpeedY; + break; + } + } + + /** + * Draw particle + */ + Particle.prototype.draw = function() { + // Draw circle + ctx.beginPath(); + ctx.arc(this.position.x + this.parallaxOffsetX, this.position.y + this.parallaxOffsetY, options.particleRadius / 2, 0, Math.PI * 2, true); + ctx.closePath(); + ctx.fill(); + + // Draw lines + ctx.beginPath(); + // Iterate over all particles which are higher in the stack than this one + for (var i = particles.length - 1; i > this.stackPos; i--) { + var p2 = particles[i]; + + // Pythagorus theorum to get distance between two points + var a = this.position.x - p2.position.x + var b = this.position.y - p2.position.y + var dist = Math.sqrt((a * a) + (b * b)).toFixed(2); + + // If the two particles are in proximity, join them + if (dist < options.proximity) { + ctx.moveTo(this.position.x + this.parallaxOffsetX, this.position.y + this.parallaxOffsetY); + if (options.curvedLines) { + ctx.quadraticCurveTo(Math.max(p2.position.x, p2.position.x), Math.min(p2.position.y, p2.position.y), p2.position.x + p2.parallaxOffsetX, p2.position.y + p2.parallaxOffsetY); + } else { + ctx.lineTo(p2.position.x + p2.parallaxOffsetX, p2.position.y + p2.parallaxOffsetY); + } + } + } + ctx.stroke(); + ctx.closePath(); + } + + /** + * update particle position + */ + Particle.prototype.updatePosition = function() { + if (options.parallax) { + if (orientationSupport && !desktop) { + // Map tiltX range [-30,30] to range [0,winW] + var ratioX = (winW - 0) / (30 - -30); + pointerX = (tiltX - -30) * ratioX + 0; + // Map tiltY range [-30,30] to range [0,winH] + var ratioY = (winH - 0) / (30 - -30); + pointerY = (tiltY - -30) * ratioY + 0; + } else { + pointerX = mouseX; + pointerY = mouseY; + } + // Calculate parallax offsets + this.parallaxTargX = (pointerX - (winW / 2)) / (options.parallaxMultiplier * this.layer); + this.parallaxOffsetX += (this.parallaxTargX - this.parallaxOffsetX) / 10; // Easing equation + this.parallaxTargY = (pointerY - (winH / 2)) / (options.parallaxMultiplier * this.layer); + this.parallaxOffsetY += (this.parallaxTargY - this.parallaxOffsetY) / 10; // Easing equation + } + + var elWidth = element.offsetWidth; + var elHeight = element.offsetHeight; + + switch (options.directionX) { + case 'left': + if (this.position.x + this.speed.x + this.parallaxOffsetX < 0) { + this.position.x = elWidth - this.parallaxOffsetX; + } + break; + case 'right': + if (this.position.x + this.speed.x + this.parallaxOffsetX > elWidth) { + this.position.x = 0 - this.parallaxOffsetX; + } + break; + default: + // If particle has reached edge of canvas, reverse its direction + if (this.position.x + this.speed.x + this.parallaxOffsetX > elWidth || this.position.x + this.speed.x + this.parallaxOffsetX < 0) { + this.speed.x = -this.speed.x; + } + break; + } + + switch (options.directionY) { + case 'up': + if (this.position.y + this.speed.y + this.parallaxOffsetY < 0) { + this.position.y = elHeight - this.parallaxOffsetY; + } + break; + case 'down': + if (this.position.y + this.speed.y + this.parallaxOffsetY > elHeight) { + this.position.y = 0 - this.parallaxOffsetY; + } + break; + default: + // If particle has reached edge of canvas, reverse its direction + if (this.position.y + this.speed.y + this.parallaxOffsetY > elHeight || this.position.y + this.speed.y + this.parallaxOffsetY < 0) { + this.speed.y = -this.speed.y; + } + break; + } + + // Move particle + this.position.x += this.speed.x; + this.position.y += this.speed.y; + } + + /** + * Setter: particle stacking position + */ + Particle.prototype.setStackPos = function(i) { + this.stackPos = i; + } + + function option (key, val) { + if (val) { + options[key] = val; + } else { + return options[key]; + } + } + + function destroy() { + console.log('destroy'); + canvas.parentNode.removeChild(canvas); + hook('onDestroy'); + if ($) { + $(element).removeData('plugin_' + pluginName); + } + } + + function hook(hookName) { + if (options[hookName] !== undefined) { + options[hookName].call(element); + } + } + + init(); + + return { + option: option, + destroy: destroy, + start: start, + pause: pause + }; + } + + window[pluginName] = function(elem, options) { + return new Plugin(elem, options); + }; + + window[pluginName].defaults = { + minSpeedX: 0.1, + maxSpeedX: 0.7, + minSpeedY: 0.1, + maxSpeedY: 0.7, + directionX: 'center', // 'center', 'left' or 'right'. 'center' = dots bounce off edges + directionY: 'center', // 'center', 'up' or 'down'. 'center' = dots bounce off edges + density: 10000, // How many particles will be generated: one particle every n pixels + dotColor: '#666666', + lineColor: '#666666', + particleRadius: 7, // Dot size + lineWidth: 1, + curvedLines: false, + proximity: 100, // How close two dots need to be before they join + parallax: true, + parallaxMultiplier: 5, // The lower the number, the more extreme the parallax effect + onInit: function() {}, + onDestroy: function() {} + }; + + // nothing wrong with hooking into jQuery if it's there... + if ($) { + $.fn[pluginName] = function(options) { + if (typeof arguments[0] === 'string') { + var methodName = arguments[0]; + var args = Array.prototype.slice.call(arguments, 1); + var returnVal; + this.each(function() { + if ($.data(this, 'plugin_' + pluginName) && typeof $.data(this, 'plugin_' + pluginName)[methodName] === 'function') { + returnVal = $.data(this, 'plugin_' + pluginName)[methodName].apply(this, args); + } + }); + if (returnVal !== undefined){ + return returnVal; + } else { + return this; + } + } else if (typeof options === "object" || !options) { + return this.each(function() { + if (!$.data(this, 'plugin_' + pluginName)) { + $.data(this, 'plugin_' + pluginName, new Plugin(this, options)); + } + }); + } + }; + } + +})(window, document); + +/** + * requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel + * @see: http://paulirish.com/2011/requestanimationframe-for-smart-animating/ + * @see: http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating + * @license: MIT license + */ +(function() { + var lastTime = 0; + var vendors = ['ms', 'moz', 'webkit', 'o']; + for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { + window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; + window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] + || window[vendors[x]+'CancelRequestAnimationFrame']; + } + + if (!window.requestAnimationFrame) + window.requestAnimationFrame = function(callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = window.setTimeout(function() { callback(currTime + timeToCall); }, + timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + + if (!window.cancelAnimationFrame) + window.cancelAnimationFrame = function(id) { + clearTimeout(id); + }; +}());