Merge branch 'master' of github.com:Retrospring/retrospring into optimize-js

This commit is contained in:
Yuki 2015-05-18 14:36:46 +05:30
commit 7a5b1cf1de
57 changed files with 940 additions and 110 deletions

View File

@ -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=

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,4 @@
@media (max-width: 768px) {
@import "mobile/settings";
@import "mobile/profile";
}

View File

@ -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;
}
}
}
}
}

View File

@ -0,0 +1,11 @@
#profile-header-media {
clear: both;
display: block;
.pull-left {
float: none !important;
clear: both;
img {
width: 100%
}
}
}

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,2 @@
module DiscoverHelper
end

View File

@ -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

View File

@ -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|

View File

@ -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|

View File

@ -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|

View File

@ -1,4 +1,4 @@
- provide(:title, "Sign up | #{APP_CONFIG['site_name']}")
- provide(:title, generate_title("Sign Up"))
.container
%h1 Sign up

View File

@ -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'

View File

@ -1,4 +1,4 @@
- provide(:title, "Unlock | #{APP_CONFIG['site_name']}")
- provide(:title, generate_title("Unlock"))
.container
%h1 Resend unlock instructions
= render 'layouts/messages'

View File

@ -0,0 +1,3 @@
.tab-pane.active.fade.in{role: "tabpanel", id: "answers"}
- answers.each do |a|
= render 'shared/answerbox', a: a

View File

@ -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

View File

@ -0,0 +1,3 @@
.tab-pane.fade{role: "tabpanel", id: "comments"}
- comments.each do |a|
= render 'shared/answerbox', a: a

View File

@ -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

View File

@ -0,0 +1,3 @@
.tab-pane.active.fade.in{role: "tabpanel", id: "new"}
- new.each do |user|
= render 'discover/userbox', u: user, type: "new"

View File

@ -0,0 +1,3 @@
.tab-pane.fade{role: "tabpanel", id: "questions"}
- questions.each do |q|
= render 'shared/question', q: q, type: "discover"

View File

@ -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")

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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"}

View File

@ -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

View File

@ -1,4 +1,4 @@
- provide(:title, "Moderation | #{APP_CONFIG['site_name']}")
- provide(:title, generate_title("Moderation"))
= render 'moderation/moderation_nav'
.container.j2-page
.row

View File

@ -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'

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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

View File

@ -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!

View File

@ -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'

View File

@ -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"}

View File

@ -1,4 +1,4 @@
- provide(:title, "Privacy Policy | #{APP_CONFIG['site_name']}")
- provide(:title, generate_title("Privacy Policy"))
.container
.panel.panel-default
.panel-body

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -1,4 +1,4 @@
.panel.panel-default.profile--panel
.panel.panel-default.profile--panel#profile-stats
.panel-heading
%h3.panel-title Stats
.panel-body

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);
};
}());