├── .gitignore ├── views ├── stylesheets │ ├── screen.scss │ └── partials │ │ ├── _bootstrap.scss │ │ ├── _main.scss │ │ ├── _type.scss │ │ ├── _reset.scss │ │ ├── _scaffolding.scss │ │ ├── _forms.scss │ │ ├── _preboot.scss │ │ └── _patterns.scss ├── _playlist_tracks.erb ├── layout.erb └── index.erb ├── screenshot1.png ├── screenshot2.png ├── public ├── images │ ├── call_button.jpg │ └── hang_up_button.jpeg └── js │ ├── jquery-spotifydata.js │ ├── application.js │ └── faye-browser-min.js ├── Gemfile ├── config.yml.example ├── config └── compass.rb ├── partials.rb ├── node ├── faye_server.coffee └── faye_server.js ├── config.ru ├── Gemfile.lock ├── app.rb └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | config.yml 2 | spotify_appkey.key 3 | -------------------------------------------------------------------------------- /views/stylesheets/screen.scss: -------------------------------------------------------------------------------- 1 | @import "partials/bootstrap"; 2 | @import "partials/main" -------------------------------------------------------------------------------- /screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamin/Spotify-Water-Cooler/HEAD/screenshot1.png -------------------------------------------------------------------------------- /screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamin/Spotify-Water-Cooler/HEAD/screenshot2.png -------------------------------------------------------------------------------- /public/images/call_button.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamin/Spotify-Water-Cooler/HEAD/public/images/call_button.jpg -------------------------------------------------------------------------------- /public/images/hang_up_button.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamin/Spotify-Water-Cooler/HEAD/public/images/hang_up_button.jpeg -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gem "sinatra", "1.3.2" 4 | gem "compass" 5 | gem 'twilio-ruby' 6 | gem 'hallon' 7 | gem 'racksh' 8 | gem 'faye' 9 | gem 'thin' -------------------------------------------------------------------------------- /config.yml.example: -------------------------------------------------------------------------------- 1 | HALLON_APPKEY: 'spotify_appkey.key' # Spotify API Key 2 | HALLON_USERNAME: 'harisamin' # Spotify username 3 | HALLON_PASSWORD: 'somepassword' # Spotify password 4 | 5 | TWILIO_ACCOUNT_SID: 'something' 6 | TWILIO_AUTH_TOKEN: 'something' 7 | TWILIO_APP_ID: 'something' 8 | TWILIO_NUMBER: '+18675309' # this might not be necessary -------------------------------------------------------------------------------- /views/_playlist_tracks.erb: -------------------------------------------------------------------------------- 1 | <% tracks.each do |t| %> 2 |
  • 3 |
    4 |
    5 | 6 |
    7 |
    8 | <%= t[:artist] %>: <%= t[:name] %> 9 |
    10 |
    11 |
  • 12 | <% end %> -------------------------------------------------------------------------------- /config/compass.rb: -------------------------------------------------------------------------------- 1 | # Complile stylesheets for production using: 2 | # compass compile --force 3 | 4 | if defined?(Sinatra) # Running within sinatra 5 | project_path = Sinatra::Application.root 6 | environment = :development 7 | else # command line tool 8 | css_dir = File.join 'public', 'stylesheets' 9 | relative_assets = true 10 | environment = :production 11 | end 12 | 13 | # Common Config 14 | sass_dir = File.join 'views', 'stylesheets' 15 | images_dir = File.join 'public', 'images' 16 | http_path = "/" 17 | http_images_path = "/images" 18 | http_stylesheets_path = "/stylesheets" 19 | output_style = :compressed -------------------------------------------------------------------------------- /views/stylesheets/partials/_bootstrap.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v1.1.1 3 | * 4 | * Copyright 2011 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | * 10 | * Converted to Sass by @johnwlong. 11 | * 12 | * Date: @DATE 13 | */ 14 | 15 | // CSS Reset 16 | @import "reset.scss"; 17 | 18 | // Core 19 | @import "preboot.scss"; 20 | @import "scaffolding.scss"; 21 | 22 | // Styled patterns and elements 23 | @import "type.scss"; 24 | @import "forms.scss"; 25 | //@import "tables.scss"; 26 | @import "patterns.scss"; 27 | //@import "datepicker.scss"; 28 | -------------------------------------------------------------------------------- /views/stylesheets/partials/_main.scss: -------------------------------------------------------------------------------- 1 | li.spotify-track { 2 | margin: 0 0 10px; 3 | cursor: pointer; 4 | } 5 | 6 | #chat-container ul { 7 | width: 100%; 8 | height: 200px; 9 | border: 1px solid #eeeeee; 10 | background: #fbfbfb; 11 | overflow-y: scroll; 12 | font-size: 12px; 13 | list-style-position: inside; 14 | list-style: none; 15 | padding: 0; 16 | margin: 0px; 17 | margin-bottom: 15px; 18 | } 19 | 20 | #chat-container ul li { margin: 0 0 10px; } 21 | 22 | #message{ width: 90%; } 23 | 24 | .track-container{ display: block; } 25 | 26 | .track-image{ float: left; } 27 | 28 | .track-info{ 29 | float: left; 30 | padding-top: 20px; 31 | padding-left: 20px; 32 | } -------------------------------------------------------------------------------- /partials.rb: -------------------------------------------------------------------------------- 1 | # stolen from http://github.com/cschneid/irclogger/blob/master/lib/partials.rb 2 | # and made a lot more robust by me 3 | # this implementation uses erb by default. if you want to use any other template mechanism 4 | # then replace `erb` on line 13 and line 17 with `haml` or whatever 5 | module Sinatra::Partials 6 | def partial(template, *args) 7 | template_array = template.to_s.split('/') 8 | template = template_array[0..-2].join('/') + "/_#{template_array[-1]}" 9 | options = args.last.is_a?(Hash) ? args.pop : {} 10 | options.merge!(:layout => false) 11 | locals = options[:locals] || {} 12 | if collection = options.delete(:collection) then 13 | collection.inject([]) do |buffer, member| 14 | buffer << erb(:"#{template}", options.merge(:layout => 15 | false, :locals => {template_array[-1].to_sym => member}.merge(locals))) 16 | end.join("\n") 17 | else 18 | erb(:"#{template}", options) 19 | end 20 | end 21 | end -------------------------------------------------------------------------------- /node/faye_server.coffee: -------------------------------------------------------------------------------- 1 | http = require 'http' 2 | faye = require 'faye' 3 | 4 | # ********* Utility Stuff *********** 5 | process.on 'uncaughtException', (err) -> 6 | console.log "Error: #{err}" 7 | # Handle non-Bayeux requests 8 | server = http.createServer (request, response) -> 9 | response.writeHead 200, {'Content-Type': 'text/plain'} 10 | response.write 'Hello, non-Bayeux request' 11 | response.end 12 | # Server logging 13 | serverLog = 14 | incoming: (message, callback) -> 15 | if message.channel == '/meta/subscribe' 16 | logWithTimeStamp "CLIENT SUBSCRIBED Client ID: #{message.clientId}" 17 | if message.channel.match(/\/users\/*/) 18 | logWithTimeStamp "USER MESSAGE ON CHANNEL: #{message.channel}" 19 | callback(message) 20 | 21 | logWithTimeStamp = (logMessage) -> 22 | timestampedMessage = "#{Date()} | #{logMessage}" 23 | console.log timestampedMessage 24 | # ************************************* 25 | 26 | bayeux = new faye.NodeAdapter( mount: '/faye', timeout: 45) 27 | 28 | bayeux.addExtension serverLog 29 | bayeux.attach server 30 | console.log "Starting Faye server on port 5222" 31 | server.listen 5222 -------------------------------------------------------------------------------- /views/layout.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Spotify Water Cooler 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 22 | 23 | 24 | 25 | 26 |
    27 | <%= yield %> 28 | 31 |
    32 | 33 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "bundler" 3 | Bundler.require 4 | 5 | require 'socket' 6 | require "./app.rb" 7 | 8 | 9 | def load_config(file) 10 | if File.exist?(file) 11 | yaml = YAML.load_file(file) 12 | ENV['HALLON_APPKEY'] = yaml['HALLON_APPKEY'] 13 | ENV['HALLON_USERNAME'] = yaml['HALLON_USERNAME'] 14 | ENV['HALLON_PASSWORD'] = yaml['HALLON_PASSWORD'] 15 | 16 | ENV['TWILIO_ACCOUNT_SID'] = yaml['TWILIO_ACCOUNT_SID'] 17 | ENV['TWILIO_AUTH_TOKEN'] = yaml['TWILIO_AUTH_TOKEN'] 18 | ENV['TWILIO_NUMBER'] = yaml['TWILIO_NUMBER'] 19 | ENV['TWILIO_APP_ID'] = yaml['TWILIO_APP_ID'] 20 | else 21 | return "Please setup config.yml" 22 | end 23 | end 24 | 25 | configure do 26 | load_config "./config.yml" 27 | 28 | HALLON_SESSION = Hallon::Session.initialize IO.read(ENV['HALLON_APPKEY']) do 29 | on(:log_message) do |message| 30 | puts "[LOG] #{message}" 31 | end 32 | end 33 | 34 | HALLON_SESSION.login!(ENV['HALLON_USERNAME'], ENV['HALLON_PASSWORD']) 35 | puts "Successfully logged in!" 36 | 37 | set :scss, {:style => :compact, :debug_info => false} 38 | Compass.add_project_configuration(File.join(Sinatra::Application.root, 'config', 'compass.rb')) 39 | end 40 | 41 | use Faye::RackAdapter, :mount => '/faye', :timeout => 45 42 | run MainApp -------------------------------------------------------------------------------- /node/faye_server.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var bayeux, faye, http, logWithTimeStamp, server, serverLog; 3 | http = require('http'); 4 | faye = require('faye'); 5 | process.on('uncaughtException', function(err) { 6 | return console.log("Error: " + err); 7 | }); 8 | server = http.createServer(function(request, response) { 9 | response.writeHead(200, { 10 | 'Content-Type': 'text/plain' 11 | }); 12 | response.write('Hello, non-Bayeux request'); 13 | return response.end; 14 | }); 15 | serverLog = { 16 | incoming: function(message, callback) { 17 | if (message.channel === '/meta/subscribe') { 18 | logWithTimeStamp("CLIENT SUBSCRIBED Client ID: " + message.clientId); 19 | } 20 | if (message.channel.match(/\/users\/*/)) { 21 | logWithTimeStamp("USER MESSAGE ON CHANNEL: " + message.channel); 22 | } 23 | return callback(message); 24 | } 25 | }; 26 | logWithTimeStamp = function(logMessage) { 27 | var timestampedMessage; 28 | timestampedMessage = "" + (Date()) + " | " + logMessage; 29 | return console.log(timestampedMessage); 30 | }; 31 | bayeux = new faye.NodeAdapter({ 32 | mount: '/faye', 33 | timeout: 45 34 | }); 35 | bayeux.addExtension(serverLog); 36 | bayeux.attach(server); 37 | console.log("Starting Faye server on port 5222"); 38 | server.listen(5222); 39 | }).call(this); 40 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | addressable (2.2.6) 5 | builder (3.0.0) 6 | chunky_png (1.2.5) 7 | compass (0.11.5) 8 | chunky_png (~> 1.2) 9 | fssm (>= 0.2.7) 10 | sass (~> 3.1) 11 | cookiejar (0.3.0) 12 | daemons (1.1.4) 13 | em-hiredis (0.1.0) 14 | hiredis (~> 0.3.0) 15 | em-http-request (0.3.0) 16 | addressable (>= 2.0.0) 17 | escape_utils 18 | eventmachine (>= 0.12.9) 19 | escape_utils (0.2.4) 20 | eventmachine (0.12.10) 21 | faye (0.7.1) 22 | cookiejar (>= 0.3.0) 23 | em-hiredis (>= 0.0.1) 24 | em-http-request (~> 0.3) 25 | eventmachine (~> 0.12.0) 26 | json (>= 1.0) 27 | rack (>= 1.0) 28 | thin (~> 1.2) 29 | ffi (1.0.11) 30 | fssm (0.2.7) 31 | hallon (0.12.0) 32 | ref (~> 1.0) 33 | spotify (~> 10.3.0) 34 | hiredis (0.3.2) 35 | json (1.6.5) 36 | jwt (0.1.4) 37 | json (>= 1.2.4) 38 | multi_json (1.0.4) 39 | rack (1.4.1) 40 | rack-protection (1.2.0) 41 | rack 42 | rack-test (0.6.1) 43 | rack (>= 1.0) 44 | racksh (0.9.10) 45 | rack (>= 1.0) 46 | rack-test (>= 0.5) 47 | ref (1.0.0) 48 | sass (3.1.13) 49 | sinatra (1.3.2) 50 | rack (>= 1.3.6, ~> 1.3) 51 | rack-protection (~> 1.2) 52 | tilt (>= 1.3.3, ~> 1.3) 53 | spotify (10.3.0) 54 | ffi (>= 1.0.11, ~> 1.0) 55 | thin (1.2.11) 56 | daemons (>= 1.0.9) 57 | eventmachine (>= 0.12.6) 58 | rack (>= 1.0.0) 59 | tilt (1.3.3) 60 | twilio-ruby (3.5.1) 61 | builder (>= 2.1.2) 62 | jwt (>= 0.1.2) 63 | multi_json (>= 1.0.3) 64 | 65 | PLATFORMS 66 | ruby 67 | 68 | DEPENDENCIES 69 | compass 70 | faye 71 | hallon 72 | racksh 73 | sinatra (= 1.3.2) 74 | thin 75 | twilio-ruby 76 | -------------------------------------------------------------------------------- /app.rb: -------------------------------------------------------------------------------- 1 | # Set up initializers 2 | Dir["./initialize/**/*.rb"].each { |int| require int } 3 | 4 | # Include all of the models 5 | Dir["./models/**/*.rb"].each { |model| require model } 6 | 7 | # Routes 8 | require 'erb' 9 | require './partials' 10 | 11 | class MainApp < Sinatra::Base 12 | helpers Sinatra::Partials 13 | 14 | get '/stylesheets/:name.css' do 15 | content_type 'text/css', :charset => 'utf-8' 16 | scss(:"stylesheets/#{params[:name]}") 17 | end 18 | 19 | get "/" do 20 | @host_with_port = request.host_with_port 21 | 22 | if ENV['TWILIO_ACCOUNT_SID'] && ENV['TWILIO_AUTH_TOKEN'] && ENV['TWILIO_APP_ID'] 23 | capability = Twilio::Util::Capability.new ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN'] 24 | capability.allow_client_outgoing ENV['TWILIO_APP_ID'] 25 | @token = capability.generate 26 | else 27 | @token = nil 28 | end 29 | @all_playlists = HALLON_SESSION.container.contents 30 | @playlist_tracks = @all_playlists.first.tracks.map do |t| 31 | { 32 | :playlist_index => t.index , 33 | :artist => t.artist.nil? ? nil : t.artist.name, 34 | :name => t.name, 35 | :image_url => t.album.nil? ? nil : t.album.cover(false).to_url 36 | } 37 | end.compact.sort_by{|h| h[:playlist_index]} 38 | erb :index 39 | end 40 | 41 | get "/add_track" do 42 | t = Hallon::Track.new( params[:spotify_url] ) 43 | playlist = HALLON_SESSION.container.contents.find{|p| p.name == params[:playlist_name]} 44 | playlist.insert(0, t) 45 | state_changed = false 46 | playlist.on(:playlist_state_changed) { state_changed = true } 47 | HALLON_SESSION.wait_for { state_changed && ! playlist.pending? } 48 | playlist_tracks = playlist.tracks.map do |t| 49 | { 50 | :playlist_index => t.index , 51 | :artist => t.artist.nil? ? nil : t.artist.name, 52 | :name => t.name, 53 | :image_url => t.album.nil? ? nil : t.album.cover(false).to_url 54 | } 55 | end.compact.sort_by{|h| h[:playlist_index]} 56 | 57 | partial(:playlist_tracks, :locals => {:tracks => playlist_tracks}) 58 | end 59 | 60 | get "/change_playlist" do 61 | playlist = HALLON_SESSION.container.contents.find{|p| p.name == params[:playlist_name]} 62 | playlist_tracks = playlist.tracks.map do |t| 63 | { 64 | :playlist_index => t.index , 65 | :artist => t.artist.nil? ? nil : t.artist.name, 66 | :name => t.name, 67 | :image_url => t.album.nil? ? nil : t.album.cover(false).to_url 68 | } 69 | end.compact.sort_by{|h| h[:playlist_index]} 70 | 71 | partial(:playlist_tracks, :locals => {:tracks => playlist_tracks}) 72 | end 73 | 74 | post '/twilio' do 75 | ' 76 | 77 | 1234 78 | 79 | ' 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /views/index.erb: -------------------------------------------------------------------------------- 1 |
    2 |

    Sptify Water Cooler

    3 |

    Killing productivity...one beat at a time!

    4 |

    5 |
    6 | 7 | 8 | 9 |
    10 |
    11 |

    Search For Track

    12 |

    13 |

    14 |
    15 |
    16 |
    17 | 18 | 19 |
    20 |
    21 |
    22 |
    23 | 24 |

    25 |
    26 | 27 |
    28 |

    Current Playlist

    29 | 30 |
    31 |
    32 |
    33 |
    34 | 39 |
    40 |
    41 |
    42 |
    43 | 44 |

    45 |

    48 |

    49 |
    50 |
    51 | 52 |
    53 |
    54 |

    Water Cooler

    55 | 56 |

    Talk

    57 | Call 58 | Call 59 | 60 |

    Chat

    61 |
    62 |
    63 |
    64 |
    65 |
    66 | 67 | 68 |
    69 |
    70 |
    71 |
    72 |
    73 | 74 | 84 | 85 |
    86 |
    -------------------------------------------------------------------------------- /public/js/jquery-spotifydata.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Spotify Metadata API Plugin v0.0.1 3 | * http://rds.github.com/ 4 | * 5 | * Copyright 2011 Richard Smith / May Contain Cocoa 6 | */ 7 | 8 | (function($) { 9 | function grepWithLimit(elems, callback, limit, inv) { 10 | var ret = [], retVal; 11 | inv = !!inv; 12 | 13 | // Go through the array, only saving the items 14 | // that pass the validator function 15 | for ( var i = 0, length = elems.length; i < length; i++ ) { 16 | retVal = !!callback( elems[ i ], i ); 17 | if ( inv !== retVal ) { 18 | ret.push( elems[ i ] ); 19 | } 20 | if (ret.length >= limit) { 21 | break 22 | } 23 | } 24 | 25 | return ret; 26 | } 27 | 28 | $.spotifydata = function(method) { 29 | var methods = spotifydataMethods; 30 | if (methods[method]) { 31 | return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); 32 | } else { 33 | $.error('jQuery.spotifydata#' + method + ' does not exist'); 34 | } 35 | } 36 | 37 | var spotifydataMethods = { 38 | filter: function(tracks, twoLetterCountryCode, limit) { 39 | if (tracks.tracks) { 40 | tracks = tracks.tracks; 41 | } 42 | if (!tracks[0]) { 43 | return $.error('No tracks provided'); 44 | } else if (!tracks[0]['album']['availability']) { 45 | return $.error('No tracks provided'); 46 | } 47 | if (limit) { 48 | limit = 100; 49 | } 50 | if (twoLetterCountryCode == 'UK') { 51 | twoLetterCountryCode = 'GB'; 52 | } 53 | var regex = new RegExp('('+twoLetterCountryCode+'|worldwide)', 'g') 54 | tracks = grepWithLimit(tracks, function(track, i) { 55 | return track.album.availability.territories.search(regex) >= 0; 56 | }, 1); 57 | return tracks; 58 | }, 59 | 60 | lookup: function(uri, options, callback) { 61 | var data = { uri: uri }; 62 | $.extend(options, {}); 63 | if (options.extras) { 64 | data['extras'] = options.extras; 65 | } 66 | return $.get('http://ws.spotify.com/lookup/1/', data, callback, options.dataType || 'json'); 67 | }, 68 | 69 | search: function(query, options, callback) { 70 | if (typeof options === 'function') { 71 | callback = options; 72 | } 73 | if (typeof options !== 'objet') { 74 | options = {} 75 | } 76 | if ($.isArray(query)) { 77 | return $.each(query, function(i, query) { 78 | return spotifydataMethods.search(query, options, callback) 79 | }) 80 | } else { 81 | var data = { q: query, page: options.page || 1 }; 82 | return $.get('http://ws.spotify.com/search/1/' + (options.method || 'track'), data, callback, options.dataType || 'json') 83 | } 84 | }, 85 | 86 | track: function(query, options, callback) { 87 | return spotifydataMethods.search(query, options, callback); 88 | }, 89 | 90 | artist: function(query, options, callback) { 91 | return spotifydataMethods.search(query, $.extend(data, { method: 'artist' }), callback); 92 | }, 93 | 94 | album: function(query, options, callback) { 95 | return spotifydataMethods.search(queryquery, $.extend(options, { method: 'album' }), callback); 96 | } 97 | } 98 | })(jQuery); -------------------------------------------------------------------------------- /views/stylesheets/partials/_type.scss: -------------------------------------------------------------------------------- 1 | /* Typography.scss 2 | * Headings, body text, lists, code, and more for a versatile and durable typography system 3 | * ---------------------------------------------------------------------------------------- */ 4 | 5 | 6 | // BODY TEXT 7 | // --------- 8 | 9 | p { 10 | @include shorthand-font(normal,$basefont,$baseline); 11 | margin-bottom: $baseline / 2; 12 | small { 13 | font-size: $basefont - 2; 14 | color: $grayLight; 15 | } 16 | } 17 | 18 | 19 | // HEADINGS 20 | // -------- 21 | 22 | h1, h2, h3, h4, h5, h6 { 23 | font-weight: bold; 24 | color: $grayDark; 25 | small { 26 | color: $grayLight; 27 | } 28 | } 29 | h1 { 30 | margin-bottom: $baseline; 31 | font-size: 30px; 32 | line-height: $baseline * 2; 33 | small { 34 | font-size: 18px; 35 | } 36 | } 37 | h2 { 38 | font-size: 24px; 39 | line-height: $baseline * 2; 40 | small { 41 | font-size: 14px; 42 | } 43 | } 44 | h3, h4, h5, h6 { 45 | line-height: $baseline * 2; 46 | } 47 | h3 { 48 | font-size: 18px; 49 | small { 50 | font-size: 14px; 51 | } 52 | } 53 | h4 { 54 | font-size: 16px; 55 | small { 56 | font-size: 12px; 57 | } 58 | } 59 | h5 { 60 | font-size: 14px; 61 | } 62 | h6 { 63 | font-size: 13px; 64 | color: $grayLight; 65 | text-transform: uppercase; 66 | } 67 | 68 | 69 | // COLORS 70 | // ------ 71 | 72 | // Unordered and Ordered lists 73 | ul, ol { 74 | margin: 0 0 $baseline 25px; 75 | } 76 | ul ul, 77 | ul ol, 78 | ol ol, 79 | ol ul { 80 | margin-bottom: 0; 81 | } 82 | ul { 83 | list-style: disc; 84 | } 85 | ol { 86 | list-style: decimal; 87 | } 88 | li { 89 | line-height: $baseline; 90 | color: $gray; 91 | } 92 | ul.unstyled { 93 | list-style: none; 94 | margin-left: 0; 95 | } 96 | 97 | // Description Lists 98 | dl { 99 | margin-bottom: $baseline; 100 | dt, dd { 101 | line-height: $baseline; 102 | } 103 | dt { 104 | font-weight: bold; 105 | } 106 | dd { 107 | margin-left: $baseline / 2; 108 | } 109 | } 110 | 111 | // MISC 112 | // ---- 113 | 114 | // Horizontal rules 115 | hr { 116 | margin: 0 0 19px; 117 | border: 0; 118 | border-bottom: 1px solid #eee; 119 | } 120 | 121 | // Emphasis 122 | strong { 123 | font-style: inherit; 124 | font-weight: bold; 125 | line-height: inherit; 126 | } 127 | em { 128 | font-style: italic; 129 | font-weight: inherit; 130 | line-height: inherit; 131 | } 132 | .muted { 133 | color: $grayLight; 134 | } 135 | 136 | // Blockquotes 137 | blockquote { 138 | margin-bottom: $baseline; 139 | border-left: 5px solid #eee; 140 | padding-left: 15px; 141 | p { 142 | @include shorthand-font(300,14px,$baseline); 143 | margin-bottom: 0; 144 | } 145 | small { 146 | display: block; 147 | @include shorthand-font(300,12px,$baseline); 148 | color: $grayLight; 149 | &:before { 150 | content: '\2014 \00A0'; 151 | } 152 | } 153 | } 154 | 155 | // Addresses 156 | address { 157 | display: block; 158 | line-height: $baseline; 159 | margin-bottom: $baseline; 160 | } 161 | 162 | // Inline and block code styles 163 | code, pre { 164 | padding: 0 3px 2px; 165 | font-family: Monaco, Andale Mono, Courier New, monospace; 166 | font-size: 12px; 167 | @include border-radius(3px); 168 | } 169 | code { 170 | background-color: lighten($orange, 40%); 171 | color: rgba(0,0,0,.75); 172 | padding: 1px 3px; 173 | } 174 | pre { 175 | background-color: #f5f5f5; 176 | display: block; 177 | padding: $baseline - 1; 178 | margin: 0 0 $baseline; 179 | line-height: $baseline; 180 | font-size: 12px; 181 | border: 1px solid #ccc; 182 | border: 1px solid rgba(0,0,0,.15); 183 | @include border-radius(3px); 184 | white-space: pre; 185 | white-space: pre-wrap; 186 | word-wrap: break-word; 187 | 188 | } 189 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spotify Water Cooler - Killing Productivity...one beat at a time! 2 | 3 | 4 | ## Winner of API Hackday NYC 2012 ## 5 | 6 | ![ApiHackday logo](http://209.114.47.122/wp-content/images/apihackday_logo.png) 7 | 8 | Do you love Spotify? Do you have Spotify running on a computer in your office or shared space that blasts awesome music all the time? Are you tired of having to physically or remotely go to the computer to manage the playlists on Spotify? Well fear not... Spotify Water Cooler is here! 9 | 10 | Search songs on Spotify and add them to your playlists being played! But this is not just to get your groove on, its a water cooler, chat with your friends, colleagues, frienemies, associates, & fellow sociopaths while you groove and choose to to groove to. Chat either with the built in chat or use voice chat in your browser powered by the awesome Twilio Voice Chat api all in your browser. 11 | 12 | Hmmm...joined the chat a bit late and wanna know what people were talking about? TOO BAD! Real life water coolers don't have chat logs and neither do we. Don't worry, no one is talking about you behind your back *wink wink*. 13 | 14 | ## Installation and usage ## 15 | 16 | These instructions have been written for OS X. 17 | 18 | ### Pre-requisites ### 19 | * [Spotify API Key](http://developer.spotify.com/en/libspotify/overview/) Download libspotify and your app key 20 | * [TWILIO Client API Key](http://www.twilio.com/api/client) Sign up, its SUPER easy! 21 | * [Ruby](http://www.ruby-lang.org/) 1.9. Use [RVM](http://rvm.beginrescueend.com/) to manage your Ruby installations. It's good. 22 | * [Rubygems](http://rubygems.org/) 23 | * [Git](http://git-scm.com/) 24 | * The [Bundler](http://rubygems.org/gems/bundler) gem. Install with 'gem install bundler'. 25 | 26 | ### Install dependencies ### 27 | 28 | Use Bundler to install project dependencies for you: 29 | 30 | $ bundle install 31 | 32 | This will install gems and various other dependencies if not already on your system. It will also create a Gemfile.lock file which will ensure that dependencies do not change unless you explicitly rerun `bundle install` again. 33 | 34 | 35 | ### Configure ### 36 | 37 | Copy the example config file: 38 | 39 | $ cp config.yml.example config.yml 40 | 41 | Enter your Spotify and Twilio credentials in config.yml. Make sure to place your Spotify key (e.g. spotify_appkey.key) in the root folder of the project. 42 | 43 | ### Run locally ### 44 | 45 | To run the application: 46 | 47 | $ thin start 48 | 49 | The app will be viewable at `http://localhost:3000` 50 | 51 | ## Screenshots ## 52 | 53 | ![Screenshot 1](https://github.com/hamin/Spotify-Water-Cooler/raw/master/screenshot1.png) 54 | 55 | ![Screenshot 2](https://github.com/hamin/Spotify-Water-Cooler/raw/master/screenshot2.png) 56 | 57 | ## Powered By ## 58 | ![Spotify logo](http://blogs.channel4.com/benjamin-cohen-on-technology/files/2010/10/spotify-logo-1.png) 59 | 60 | ![Twilio logo](http://www.twilio.com/packages/company/img/logos_downloadable_round.png) 61 | 62 | ### License ### 63 | 64 | (The MIT License) 65 | 66 | Copyright (c) 2012 Haris Amin 67 | 68 | Permission is hereby granted, free of charge, to any person obtaining a copy of 69 | this software and associated documentation files (the 'Software'), to deal in 70 | the Software without restriction, including without limitation the rights to use, 71 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 72 | Software, and to permit persons to whom the Software is furnished to do so, 73 | subject to the following conditions: 74 | 75 | The above copyright notice and this permission notice shall be included in all 76 | copies or substantial portions of the Software. 77 | 78 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 79 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 80 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 81 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 82 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 83 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /public/js/application.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | 3 | 4 | $('#search-form').bind('submit',function(event) { 5 | event.preventDefault(); 6 | 7 | function trimCrap(line) { 8 | return (typeof line == 'string') ? $.trim(line).replace(/ \W /gi, ' ').replace(/\s+/gi, ' ') : line; 9 | }; 10 | 11 | tracks = $.map($.unique($('#search').val().split('\n')), function(track) { 12 | return (/\w/).exec(track) ? trimCrap(track) : null; 13 | }); 14 | 15 | var result, results = [], resultsCount = 0, foundResultsCount = 0, track, spotifyTracks; 16 | 17 | $.spotifydata('search', tracks, function lambda(data) { 18 | result = {}; 19 | result['info'] = data.info; 20 | if (data.tracks.length > 0) { 21 | apiResults = $.spotifydata('filter', data.tracks, 'US', 10); 22 | result['track'] = apiResults[0]; 23 | spotifyTracks = data.tracks.slice(0,9); 24 | 25 | if (!result.track) { 26 | result['unavailableTrack'] = data.tracks[0]; 27 | } 28 | result['result'] = data.tracks.indexOf(result.track) + 1; 29 | } else { 30 | var altQuery = result.info.query.replace(/(\(|\[).+(\)|\])/,''); 31 | 32 | if (altQuery != result.info.query) { 33 | $.spotifydata('search', altQuery, lambda); 34 | } 35 | } 36 | results[resultsCount++] = result; 37 | result['found'] = !!result.track; 38 | if (result.track) { 39 | foundResultsCount++; 40 | }; 41 | $("#search-results").html(''); 42 | 43 | $.each(spotifyTracks, function(index, val) { 44 | html = "
  • " + val.artists[0].name + ": " + val.name + "
  • "; 45 | $("#search-results").append(html); 46 | }); // End of inserting tracks html 47 | }); // End of spotify search 48 | }); // End of search-form bind 49 | 50 | $("#search-results li.spotify-track").live("click", function(event) { 51 | playlistName = $("#current-playlist").val(); 52 | $.get('/add_track', {spotify_url: $(this).attr('id'), playlist_name: playlistName}, function(data, textStatus, xhr) { 53 | //optional stuff to do after success 54 | $("#playlist-tracks").html(data); 55 | }); 56 | }); 57 | 58 | 59 | // Select Playlist 60 | $("#current-playlist").bind('change', function(event) { 61 | // Act on the event 62 | playlistName = $(this).val(); 63 | 64 | $.get('/change_playlist', {playlist_name: playlistName}, function(data, textStatus, xhr) { 65 | //optional stuff to do after success 66 | $("#playlist-tracks").html(data); 67 | }); 68 | }); 69 | 70 | // Chat Stuff 71 | var conversation = $("#chat-container ul"); 72 | var nickname = null; 73 | var subscription = client.subscribe('/chat', function(message) { 74 | conversation.append('
  • (' + message.cleanTime + ') ' + message.nick + ' — ' + message.msg + '
  • '); 75 | conversation.scrollTop(999999); // Hack: autoscroll down to last message 76 | }); 77 | 78 | $('#login-form').bind('submit',function(event) { 79 | event.preventDefault(); 80 | nickname = $('#login-nick').val(); 81 | $('#chat-login').hide(); 82 | $('#chat-container').show(); 83 | // $('#what-to-do').removeClass('center'); 84 | }); 85 | 86 | $('#chat-form').bind('submit',function(event) { 87 | event.preventDefault(); 88 | picture_id = $('.photo').attr('id'); 89 | date = new Date(); 90 | cleanDate = date.getHours() + ':' + date.getMinutes(); 91 | msgDate = date.getHours() + ':' + date.getMinutes(); 92 | message = { picture_id: picture_id, nick: nickname, cleanTime: cleanDate, time: date, msg: $('#message').val() }; 93 | client.publish('/chat', message); 94 | $('#message').val('').focus(); 95 | }); 96 | 97 | // Twilio Stuff 98 | var connection; 99 | function call() { 100 | connection = Twilio.Device.connect(); 101 | } 102 | 103 | function hangup() { 104 | connection.disconnect(); 105 | } 106 | 107 | $('#hangup').hide(); 108 | 109 | $('#call').click(function(e) { 110 | e.preventDefault(); 111 | call(); 112 | $(this).hide(); 113 | $('#hangup').show(); 114 | $('#chat-login grid_6.alpha.right').hide(); 115 | }); 116 | 117 | $('#hangup').click(function(e) { 118 | e.preventDefault(); 119 | hangup(); 120 | $(this).hide(); 121 | $('#call').show(); 122 | }); 123 | 124 | 125 | }); -------------------------------------------------------------------------------- /views/stylesheets/partials/_reset.scss: -------------------------------------------------------------------------------- 1 | /* Reset.scss 2 | * Props to Eric Meyer (meyerweb.com) for his CSS reset file. We're using an adapted version here that cuts out some of the reset HTML elements we will never need here (i.e., dfn, samp, etc). 3 | * ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */ 4 | 5 | 6 | // ERIC MEYER RESET 7 | // -------------------------------------------------- 8 | 9 | html, body { margin: 0; padding: 0; } 10 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, cite, code, del, dfn, em, img, q, s, samp, small, strike, strong, sub, sup, tt, var, dd, dl, dt, li, ol, ul, fieldset, form, label, legend, button, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; font-weight: normal; font-style: normal; font-size: 100%; line-height: 1; font-family: inherit; } 11 | table { border-collapse: collapse; border-spacing: 0; } 12 | ol, ul { list-style: none; } 13 | q:before, q:after, blockquote:before, blockquote:after { content: ""; } 14 | 15 | 16 | // Normalize.css 17 | // Pulling in select resets form the normalize.css project 18 | // -------------------------------------------------- 19 | 20 | // Display in IE6-9 and FF3 21 | // ------------------------- 22 | // Source: http://github.com/necolas/normalize.css 23 | html { 24 | overflow-y: scroll; 25 | font-size: 100%; 26 | -webkit-text-size-adjust: 100%; 27 | -ms-text-size-adjust: 100%; 28 | } 29 | // Focus states 30 | a:focus { 31 | outline: thin dotted; 32 | } 33 | 34 | // Display in IE6-9 and FF3 35 | // ------------------------- 36 | // Source: http://github.com/necolas/normalize.css 37 | article, 38 | aside, 39 | details, 40 | figcaption, 41 | figure, 42 | footer, 43 | header, 44 | hgroup, 45 | nav, 46 | section { 47 | display: block; 48 | } 49 | 50 | // Display block in IE6-9 and FF3 51 | // ------------------------- 52 | // Source: http://github.com/necolas/normalize.css 53 | audio, 54 | canvas, 55 | video { 56 | display: inline-block; 57 | *display: inline; 58 | *zoom: 1; 59 | } 60 | 61 | // Prevents modern browsers from displaying 'audio' without controls 62 | // ------------------------- 63 | // Source: http://github.com/necolas/normalize.css 64 | audio:not([controls]) { 65 | display: none; 66 | } 67 | 68 | // Prevents sub and sup affecting line-height in all browsers 69 | // ------------------------- 70 | // Source: http://github.com/necolas/normalize.css 71 | sub, 72 | sup { 73 | font-size: 75%; 74 | line-height: 0; 75 | position: relative; 76 | vertical-align: baseline; 77 | } 78 | sup { 79 | top: -0.5em; 80 | } 81 | sub { 82 | bottom: -0.25em; 83 | } 84 | 85 | // Img border in a's and image quality 86 | // ------------------------- 87 | // Source: http://github.com/necolas/normalize.css 88 | img { 89 | border: 0; 90 | -ms-interpolation-mode: bicubic; 91 | } 92 | 93 | // Forms 94 | // ------------------------- 95 | // Source: http://github.com/necolas/normalize.css 96 | 97 | // Font size in all browsers, margin changes, misc consistency 98 | button, 99 | input, 100 | select, 101 | textarea { 102 | font-size: 100%; 103 | margin: 0; 104 | vertical-align: baseline; 105 | *vertical-align: middle; 106 | } 107 | button, 108 | input { 109 | line-height: normal; // FF3/4 have !important on line-height in UA stylesheet 110 | *overflow: visible; // Inner spacing ie IE6/7 111 | } 112 | button::-moz-focus-inner, 113 | input::-moz-focus-inner { // Inner padding and border oddities in FF3/4 114 | border: 0; 115 | padding: 0; 116 | } 117 | button, 118 | input[type="button"], 119 | input[type="reset"], 120 | input[type="submit"] { 121 | cursor: pointer; // Cursors on all buttons applied consistently 122 | -webkit-appearance: button; // Style clicable inputs in iOS 123 | } 124 | input[type="search"] { // Appearance in Safari/Chrome 125 | -webkit-appearance: textfield; 126 | -webkit-box-sizing: content-box; 127 | -moz-box-sizing: content-box; 128 | box-sizing: content-box; 129 | } 130 | input[type="search"]::-webkit-search-decoration { 131 | -webkit-appearance: none; // Inner-padding issues in Chrome OSX, Safari 5 132 | } 133 | textarea { 134 | overflow: auto; // Remove vertical scrollbar in IE6-9 135 | vertical-align: top; // Readability and alignment cross-browser 136 | } 137 | 138 | // Tables 139 | // ------------------------- 140 | // Source: http://github.com/necolas/normalize.css 141 | 142 | // Remove spacing between table cells 143 | table { 144 | border-collapse: collapse; 145 | border-spacing: 0; 146 | } -------------------------------------------------------------------------------- /views/stylesheets/partials/_scaffolding.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Scaffolding.scss 3 | * Basic and global styles for generating a grid system, structural layout, and page templates 4 | * ------------------------------------------------------------------------------------------- */ 5 | 6 | // GRID SYSTEM 7 | // ----------- 8 | 9 | .row { 10 | @include clearfix(); 11 | margin-left: -20px; 12 | 13 | // Default columns 14 | .span1, 15 | .span2, 16 | .span3, 17 | .span4, 18 | .span5, 19 | .span6, 20 | .span7, 21 | .span8, 22 | .span9, 23 | .span10, 24 | .span11, 25 | .span12, 26 | .span13, 27 | .span14, 28 | .span15, 29 | .span16 { 30 | display: inline; 31 | float: left; 32 | margin-left: 20px; 33 | } 34 | 35 | // Default columns 36 | .span1 { @include columns(1); } 37 | .span2 { @include columns(2); } 38 | .span3 { @include columns(3); } 39 | .span4 { @include columns(4); } 40 | .span5 { @include columns(5); } 41 | .span6 { @include columns(6); } 42 | .span7 { @include columns(7); } 43 | .span8 { @include columns(8); } 44 | .span9 { @include columns(9); } 45 | .span10 { @include columns(10); } 46 | .span11 { @include columns(11); } 47 | .span12 { @include columns(12); } 48 | .span13 { @include columns(13); } 49 | .span14 { @include columns(14); } 50 | .span15 { @include columns(15); } 51 | .span16 { @include columns(16); } 52 | 53 | // Offset column options 54 | .offset1 { @include offset(1); } 55 | .offset2 { @include offset(2); } 56 | .offset3 { @include offset(3); } 57 | .offset4 { @include offset(4); } 58 | .offset5 { @include offset(5); } 59 | .offset6 { @include offset(6); } 60 | .offset7 { @include offset(7); } 61 | .offset8 { @include offset(8); } 62 | .offset9 { @include offset(9); } 63 | .offset10 { @include offset(10); } 64 | .offset11 { @include offset(11); } 65 | .offset12 { @include offset(12); } 66 | } 67 | 68 | 69 | // STRUCTURAL LAYOUT 70 | // ----------------- 71 | 72 | html, body { 73 | background-color: #fff; 74 | } 75 | body { 76 | margin: 0; 77 | @include sans-serif-font(normal,$basefont,$baseline); 78 | color: $gray; 79 | text-rendering: optimizeLegibility; 80 | } 81 | 82 | // Container (centered, fixed-width layouts) 83 | .container { 84 | width: 940px; 85 | margin: 0 auto; 86 | } 87 | 88 | // Fluid layouts (left aligned, with sidebar, min- & max-width content) 89 | .container-fluid { 90 | padding: 0 20px; 91 | @include clearfix(); 92 | .sidebar { 93 | float: left; 94 | width: 220px; 95 | } 96 | .content { 97 | min-width: 700px; 98 | max-width: 1180px; 99 | margin-left: 240px; 100 | } 101 | } 102 | 103 | 104 | // BASE STYLES 105 | // ----------- 106 | 107 | // Links 108 | a { 109 | color: $linkColor; 110 | text-decoration: none; 111 | line-height: inherit; 112 | font-weight: inherit; 113 | &:hover { 114 | color: $linkColorHover; 115 | text-decoration: underline; 116 | } 117 | } 118 | 119 | // Buttons 120 | .btn { 121 | display: inline-block; 122 | @include vertical-three-colors-gradient(#fff, #fff, 0.25, darken(#fff, 10%)); 123 | padding: 4px 14px; 124 | text-shadow: 0 1px 1px rgba(255,255,255,.75); 125 | color: #333; 126 | font-size: 13px; 127 | line-height: $baseline; 128 | border: 1px solid #ccc; 129 | border-bottom-color: #bbb; 130 | @include border-radius(4px); 131 | $shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05); 132 | @include box-shadow($shadow); 133 | &:hover { 134 | background-position: 0 -15px; 135 | color: #333; 136 | text-decoration: none; 137 | } 138 | } 139 | .primary { 140 | @include vertical-gradient(#049CDB, #0064CD); 141 | color: #fff; 142 | text-shadow: 0 -1px 0 rgba(0,0,0,.25); 143 | border: 1px solid darken(#0064CD, 10%); 144 | border-bottom-color: darken(#0064CD, 15%); 145 | &:hover { 146 | color: #fff; 147 | } 148 | } 149 | 150 | .btn { 151 | //.button(#1174C6); 152 | @include transition(.1s linear all); 153 | &.primary { 154 | //@include vertical-gradient($blue, $blueDark); 155 | color: #fff; 156 | text-shadow: 0 -1px 0 rgba(0,0,0,.25); 157 | border-color: $blueDark $blueDark darken($blueDark, 15%); 158 | border-color: rgba(0,0,0,.1) rgba(0,0,0,.1) fadein(rgba(0,0,0,.1), 15%); 159 | &:hover { 160 | color: #fff; 161 | } 162 | } 163 | &.large { 164 | font-size: 16px; 165 | line-height: 28px; 166 | @include border-radius(6px); 167 | } 168 | &.small { 169 | padding-right: 9px; 170 | padding-left: 9px; 171 | font-size: 11px; 172 | } 173 | &.disabled { 174 | background-image: none; 175 | @include opacity(65); 176 | cursor: default; 177 | @include box-shadow(none); 178 | } 179 | 180 | // this can't be included with the .disabled def because IE8 and below will drop it ;_; 181 | &:disabled { 182 | background-image: none; 183 | @include opacity(65); 184 | cursor: default; 185 | @include box-shadow(none); 186 | &.primary { 187 | color: #fff; 188 | } 189 | } 190 | &:active { 191 | $shadow: inset 0 3px 7px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05); 192 | @include box-shadow($shadow); 193 | } 194 | } 195 | 196 | // Help Firefox not be a jerk about adding extra padding to buttons 197 | button.btn, 198 | input[type=submit].btn { 199 | &::-moz-focus-inner { 200 | padding: 0; 201 | border: 0; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /views/stylesheets/partials/_forms.scss: -------------------------------------------------------------------------------- 1 | /* Forms.scss 2 | * Base styles for various input types, form layouts, and states 3 | * ------------------------------------------------------------- */ 4 | 5 | 6 | // FORM STYLES 7 | // ----------- 8 | 9 | form { 10 | margin-bottom: $baseline; 11 | } 12 | 13 | // Groups of fields with labels on top (legends) 14 | fieldset { 15 | margin-bottom: $baseline; 16 | padding-top: $baseline; 17 | legend { 18 | display: block; 19 | margin-left: 150px; 20 | font-size: 20px; 21 | line-height: 1; 22 | *margin: 0 0 5px 145px; /* IE6-7 */ 23 | *line-height: 1.5; /* IE6-7 */ 24 | color: $grayDark; 25 | } 26 | } 27 | 28 | // Parent element that clears floats and wraps labels and fields together 29 | .clearfix { 30 | margin-bottom: $baseline; 31 | } 32 | 33 | // Set font for forms 34 | label, 35 | input, 36 | select, 37 | textarea { 38 | @include sans-serif-font(normal,13px,normal); 39 | } 40 | 41 | // Float labels left 42 | label { 43 | padding-top: 6px; 44 | font-size: 13px; 45 | line-height: 18px; 46 | float: left; 47 | width: 130px; 48 | text-align: right; 49 | color: $grayDark; 50 | } 51 | 52 | // Shift over the inside div to align all label's relevant content 53 | div.input { 54 | margin-left: 150px; 55 | } 56 | 57 | // Checkboxs and radio buttons 58 | input[type=checkbox], 59 | input[type=radio] { 60 | cursor: pointer; 61 | } 62 | 63 | // Inputs, Textareas, Selects 64 | input[type=text], 65 | input[type=password], 66 | textarea, 67 | select, 68 | .uneditable-input { 69 | display: inline-block; 70 | width: 210px; 71 | padding: 4px; 72 | font-size: 13px; 73 | line-height: $baseline; 74 | height: $baseline; 75 | color: $gray; 76 | border: 1px solid #ccc; 77 | @include border-radius(3px); 78 | } 79 | select, 80 | input[type=file] { 81 | height: $baseline * 1.5; 82 | line-height: $baseline * 1.5; 83 | } 84 | textarea { 85 | height: auto; 86 | } 87 | .uneditable-input { 88 | background-color: #eee; 89 | display: block; 90 | border-color: #ccc; 91 | @include box-shadow(inset 0 1px 2px rgba(0,0,0,.075)); 92 | } 93 | 94 | // Placeholder text gets special styles; can't be bundled together though for some reason 95 | :-moz-placeholder { 96 | color: $grayLight; 97 | } 98 | ::-webkit-input-placeholder { 99 | color: $grayLight; 100 | } 101 | 102 | // Focus states 103 | input[type=text], 104 | input[type=password], 105 | select, textarea { 106 | $transition: border linear .2s, box-shadow linear .2s; 107 | @include transition($transition); 108 | @include box-shadow(inset 0 1px 3px rgba(0,0,0,.1)); 109 | } 110 | input[type=text]:focus, 111 | input[type=password]:focus, 112 | textarea:focus { 113 | outline: none; 114 | border-color: rgba(82,168,236,.8); 115 | $shadow: inset 0 1px 3px rgba(0,0,0,.1), 0 0 8px rgba(82,168,236,.6); 116 | @include box-shadow($shadow); 117 | } 118 | 119 | // Error styles 120 | form div.error { 121 | background: lighten($red, 57%); 122 | padding: 10px 0; 123 | margin: -10px 0 10px; 124 | @include border-radius(4px); 125 | $error-text: desaturate(lighten($red, 25%), 25%); 126 | > label, 127 | span.help-inline, 128 | span.help-block { 129 | color: $red; 130 | } 131 | input[type=text], 132 | input[type=password], 133 | textarea { 134 | border-color: $error-text; 135 | @include box-shadow(0 0 3px rgba(171,41,32,.25)); 136 | &:focus { 137 | border-color: darken($error-text, 10%); 138 | @include box-shadow(0 0 6px rgba(171,41,32,.5)); 139 | } 140 | } 141 | .input-prepend, 142 | .input-append { 143 | span.add-on { 144 | background: lighten($red, 50%); 145 | border-color: $error-text; 146 | color: darken($error-text, 10%); 147 | } 148 | } 149 | } 150 | 151 | // Form element sizes 152 | .input-mini, input.mini, textarea.mini, select.mini { 153 | width: 60px; 154 | } 155 | .input-small, input.small, textarea.small, select.small { 156 | width: 90px; 157 | } 158 | .input-medium, input.medium, textarea.medium, select.medium { 159 | width: 150px; 160 | } 161 | .input-large, input.large, textarea.large, select.large { 162 | width: 210px; 163 | } 164 | .input-xlarge, input.xlarge, textarea.xlarge, select.xlarge { 165 | width: 270px; 166 | } 167 | .input-xxlarge, input.xxlarge, textarea.xxlarge, select.xxlarge { 168 | width: 530px; 169 | } 170 | textarea.xxlarge { 171 | overflow-y: scroll; 172 | } 173 | 174 | // Turn off focus for disabled (read-only) form elements 175 | input[readonly]:focus, 176 | textarea[readonly]:focus, 177 | input.disabled { 178 | background: #f5f5f5; 179 | border-color: #ddd; 180 | @include box-shadow(none); 181 | } 182 | 183 | // Actions (the buttons) 184 | .actions { 185 | background: #f5f5f5; 186 | margin-top: $baseline; 187 | margin-bottom: $baseline; 188 | padding: ($baseline - 1) 20px $baseline 150px; 189 | border-top: 1px solid #ddd; 190 | @include border-radius(0 0 3px 3px); 191 | .secondary-action { 192 | float: right; 193 | a { 194 | line-height: 30px; 195 | &:hover { 196 | text-decoration: underline; 197 | } 198 | } 199 | } 200 | } 201 | 202 | // Help Text 203 | .help-inline, 204 | .help-block { 205 | font-size: 12px; 206 | line-height: $baseline; 207 | color: $grayLight; 208 | } 209 | .help-inline { 210 | padding-left: 5px; 211 | *position: relative; /* IE6-7 */ 212 | *top: -5px; /* IE6-7 */ 213 | } 214 | 215 | // Big blocks of help text 216 | .help-block { 217 | display: block; 218 | max-width: 600px; 219 | } 220 | 221 | // Inline Fields (input fields that appear as inline objects 222 | .inline-inputs { 223 | color: $gray; 224 | span, input[type=text] { 225 | display: inline-block; 226 | } 227 | input.mini { 228 | width: 60px; 229 | } 230 | input.small { 231 | width: 90px; 232 | } 233 | span { 234 | padding: 0 2px 0 1px; 235 | } 236 | } 237 | 238 | // Allow us to put symbols and text within the input field for a cleaner look 239 | .input-prepend, 240 | .input-append { 241 | input[type=text], 242 | input[type=password] { 243 | @include border-radius(0 3px 3px 0); 244 | } 245 | .add-on { 246 | background: #f5f5f5; 247 | float: left; 248 | display: block; 249 | width: auto; 250 | min-width: 16px; 251 | padding: 4px 4px 4px 5px; 252 | color: $grayLight; 253 | font-weight: normal; 254 | line-height: 18px; 255 | height: 18px; 256 | text-align: center; 257 | text-shadow: 0 1px 0 #fff; 258 | border: 1px solid #ccc; 259 | border-right-width: 0; 260 | @include border-radius(3px 0 0 3px); 261 | } 262 | .active { 263 | background: lighten($green, 30); 264 | border-color: $green; 265 | } 266 | } 267 | .input-prepend { 268 | .add-on { 269 | *margin-top: 1px; /* IE6-7 */ 270 | } 271 | } 272 | .input-append { 273 | input[type=text], 274 | input[type=password] { 275 | float: left; 276 | @include border-radius(3px 0 0 3px); 277 | } 278 | .add-on { 279 | @include border-radius(0 3px 3px 0); 280 | border-right-width: 1px; 281 | border-left-width: 0; 282 | } 283 | } 284 | 285 | // Stacked options for forms (radio buttons or checkboxes) 286 | .inputs-list { 287 | margin: 0 0 5px; 288 | width: 100%; 289 | li { 290 | display: block; 291 | padding: 0; 292 | width: 100%; 293 | label { 294 | display: block; 295 | float: none; 296 | width: auto; 297 | padding: 0; 298 | line-height: $baseline; 299 | text-align: left; 300 | white-space: normal; 301 | strong { 302 | color: $gray; 303 | } 304 | small { 305 | font-size: 12px; 306 | font-weight: normal; 307 | } 308 | } 309 | ul.inputs-list { 310 | margin-left: 25px; 311 | margin-bottom: 10px; 312 | padding-top: 0; 313 | } 314 | &:first-child { 315 | padding-top: 5px; 316 | } 317 | } 318 | input[type=radio], 319 | input[type=checkbox] { 320 | margin-bottom: 0; 321 | } 322 | } 323 | 324 | // Stacked forms 325 | .form-stacked { 326 | padding-left: 20px; 327 | fieldset { 328 | padding-top: $baseline / 2; 329 | } 330 | legend { 331 | margin-left: 0; 332 | } 333 | label { 334 | display: block; 335 | float: none; 336 | width: auto; 337 | font-weight: bold; 338 | text-align: left; 339 | line-height: 20px; 340 | padding-top: 0; 341 | } 342 | .clearfix { 343 | margin-bottom: $baseline / 2; 344 | div.input { 345 | margin-left: 0; 346 | } 347 | } 348 | .inputs-list { 349 | margin-bottom: 0; 350 | li { 351 | padding-top: 0; 352 | label { 353 | font-weight: normal; 354 | padding-top: 0; 355 | } 356 | } 357 | } 358 | div.error { 359 | padding-top: 10px; 360 | padding-bottom: 10px; 361 | padding-left: 10px; 362 | margin-top: 0; 363 | margin-left: -10px; 364 | } 365 | .actions { 366 | margin-left: -20px; 367 | padding-left: 20px; 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /views/stylesheets/partials/_preboot.scss: -------------------------------------------------------------------------------- 1 | /* Preboot.scss 2 | * Variables and mixins to pre-ignite any new web development project 3 | * ------------------------------------------------------------------ */ 4 | 5 | 6 | // VARIABLES 7 | // --------- 8 | 9 | // Links 10 | $linkColor: #0069d6; 11 | $linkColorHover: darken($linkColor, 10); 12 | 13 | // Grays 14 | $black: #000; 15 | $grayDark: lighten($black, 25%); 16 | $gray: lighten($black, 50%); 17 | $grayLight: lighten($black, 75%); 18 | $grayLighter: lighten($black, 90%); 19 | $white: #fff; 20 | 21 | // Accent Colors 22 | $blue: #049CDB; 23 | $blueDark: #0064CD; 24 | $green: #46a546; 25 | $red: #9d261d; 26 | $yellow: #ffc40d; 27 | $orange: #f89406; 28 | $pink: #c3325f; 29 | $purple: #7a43b6; 30 | 31 | // Baseline grid 32 | $basefont: 13px; 33 | $baseline: 18px; 34 | 35 | // Griditude 36 | $gridColumns: 16; 37 | $gridColumnWidth: 40px; 38 | $gridGutterWidth: 20px; 39 | $extraSpace: 40px; 40 | $siteWidth: ($gridColumns * $gridColumnWidth) + ($gridGutterWidth * ($gridColumns - 1)); 41 | 42 | // Color Scheme 43 | $baseColor: $blue; // Set a base color 44 | $complement: complement($baseColor); // Determine a complementary color 45 | $split1: adjust-hue($baseColor, 158); // Split complements 46 | $split2: adjust-hue($baseColor, -158); 47 | $triad1: adjust-hue($baseColor, 135); // Triads colors 48 | $triad2: adjust-hue($baseColor, -135); 49 | $tetra1: adjust-hue($baseColor, 90); // Tetra colors 50 | $tetra2: adjust-hue($baseColor, -90); 51 | $analog1: adjust-hue($baseColor, 22); // Analogs colors 52 | $analog2: adjust-hue($baseColor, -22); 53 | 54 | 55 | // MIXINS 56 | // ------ 57 | 58 | // Gradients 59 | @mixin horizontal-gradient ($startColor: #555, $endColor: #333) { 60 | background-color: $endColor; 61 | background-repeat: repeat-x; 62 | background-image: -khtml-gradient(linear, left top, right top, from($startColor), to($endColor)); // Konqueror 63 | background-image: -moz-linear-gradient(left, $startColor, $endColor); // FF 3.6+ 64 | background-image: -ms-linear-gradient(left, $startColor, $endColor); // IE10 65 | background-image: -webkit-gradient(linear, left top, right top, color-stop(0%, $startColor), color-stop(100%, $endColor)); // Safari 4+, Chrome 2+ 66 | background-image: -webkit-linear-gradient(left, $startColor, $endColor); // Safari 5.1+, Chrome 10+ 67 | background-image: -o-linear-gradient(left, $startColor, $endColor); // Opera 11.10 68 | background-image: linear-gradient(left, $startColor, $endColor); // Le standard 69 | } 70 | 71 | @mixin vertical-gradient ($startColor: #555, $endColor: #333) { 72 | background-color: $endColor; 73 | background-repeat: repeat-x; 74 | background-image: -khtml-gradient(linear, left top, left bottom, from($startColor), to($endColor)); // Konqueror 75 | background-image: -moz-linear-gradient(top, $startColor, $endColor); // FF 3.6+ 76 | background-image: -ms-linear-gradient(top, $startColor, $endColor); // IE10 77 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, $startColor), color-stop(100%, $endColor)); // Safari 4+, Chrome 2+ 78 | background-image: -webkit-linear-gradient(top, $startColor, $endColor); // Safari 5.1+, Chrome 10+ 79 | background-image: -o-linear-gradient(top, $startColor, $endColor); // Opera 11.10 80 | background-image: linear-gradient(top, $startColor, $endColor); // The standard 81 | } 82 | 83 | @mixin directional-gradient ($startColor: #555, $endColor: #333, $deg: 45deg) { 84 | background-color: $endColor; 85 | background-repeat: repeat-x; 86 | background-image: -moz-linear-gradient($deg, $startColor, $endColor); // FF 3.6+ 87 | background-image: -ms-linear-gradient($deg, $startColor, $endColor); // IE10 88 | background-image: -webkit-linear-gradient($deg, $startColor, $endColor); // Safari 5.1+, Chrome 10+ 89 | background-image: -o-linear-gradient($deg, $startColor, $endColor); // Opera 11.10 90 | background-image: linear-gradient($deg, $startColor, $endColor); // The standard 91 | } 92 | 93 | @mixin vertical-three-colors-gradient($startColor: #00b3ee, $midColor: #7a43b6, $colorStop: 50%, $endColor: #c3325f) { 94 | background-color: $endColor; 95 | background-repeat: no-repeat; 96 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from($startColor), color-stop($colorStop, $midColor), to($endColor)); 97 | background-image: -webkit-linear-gradient($startColor, $midColor $colorStop, $endColor); 98 | background-image: -moz-linear-gradient($startColor, $midColor $colorStop, $endColor); 99 | background-image: -ms-linear-gradient($startColor, $midColor $colorStop, $endColor); 100 | background-image: -o-linear-gradient($startColor, $midColor $colorStop, $endColor); 101 | background-image: linear-gradient($startColor, $midColor $colorStop, $endColor); 102 | } 103 | 104 | // Opacity 105 | @mixin opacity($opacity: 100) { 106 | filter: alpha(opacity=$opacity); 107 | -khtml-opacity: $opacity / 100; 108 | -moz-opacity: $opacity / 100; 109 | opacity: $opacity / 100; 110 | } 111 | 112 | // Gradient Bar Colors for buttons and allerts 113 | @mixin gradientBar($primaryColor, $secondaryColor) { 114 | @include vertical-gradient($primaryColor, $secondaryColor); 115 | text-shadow: 0 -1px 0 rgba(0,0,0,.25); 116 | border-color: $secondaryColor $secondaryColor darken($secondaryColor, 15%); 117 | border-color: rgba(0,0,0,.1) rgba(0,0,0,.1) fadein(rgba(0,0,0,.1), 15%); 118 | } 119 | 120 | // Clearfix for clearing floats like a boss h5bp.com/q 121 | @mixin clearfix { 122 | zoom: 1; 123 | &:before, &:after { 124 | display: table; 125 | content: ""; 126 | } 127 | &:after { 128 | clear: both; 129 | } 130 | } 131 | .clearfix { @include clearfix; } 132 | 133 | // Center-align a block level element 134 | @mixin center-block { 135 | display: block; 136 | margin: 0 auto; 137 | } 138 | 139 | // Sizing shortcuts 140 | @mixin size($height: 5px, $width: 5px) { 141 | height: $height; 142 | width: $width; 143 | } 144 | @mixin square($size: 5px) { 145 | @include size($size, $size); 146 | } 147 | 148 | // Input placeholder text 149 | @mixin placeholder($color: $grayLight) { 150 | :-moz-placeholder { 151 | color: $color; 152 | } 153 | ::-webkit-input-placeholder { 154 | color: $color; 155 | } 156 | } 157 | 158 | // Font Stacks 159 | @mixin shorthand-font($weight: normal, $size: 14px, $lineHeight: 20px) { 160 | font-size: $size; 161 | font-weight: $weight; 162 | line-height: $lineHeight; 163 | } 164 | @mixin sans-serif-font($weight: normal, $size: 14px, $lineHeight: 20px) { 165 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 166 | font-size: $size; 167 | font-weight: $weight; 168 | line-height: $lineHeight; 169 | } 170 | @mixin serif-font($weight: normal, $size: 14px, $lineHeight: 20px) { 171 | font-family: "Georgia", Times New Roman, Times, serif; 172 | font-size: $size; 173 | font-weight: $weight; 174 | line-height: $lineHeight; 175 | } 176 | @mixin monospace-font($weight: normal, $size: 12px, $lineHeight: 20px) { 177 | font-family: "Monaco", Courier New, monospace; 178 | font-size: $size; 179 | font-weight: $weight; 180 | line-height: $lineHeight; 181 | } 182 | 183 | // Grid System 184 | @mixin container { 185 | width: $siteWidth; 186 | margin: 0 auto; 187 | @include clearfix(); 188 | } 189 | .container { @include container; } 190 | 191 | @mixin columns($columnSpan: 1) { 192 | width: ($gridColumnWidth * $columnSpan) + ($gridGutterWidth * ($columnSpan - 1)); 193 | } 194 | 195 | @mixin offset($columnOffset: 1) { 196 | margin-left: ($gridColumnWidth * $columnOffset) + ($gridGutterWidth * ($columnOffset - 1)) + $extraSpace; 197 | } 198 | 199 | // Border Radius 200 | @mixin border-radius($radius: 5px) { 201 | -webkit-border-radius: $radius; 202 | -moz-border-radius: $radius; 203 | border-radius: $radius; 204 | } 205 | 206 | // Drop shadows 207 | @mixin box-shadow($shadow: 0 1px 3px rgba(0,0,0,.25)) { 208 | -webkit-box-shadow: $shadow; 209 | -moz-box-shadow: $shadow; 210 | box-shadow: $shadow; 211 | } 212 | 213 | // Transitions 214 | @mixin transition($transition) { 215 | -webkit-transition: $transition; 216 | -moz-transition: $transition; 217 | transition: $transition; 218 | } 219 | 220 | // Background clipping 221 | @mixin background-clip($clip) { 222 | -webkit-background-clip: $clip; 223 | -moz-background-clip: $clip; 224 | background-clip: $clip; 225 | } 226 | 227 | // CSS3 Content Columns 228 | @mixin content-columns($columnCount, $columnGap: 20px) { 229 | -webkit-column-count: $columnCount; 230 | -moz-column-count: $columnCount; 231 | column-count: $columnCount; 232 | -webkit-column-gap: $columnGap; 233 | -moz-column-gap: $columnGap; 234 | column-gap: $columnGap; 235 | } 236 | 237 | // Add an alphatransparency value to any background or border color (via Elyse Holladay) 238 | @mixin translucent-background($color: $white, $alpha: 1) { 239 | background-color: hsla(hue($color), saturation($color), lightness($color), $alpha); 240 | } 241 | @mixin translucent-border($color: $white, $alpha: 1) { 242 | border-color: hsla(hue($color), saturation($color), lightness($color), $alpha); 243 | background-clip: padding-box; 244 | } 245 | 246 | // Shared colors for buttons and alerts 247 | .btn, 248 | .alert-message { 249 | // Set text color 250 | &.danger, 251 | &.danger:hover, 252 | &.error, 253 | &.error:hover, 254 | &.success, 255 | &.success:hover, 256 | &.info, 257 | &.info:hover { 258 | color: $white 259 | } 260 | // Danger and error appear as red 261 | &.danger, 262 | &.error { 263 | @include gradientBar(#ee5f5b, #c43c35); 264 | } 265 | // Success appears as green 266 | &.success { 267 | @include gradientBar(#62c462, #57a957); 268 | } 269 | // Info appears as a neutral blue 270 | &.info { 271 | @include gradientBar(#5bc0de, #339bb9); 272 | } 273 | } -------------------------------------------------------------------------------- /views/stylesheets/partials/_patterns.scss: -------------------------------------------------------------------------------- 1 | /* Patterns.scss 2 | * Repeatable UI elements outside the base styles provided from the scaffolding 3 | * ---------------------------------------------------------------------------- */ 4 | 5 | 6 | // TOPBAR 7 | // ------ 8 | 9 | // Topbar for Branding and Nav 10 | .topbar { 11 | height: 40px; 12 | position: fixed; 13 | top: 0; 14 | left: 0; 15 | right: 0; 16 | z-index: 10000; 17 | overflow: visible; 18 | 19 | // gradient is applied to it's own element because overflow visible is not honored by ie when filter is present 20 | .fill { 21 | background:#222; 22 | @include vertical-gradient(#333, #222); 23 | $shadow: 0 1px 3px rgba(0,0,0,.25), inset 0 -1px 0 rgba(0,0,0,.1); 24 | @include box-shadow($shadow); 25 | } 26 | 27 | // Links get text shadow 28 | a { 29 | color: $grayLight; 30 | text-shadow: 0 -1px 0 rgba(0,0,0,.25); 31 | } 32 | 33 | // Hover and active states 34 | a:hover, 35 | ul li.active a { 36 | background-color: #333; 37 | background-color: rgba(255,255,255,.05); 38 | color: $white; 39 | text-decoration: none; 40 | } 41 | 42 | // Website name 43 | h3 { 44 | position:relative; 45 | a { 46 | float: left; 47 | display: block; 48 | padding: 8px 20px 12px; 49 | margin-left: -20px; // negative indent to left-align the text down the page 50 | color: $white; 51 | font-size: 20px; 52 | font-weight: 200; 53 | line-height: 1; 54 | } 55 | } 56 | 57 | // Search Form 58 | form { 59 | float: left; 60 | margin: 5px 0 0 0; 61 | position: relative; 62 | @include opacity(100); 63 | input { 64 | background-color: #444; 65 | background-color: rgba(255,255,255,.3); 66 | @include sans-serif-font(13px, normal, 1); 67 | width: 220px; 68 | padding: 4px 9px; 69 | color: #fff; 70 | color: rgba(255,255,255,.75); 71 | border: 1px solid #111; 72 | @include border-radius(4px); 73 | $shadow: inset 0 1px 2px rgba(0,0,0,.1), 0 1px 0px rgba(255,255,255,.25); 74 | @include box-shadow($shadow); 75 | @include transition(none); 76 | 77 | // Placeholder text gets special styles; can't be bundled together though for some reason 78 | &:-moz-placeholder { 79 | color: $grayLighter; 80 | } 81 | &::-webkit-input-placeholder { 82 | color: $grayLighter; 83 | } 84 | // Hover states 85 | &:hover { 86 | background-color: $grayLight; 87 | background-color: rgba(255,255,255,.5); 88 | color: #fff; 89 | } 90 | // Focus states (we use .focused since IE8 and down doesn't support :focus) 91 | &:focus, 92 | &.focused { 93 | outline: none; 94 | background-color: #fff; 95 | color: $grayDark; 96 | text-shadow: 0 1px 0 #fff; 97 | border: 0; 98 | padding: 5px 10px; 99 | @include box-shadow(0 0 3px rgba(0,0,0,.15)); 100 | } 101 | } 102 | } 103 | 104 | // Navigation 105 | ul { 106 | display: block; 107 | float: left; 108 | margin: 0 10px 0 0; 109 | position: relative; 110 | &.secondary-nav { 111 | float: right; 112 | margin-left: 10px; 113 | margin-right: 0; 114 | } 115 | li { 116 | display: block; 117 | float: left; 118 | font-size: 13px; 119 | a { 120 | display: block; 121 | float: none; 122 | padding: 10px 10px 11px; 123 | line-height: 19px; 124 | text-decoration: none; 125 | &:hover { 126 | color: #fff; 127 | text-decoration: none; 128 | } 129 | } 130 | &.active a { 131 | background-color: #222; 132 | background-color: rgba(0,0,0,.5); 133 | } 134 | } 135 | 136 | // Dropdowns 137 | &.primary-nav li ul { 138 | left: 0; 139 | } 140 | &.secondary-nav li ul { 141 | right: 0; 142 | } 143 | li.menu { 144 | position: relative; 145 | a.menu { 146 | &:after { 147 | width: 0px; 148 | height: 0px; 149 | display: inline-block; 150 | content: "↓"; 151 | text-indent: -99999px; 152 | vertical-align: top; 153 | margin-top: 8px; 154 | margin-left: 4px; 155 | border-left: 4px solid transparent; 156 | border-right: 4px solid transparent; 157 | border-top: 4px solid #fff; 158 | @include opacity(50); 159 | } 160 | } 161 | &.open { 162 | a.menu, 163 | a:hover { 164 | background-color: #444; 165 | background-color: rgba(255,255,255,.1); 166 | *background-color: #444; /* IE6-7 */ 167 | color: #fff; 168 | } 169 | ul { 170 | display: block; 171 | li { 172 | a { 173 | background-color: transparent; 174 | font-weight: normal; 175 | &:hover { 176 | background-color: rgba(255,255,255,.1); 177 | *background-color: #444; /* IE6-7 */ 178 | color: #fff; 179 | } 180 | } 181 | &.active a { 182 | background-color: rgba(255,255,255,.1); 183 | font-weight: bold; 184 | } 185 | } 186 | } 187 | } 188 | } 189 | li ul { 190 | background-color: #333; 191 | float: left; 192 | display: none; 193 | position: absolute; 194 | top: 40px; 195 | min-width: 160px; 196 | max-width: 220px; 197 | _width: 160px; 198 | margin-left: 0; 199 | margin-right: 0; 200 | padding: 0; 201 | text-align: left; 202 | border: 0; 203 | zoom: 1; 204 | @include border-radius(0 0 5px 5px); 205 | @include box-shadow(0 1px 2px rgba(0,0,0,0.6)); 206 | li { 207 | float: none; 208 | clear: both; 209 | display: block; 210 | background: none; 211 | font-size: 12px; 212 | a { 213 | display: block; 214 | padding: 6px 15px; 215 | clear: both; 216 | font-weight: normal; 217 | line-height: 19px; 218 | color: #bbb; 219 | &:hover { 220 | background-color: #333; 221 | background-color: rgba(255,255,255,.25); 222 | color: #fff; 223 | } 224 | } 225 | 226 | // Dividers (basically an hr) 227 | &.divider { 228 | height: 1px; 229 | overflow: hidden; 230 | background: #222; 231 | background: rgba(0,0,0,.2); 232 | border-bottom: 1px solid rgba(255,255,255,.1); 233 | margin: 5px 0; 234 | } 235 | 236 | // Section separaters 237 | span { 238 | clear: both; 239 | display: block; 240 | background: rgba(0,0,0,.2); 241 | padding: 6px 15px; 242 | cursor: default; 243 | color: $gray; 244 | border-top: 1px solid rgba(0,0,0,.2); 245 | } 246 | } 247 | } 248 | } 249 | } 250 | 251 | 252 | // PAGE HEADERS 253 | // ------------ 254 | 255 | .hero-unit { 256 | background-color: #f5f5f5; 257 | margin-top: 60px; 258 | margin-bottom: 30px; 259 | padding: 60px; 260 | @include border-radius(6px); 261 | h1 { 262 | margin-bottom: 0; 263 | font-size: 60px; 264 | line-height: 1; 265 | letter-spacing: -1px; 266 | } 267 | p { 268 | font-size: 18px; 269 | font-weight: 200; 270 | line-height: $baseline * 1.5; 271 | } 272 | } 273 | footer { 274 | margin-top: $baseline - 1; 275 | padding-top: $baseline - 1; 276 | border-top: 1px solid #eee; 277 | } 278 | 279 | // PAGE HEADERS 280 | // ------------ 281 | 282 | .page-header { 283 | margin-bottom: $baseline - 1; 284 | border-bottom: 1px solid #ddd; 285 | @include box-shadow(0 1px 0 rgba(255,255,255,.5)); 286 | h1 { 287 | margin-bottom: ($baseline / 2) - 1px; 288 | } 289 | } 290 | 291 | // BUTTON STYLES 292 | // ------------- 293 | 294 | @mixin btnColor($primaryColor, $secondaryColor) { 295 | @include vertical-gradient($primaryColor, $secondaryColor); 296 | text-shadow: 0 -1px 0 rgba(0,0,0,.25); 297 | border-color: $secondaryColor $secondaryColor darken($secondaryColor, 15%); 298 | border-color: rgba(0,0,0,.1) rgba(0,0,0,.1) fade-in(rgba(0,0,0,.1), .15); 299 | } 300 | 301 | 302 | // Base .btn styles 303 | .btn { 304 | // Button Base 305 | cursor: pointer; 306 | display: inline-block; 307 | @include vertical-three-colors-gradient(#fff, #fff, 0.25, darken(#fff, 10%)); 308 | padding: 5px 14px 6px; 309 | text-shadow: 0 1px 1px rgba(255,255,255,.75); 310 | color: #333; 311 | font-size: 13px; 312 | line-height: normal; 313 | border: 1px solid #ccc; 314 | border-bottom-color: #bbb; 315 | @include border-radius(4px); 316 | $shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05); 317 | @include box-shadow($shadow); 318 | 319 | &:hover { 320 | background-position: 0 -15px; 321 | color: #333; 322 | text-decoration: none; 323 | } 324 | 325 | // Primary Button Type 326 | &.primary { 327 | color:#fff; 328 | @include btnColor($blue, $blueDark); 329 | } 330 | 331 | // Transitions 332 | @include transition(.1s linear all); 333 | 334 | // Active and Disabled states 335 | &.disabled { 336 | cursor: default; 337 | background-image: none; 338 | @include opacity(65); 339 | } 340 | 341 | &:disabled { 342 | // disabled pseudo can't be included with .disabled 343 | // def because IE8 and below will drop it ;_; 344 | cursor: default; 345 | background-image: none; 346 | @include opacity(65); 347 | } 348 | 349 | &:active { 350 | $shadow: inset 0 3px 7px rgba(0,0,0,.1), 0 1px 2px rgba(0,0,0,.05); 351 | @include box-shadow($shadow); 352 | } 353 | 354 | // Button Sizes 355 | &.large { 356 | font-size: 16px; 357 | line-height: normal; 358 | padding: 9px 14px 9px; 359 | @include border-radius(6px); 360 | } 361 | 362 | &.small { 363 | padding: 7px 9px 7px; 364 | font-size: 11px; 365 | } 366 | 367 | } 368 | 369 | // Help Firefox not be a jerk about adding extra padding to buttons 370 | button.btn, 371 | input[type=submit].btn { 372 | &::-moz-focus-inner { 373 | padding: 0; 374 | border: 0; 375 | } 376 | } 377 | 378 | 379 | 380 | // ERROR STYLES 381 | // ------------ 382 | 383 | // Base alert styles 384 | .alert-message { 385 | @include btnColor(#fceec1, #eedc94); 386 | margin-bottom: $baseline; 387 | padding: 7px 14px; 388 | color: $grayDark; 389 | text-shadow: 0 1px 0 rgba(255,255,255,.5); 390 | border-width: 1px; 391 | border-style: solid; 392 | @include border-radius(4px); 393 | @include box-shadow(inset 0 1px 0 rgba(255,255,255,.25)); 394 | 395 | // Remove extra margin from content 396 | h5 { 397 | line-height: $baseline; 398 | } 399 | p { 400 | margin-bottom: 0; 401 | } 402 | div { 403 | margin-top: 5px; 404 | margin-bottom: 2px; 405 | line-height: 28px; 406 | } 407 | .btn { 408 | // Provide actions with buttons 409 | @include box-shadow(0 1px 0 rgba(255,255,255,.25)); 410 | } 411 | .close { 412 | float: right; 413 | margin-top: -2px; 414 | color: $black; 415 | font-size: 20px; 416 | font-weight: bold; 417 | text-shadow: 0 1px 0 rgba(255,255,255,1); 418 | @include opacity(20); 419 | &:hover { 420 | color: $black; 421 | text-decoration: none; 422 | @include opacity(40); 423 | } 424 | } 425 | 426 | &.block-message { 427 | background-image: none; 428 | background-color: lighten(#fceec1, 5%); 429 | padding: 14px; 430 | border-color: #fceec1; 431 | @include box-shadow(none); 432 | 433 | p { 434 | margin-right: 30px; 435 | } 436 | .alert-actions { 437 | margin-top: 5px; 438 | } 439 | &.error, 440 | &.success, 441 | &.info { 442 | color: $grayDark; 443 | text-shadow: 0 1px 0 rgba(255,255,255,.5); 444 | } 445 | &.error { 446 | background-color: lighten(#f56a66, 25%); 447 | border-color: lighten(#f56a66, 20%); 448 | } 449 | &.success { 450 | background-color: lighten(#62c462, 30%); 451 | border-color: lighten(#62c462, 25%); 452 | } 453 | &.info { 454 | background-color: lighten(#6bd0ee, 25%); 455 | border-color: lighten(#6bd0ee, 20%); 456 | } 457 | } 458 | } 459 | 460 | // NAVIGATION 461 | // ---------- 462 | 463 | // Common tab and pill styles 464 | .tabs, 465 | .pills { 466 | margin: 0 0 20px; 467 | padding: 0; 468 | @include clearfix(); 469 | li { 470 | display: inline; 471 | a { 472 | float: left; 473 | width: auto; 474 | } 475 | } 476 | } 477 | 478 | // Basic Tabs 479 | .tabs { 480 | width: 100%; 481 | border-bottom: 1px solid $grayLight; 482 | li { 483 | a { 484 | margin-bottom: -1px; 485 | margin-right: 2px; 486 | padding: 0 15px; 487 | line-height: ($baseline * 2) - 1; 488 | @include border-radius(3px 3px 0 0); 489 | &:hover { 490 | background-color: $grayLighter; 491 | border-bottom: 1px solid $grayLight; 492 | } 493 | } 494 | &.active a { 495 | background-color: #fff; 496 | padding: 0 14px; 497 | border: 1px solid #ccc; 498 | border-bottom: 0; 499 | color: $gray; 500 | } 501 | } 502 | } 503 | 504 | // Basic pill nav 505 | .pills { 506 | li { 507 | a { 508 | margin: 5px 3px 5px 0; 509 | padding: 0 15px; 510 | text-shadow: 0 1px 1px #fff; 511 | line-height: 30px; 512 | @include border-radius(15px); 513 | &:hover { 514 | background: $linkColorHover; 515 | color: #fff; 516 | text-decoration: none; 517 | text-shadow: 0 1px 1px rgba(0,0,0,.25); 518 | } 519 | } 520 | &.active a { 521 | background: $linkColor; 522 | color: #fff; 523 | text-shadow: 0 1px 1px rgba(0,0,0,.25); 524 | } 525 | } 526 | } 527 | 528 | 529 | // PAGINATION 530 | // ---------- 531 | 532 | .pagination { 533 | height: $baseline * 2; 534 | margin: $baseline 0; 535 | ul { 536 | float: left; 537 | margin: 0; 538 | border: 1px solid #ddd; 539 | border: 1px solid rgba(0,0,0,.15); 540 | @include border-radius(3px); 541 | @include box-shadow(0 1px 2px rgba(0,0,0,.05)); 542 | li { 543 | display: inline; 544 | a { 545 | float: left; 546 | padding: 0 14px; 547 | line-height: ($baseline * 2) - 2; 548 | border-right: 1px solid; 549 | border-right-color: #ddd; 550 | border-right-color: rgba(0,0,0,.15); 551 | *border-right-color: #ddd; /* IE6-7 */ 552 | text-decoration: none; 553 | } 554 | a:hover, 555 | &.active a { 556 | background-color: lighten($blue, 45%); 557 | } 558 | &.disabled a, 559 | &.disabled a:hover { 560 | background-color: transparent; 561 | color: $grayLight; 562 | } 563 | &.next a { 564 | border: 0; 565 | } 566 | } 567 | } 568 | } 569 | 570 | 571 | // WELLS 572 | // ----- 573 | 574 | .well { 575 | background-color: #f5f5f5; 576 | margin-bottom: 20px; 577 | padding: 19px; 578 | min-height: 20px; 579 | border: 1px solid #eee; 580 | border: 1px solid rgba(0,0,0,.05); 581 | @include border-radius(4px); 582 | @include box-shadow(inset 0 1px 1px rgba(0,0,0,.05)); 583 | } 584 | 585 | 586 | // MODALS 587 | // ------ 588 | 589 | .modal-backdrop { 590 | background-color: rgba(0,0,0,.5); 591 | position: fixed; 592 | top: 0; 593 | left: 0; 594 | right: 0; 595 | bottom: 0; 596 | z-index: 1000; 597 | } 598 | .modal { 599 | position: fixed; 600 | top: 50%; 601 | left: 50%; 602 | z-index: 2000; 603 | width: 560px; 604 | margin: -280px 0 0 -250px; 605 | background-color: $white; 606 | border: 1px solid #999; 607 | border: 1px solid rgba(0,0,0,.3); 608 | *border: 1px solid #999; /* IE6-7 */ 609 | @include border-radius(6px); 610 | @include box-shadow(0 3px 7px rgba(0,0,0,0.3)); 611 | @include background-clip(padding-box); 612 | .modal-header { 613 | border-bottom: 1px solid #eee; 614 | padding: 5px 20px; 615 | .close { 616 | position: absolute; 617 | right: 10px; 618 | top: 10px; 619 | color: #999; 620 | line-height:10px; 621 | font-size: 18px; 622 | } 623 | } 624 | .modal-body { 625 | padding: 20px; 626 | } 627 | .modal-footer { 628 | background-color: #f5f5f5; 629 | padding: 14px 20px 15px; 630 | border-top: 1px solid #ddd; 631 | @include border-radius(0 0 6px 6px); 632 | @include box-shadow(inset 0 1px 0 #fff); 633 | @include clearfix(); 634 | margin-bottom: 0; 635 | .btn { 636 | float: right; 637 | margin-left: 10px; 638 | } 639 | } 640 | } 641 | 642 | 643 | // POPOVER ARROWS 644 | // -------------- 645 | 646 | @mixin popover-arrow-above($arrowWidth: 5px) { 647 | bottom: 0; 648 | left: 50%; 649 | margin-left: -$arrowWidth; 650 | border-left: $arrowWidth solid transparent; 651 | border-right: $arrowWidth solid transparent; 652 | border-top: $arrowWidth solid #000; 653 | } 654 | 655 | @mixin popover-arrow-left($arrowWidth: 5px) { 656 | top: 50%; 657 | right: 0; 658 | margin-top: -$arrowWidth; 659 | border-top: $arrowWidth solid transparent; 660 | border-bottom: $arrowWidth solid transparent; 661 | border-left: $arrowWidth solid #000; 662 | } 663 | 664 | @mixin popover-arrow-below($arrowWidth: 5px) { 665 | top: 0; 666 | left: 50%; 667 | margin-left: -$arrowWidth; 668 | border-left: $arrowWidth solid transparent; 669 | border-right: $arrowWidth solid transparent; 670 | border-bottom: $arrowWidth solid #000; 671 | } 672 | 673 | @mixin popover-arrow-right($arrowWidth: 5px) { 674 | top: 50%; 675 | left: 0; 676 | margin-top: -$arrowWidth; 677 | border-top: $arrowWidth solid transparent; 678 | border-bottom: $arrowWidth solid transparent; 679 | border-right: $arrowWidth solid #000; 680 | } 681 | 682 | 683 | // TWIPSY 684 | // ------ 685 | 686 | .twipsy { 687 | display: block; 688 | position: absolute; 689 | visibility: visible; 690 | padding: 5px; 691 | font-size: 11px; 692 | z-index: 1000; 693 | @include opacity(80); 694 | &.above .twipsy-arrow { @include popover-arrow-above(); } 695 | &.left .twipsy-arrow { @include popover-arrow-left(); } 696 | &.below .twipsy-arrow { @include popover-arrow-below(); } 697 | &.right .twipsy-arrow { @include popover-arrow-right(); } 698 | .twipsy-inner { 699 | padding: 3px 8px; 700 | background-color: #000; 701 | color: white; 702 | text-align: center; 703 | max-width: 200px; 704 | text-decoration: none; 705 | @include border-radius(4px); 706 | } 707 | .twipsy-arrow { 708 | position: absolute; 709 | width: 0; 710 | height: 0; 711 | } 712 | } 713 | 714 | 715 | // Background clipping 716 | @mixin background-clip($clip) { 717 | -webkit-background-clip: $clip; 718 | -moz-background-clip: $clip; 719 | background-clip: $clip; 720 | } 721 | 722 | 723 | // POPOVERS 724 | // -------- 725 | 726 | .popover { 727 | position: absolute; 728 | top: 0; 729 | left: 0; 730 | z-index: 1000; 731 | padding: 5px; 732 | display: none; 733 | &.above .arrow { @include popover-arrow-above(); } 734 | &.right .arrow { @include popover-arrow-right(); } 735 | &.below .arrow { @include popover-arrow-below(); } 736 | &.left .arrow { @include popover-arrow-left(); } 737 | .arrow { 738 | position: absolute; 739 | width: 0; 740 | height: 0; 741 | } 742 | .inner { 743 | background-color: #333; 744 | background-color: rgba(0,0,0,.8); 745 | *background-color: #333; /* IE 6-7 */ 746 | padding: 3px; 747 | overflow: hidden; 748 | width: 280px; 749 | @include border-radius(6px); 750 | @include box-shadow(0 3px 7px rgba(0,0,0,0.3)); 751 | } 752 | .title { 753 | background-color: #f5f5f5; 754 | padding: 9px 15px; 755 | line-height: 1; 756 | @include border-radius(3px 3px 0 0); 757 | border-bottom:1px solid #eee; 758 | } 759 | .content { 760 | background-color: $white; 761 | padding: 14px; 762 | @include border-radius(0 0 3px 3px); 763 | @include background-clip(padding-box); 764 | p, ul, ol { 765 | margin-bottom: 0; 766 | } 767 | } 768 | } 769 | -------------------------------------------------------------------------------- /public/js/faye-browser-min.js: -------------------------------------------------------------------------------- 1 | if(!this.Faye)Faye={};Faye.extend=function(a,b,c){if(!b)return a;for(var d in b){if(!b.hasOwnProperty(d))continue;if(a.hasOwnProperty(d)&&c===false)continue;if(a[d]!==b[d])a[d]=b[d]}return a};Faye.extend(Faye,{VERSION:'0.6.4',BAYEUX_VERSION:'1.0',ID_LENGTH:128,JSONP_CALLBACK:'jsonpcallback',CONNECTION_TYPES:['long-polling','cross-origin-long-polling','callback-polling','websocket','in-process'],MANDATORY_CONNECTION_TYPES:['long-polling','callback-polling','in-process'],ENV:(function(){return this})(),random:function(a){a=a||this.ID_LENGTH;if(a>32){var b=Math.ceil(a/32),c='';while(b--)c+=this.random(32);return c}var d=Math.pow(2,a)-1,f=d.toString(36).length,c=Math.floor(Math.random()*d).toString(36);while(c.length0)j();i=false};var m=function(){h+=1;k()};m()},toJSON:function(a){if(this.stringify)return this.stringify(a,function(key,value){return(this[key]instanceof Array)?this[key]:value});return JSON.stringify(a)},timestamp:function(){var b=new Date(),c=b.getFullYear(),d=b.getMonth()+1,f=b.getDate(),g=b.getHours(),h=b.getMinutes(),i=b.getSeconds();var j=function(a){return a<10?'0'+a:String(a)};return j(c)+'-'+j(d)+'-'+j(f)+' '+j(g)+':'+j(h)+':'+j(i)}});Faye.Class=function(a,b){if(typeof a!=='function'){b=a;a=Object}var c=function(){if(!this.initialize)return this;return this.initialize.apply(this,arguments)||this};var d=function(){};d.prototype=a.prototype;c.prototype=new d();Faye.extend(c.prototype,b);return c};Faye.Namespace=Faye.Class({initialize:function(){this._c={}},exists:function(a){return this._c.hasOwnProperty(a)},generate:function(){var a=Faye.random();while(this._c.hasOwnProperty(a))a=Faye.random();return this._c[a]=a},release:function(a){delete this._c[a]}});Faye.Error=Faye.Class({initialize:function(a,b,c){this.code=a;this.params=Array.prototype.slice.call(b);this.message=c},toString:function(){return this.code+':'+this.params.join(',')+':'+this.message}});Faye.Error.parse=function(a){a=a||'';if(!Faye.Grammar.ERROR.test(a))return new this(null,[],a);var b=a.split(':'),c=parseInt(b[0]),d=b[1].split(','),a=b[2];return new this(c,d,a)};Faye.Error.versionMismatch=function(){return new this(300,arguments,"Version mismatch").toString()};Faye.Error.conntypeMismatch=function(){return new this(301,arguments,"Connection types not supported").toString()};Faye.Error.extMismatch=function(){return new this(302,arguments,"Extension mismatch").toString()};Faye.Error.badRequest=function(){return new this(400,arguments,"Bad request").toString()};Faye.Error.clientUnknown=function(){return new this(401,arguments,"Unknown client").toString()};Faye.Error.parameterMissing=function(){return new this(402,arguments,"Missing required parameter").toString()};Faye.Error.channelForbidden=function(){return new this(403,arguments,"Forbidden channel").toString()};Faye.Error.channelUnknown=function(){return new this(404,arguments,"Unknown channel").toString()};Faye.Error.channelInvalid=function(){return new this(405,arguments,"Invalid channel").toString()};Faye.Error.extUnknown=function(){return new this(406,arguments,"Unknown extension").toString()};Faye.Error.publishFailed=function(){return new this(407,arguments,"Failed to publish").toString()};Faye.Error.serverError=function(){return new this(500,arguments,"Internal server error").toString()};Faye.Deferrable={callback:function(a,b){if(!a)return;if(this._t==='succeeded')return a.apply(b,this._j);this._k=this._k||[];this._k.push([a,b])},errback:function(a,b){if(!a)return;if(this._t==='failed')return a.apply(b,this._j);this._l=this._l||[];this._l.push([a,b])},setDeferredStatus:function(){var a=Array.prototype.slice.call(arguments),b=a.shift(),c;this._t=b;this._j=a;if(b==='succeeded')c=this._k;else if(b==='failed')c=this._l;if(!c)return;var d;while(d=c.shift())d[0].apply(d[1],this._j)}};Faye.Publisher={countSubscribers:function(a){if(!this._3||!this._3[a])return 0;return this._3[a].length},addSubscriber:function(a,b,c){this._3=this._3||{};var d=this._3[a]=this._3[a]||[];d.push([b,c])},removeSubscriber:function(a,b,c){if(!this._3||!this._3[a])return;if(!b){delete this._3[a];return}var d=this._3[a],f=d.length;while(f--){if(b!==d[f][0])continue;if(c&&d[f][1]!==c)continue;d.splice(f,1)}},removeSubscribers:function(){this._3={}},publishEvent:function(){var b=Array.prototype.slice.call(arguments),c=b.shift();if(!this._3||!this._3[c])return;Faye.each(this._3[c],function(a){a[0].apply(a[1],b)})}};Faye.Timeouts={addTimeout:function(a,b,c,d){this._4=this._4||{};if(this._4.hasOwnProperty(a))return;var f=this;this._4[a]=Faye.ENV.setTimeout(function(){delete f._4[a];c.call(d)},1000*b)},removeTimeout:function(a){this._4=this._4||{};var b=this._4[a];if(!b)return;clearTimeout(b);delete this._4[a]}};Faye.Logging={LOG_LEVELS:{error:3,warn:2,info:1,debug:0},logLevel:'error',log:function(a,b){if(!Faye.logger)return;var c=Faye.Logging.LOG_LEVELS;if(c[Faye.Logging.logLevel]>c[b])return;var a=Array.prototype.slice.apply(a),d=' ['+b.toUpperCase()+'] [Faye',f=this.className,g=a.shift().replace(/\?/g,function(){try{return Faye.toJSON(a.shift())}catch(e){return'[Object]'}});for(var h in Faye){if(f)continue;if(typeof Faye[h]!=='function')continue;if(this instanceof Faye[h])f=h}if(f)d+='.'+f;d+='] ';Faye.logger(Faye.timestamp()+d+g)}};Faye.each(Faye.Logging.LOG_LEVELS,function(a,b){Faye.Logging[a]=function(){this.log(arguments,a)}});Faye.Grammar={LOWALPHA:/^[a-z]$/,UPALPHA:/^[A-Z]$/,ALPHA:/^([a-z]|[A-Z])$/,DIGIT:/^[0-9]$/,ALPHANUM:/^(([a-z]|[A-Z])|[0-9])$/,MARK:/^(\-|\_|\!|\~|\(|\)|\$|\@)$/,STRING:/^(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)| |\/|\*|\.))*$/,TOKEN:/^(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)))+$/,INTEGER:/^([0-9])+$/,CHANNEL_SEGMENT:/^(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)))+$/,CHANNEL_SEGMENTS:/^(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)))+(\/(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)))+)*$/,CHANNEL_NAME:/^\/(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)))+(\/(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)))+)*$/,WILD_CARD:/^\*{1,2}$/,CHANNEL_PATTERN:/^(\/(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)))+)*\/\*{1,2}$/,VERSION_ELEMENT:/^(([a-z]|[A-Z])|[0-9])(((([a-z]|[A-Z])|[0-9])|\-|\_))*$/,VERSION:/^([0-9])+(\.(([a-z]|[A-Z])|[0-9])(((([a-z]|[A-Z])|[0-9])|\-|\_))*)*$/,CLIENT_ID:/^((([a-z]|[A-Z])|[0-9]))+$/,ID:/^((([a-z]|[A-Z])|[0-9]))+$/,ERROR_MESSAGE:/^(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)| |\/|\*|\.))*$/,ERROR_ARGS:/^(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)| |\/|\*|\.))*(,(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)| |\/|\*|\.))*)*$/,ERROR_CODE:/^[0-9][0-9][0-9]$/,ERROR:/^([0-9][0-9][0-9]:(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)| |\/|\*|\.))*(,(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)| |\/|\*|\.))*)*:(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)| |\/|\*|\.))*|[0-9][0-9][0-9]::(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)| |\/|\*|\.))*)$/};Faye.Extensible={addExtension:function(a){this._5=this._5||[];this._5.push(a);if(a.added)a.added()},removeExtension:function(a){if(!this._5)return;var b=this._5.length;while(b--){if(this._5[b]!==a)continue;this._5.splice(b,1);if(a.removed)a.removed()}},pipeThroughExtensions:function(c,d,f,g){this.debug('Passing through ? extensions: ?',c,d);if(!this._5)return f.call(g,d);var h=this._5.slice();var i=function(a){if(!a)return f.call(g,a);var b=h.shift();if(!b)return f.call(g,a);if(b[c])b[c](a,i);else i(a)};i(d)}};Faye.extend(Faye.Extensible,Faye.Logging);Faye.Channel=Faye.Class({initialize:function(a){this.id=this.name=a},push:function(a){this.publishEvent('message',a)},isUnused:function(){return this.countSubscribers('message')===0}});Faye.extend(Faye.Channel.prototype,Faye.Publisher);Faye.extend(Faye.Channel,{HANDSHAKE:'/meta/handshake',CONNECT:'/meta/connect',SUBSCRIBE:'/meta/subscribe',UNSUBSCRIBE:'/meta/unsubscribe',DISCONNECT:'/meta/disconnect',META:'meta',SERVICE:'service',expand:function(a){var b=this.parse(a),c=['/**',a];var d=b.slice();d[d.length-1]='*';c.push(this.unparse(d));for(var f=1,g=b.length;f=Math.pow(2,32))this._e=0;return this._e.toString(36)},_z:function(a){Faye.extend(this._6,a);if(this._6.reconnect===this.HANDSHAKE&&this._1!==this.DISCONNECTED){this._1=this.UNCONNECTED;this._0=null;this._w()}},_A:function(a){if(!a.channel||!a.data)return;this.info('Client ? calling listeners for ? with ?',this._0,a.channel,a.data);this._2.distributeMessage(a)},_C:function(){if(!this._p)return;this._p=null;this.info('Closed connection for ?',this._0)},_w:function(){this._C();var a=this;Faye.ENV.setTimeout(function(){a.connect()},this._6.interval)}});Faye.extend(Faye.Client.prototype,Faye.Deferrable);Faye.extend(Faye.Client.prototype,Faye.Logging);Faye.extend(Faye.Client.prototype,Faye.Extensible);Faye.Transport=Faye.extend(Faye.Class({MAX_DELAY:0.0,batching:true,initialize:function(a,b){this.debug('Created new ? transport for ?',this.connectionType,b);this._8=a;this._a=b;this._f=[]},send:function(a,b){this.debug('Client ? sending message to ?: ?',this._8._0,this._a,a);if(!this.batching)return this.request([a],b);this._f.push(a);this._7=b;if(a.channel===Faye.Channel.HANDSHAKE)return this.flush();if(a.channel===Faye.Channel.CONNECT)this._q=a;this.addTimeout('publish',this.MAX_DELAY,this.flush,this)},flush:function(){this.removeTimeout('publish');if(this._f.length>1&&this._q)this._q.advice={timeout:0};this.request(this._f,this._7);this._q=null;this._f=[]},receive:function(a){this.debug('Client ? received from ?: ?',this._8._0,this._a,a);Faye.each(a,this._8.receiveMessage,this._8)},retry:function(a,b){var c=this;return function(){Faye.ENV.setTimeout(function(){c.request(a,2*b)},1000*b)}}}),{get:function(g,h,i,j){var k=g.endpoint;if(h===undefined)h=this.supportedConnectionTypes();Faye.asyncEach(this._r,function(b,c){var d=b[0],f=b[1];if(Faye.indexOf(h,d)<0)return c();f.isUsable(k,function(a){if(a)i.call(j,new f(g,k));else c()})},function(){throw new Error('Could not find a usable connection type for '+k);})},register:function(a,b){this._r.push([a,b]);b.prototype.connectionType=a},_r:[],supportedConnectionTypes:function(){return Faye.map(this._r,function(a){return a[0]})}});Faye.extend(Faye.Transport.prototype,Faye.Logging);Faye.extend(Faye.Transport.prototype,Faye.Timeouts);Faye.Event={_g:[],on:function(a,b,c,d){var f=function(){c.call(d)};if(a.addEventListener)a.addEventListener(b,f,false);else a.attachEvent('on'+b,f);this._g.push({_h:a,_s:b,_m:c,_n:d,_x:f})},detach:function(a,b,c,d){var f=this._g.length,g;while(f--){g=this._g[f];if((a&&a!==g._h)||(b&&b!==g._s)||(c&&c!==g._m)||(d&&d!==g._n))continue;if(g._h.removeEventListener)g._h.removeEventListener(g._s,g._x,false);else g._h.detachEvent('on'+g._s,g._x);this._g.splice(f,1);g=null}}};Faye.Event.on(Faye.ENV,'unload',Faye.Event.detach,Faye.Event);Faye.URI=Faye.extend(Faye.Class({queryString:function(){var c=[],d;Faye.each(this.params,function(a,b){c.push(encodeURIComponent(a)+'='+encodeURIComponent(b))});return c.join('&')},isLocal:function(){var a=Faye.URI.parse(Faye.ENV.location.href);var b=(a.hostname!==this.hostname)||(a.port!==this.port)||(a.protocol!==this.protocol);return!b},toURL:function(){var a=this.queryString();return this.protocol+this.hostname+':'+this.port+this.pathname+(a?'?'+a:'')}}),{parse:function(d,f){if(typeof d!=='string')return d;var g=new this();var h=function(b,c){d=d.replace(c,function(a){if(a)g[b]=a;return''})};h('protocol',/^https?\:\/+/);h('hostname',/^[^\/\:]+/);h('port',/^:[0-9]+/);Faye.extend(g,{protocol:'http://',hostname:Faye.ENV.location.hostname,port:Faye.ENV.location.port},false);if(!g.port)g.port=(g.protocol==='https://')?'443':'80';g.port=g.port.replace(/\D/g,'');var i=d.split('?'),j=i.shift(),k=i.join('?'),m=k?k.split('&'):[],o=m.length,l={};while(o--){i=m[o].split('=');l[decodeURIComponent(i[0]||'')]=decodeURIComponent(i[1]||'')}if(typeof f==='object')Faye.extend(l,f);g.pathname=j;g.params=l;return g}});if(!this.JSON){JSON={}}(function(){function k(a){return a<10?'0'+a:a}if(typeof Date.prototype.toJSON!=='function'){Date.prototype.toJSON=function(a){return this.getUTCFullYear()+'-'+k(this.getUTCMonth()+1)+'-'+k(this.getUTCDate())+'T'+k(this.getUTCHours())+':'+k(this.getUTCMinutes())+':'+k(this.getUTCSeconds())+'Z'};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(a){return this.valueOf()}}var m=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,o=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,l,p,s={'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'},n;function r(c){o.lastIndex=0;return o.test(c)?'"'+c.replace(o,function(a){var b=s[a];return typeof b==='string'?b:'\\u'+('0000'+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+c+'"'}function q(a,b){var c,d,f,g,h=l,i,j=b[a];if(j&&typeof j==='object'&&typeof j.toJSON==='function'){j=j.toJSON(a)}if(typeof n==='function'){j=n.call(b,a,j)}switch(typeof j){case'string':return r(j);case'number':return isFinite(j)?String(j):'null';case'boolean':case'null':return String(j);case'object':if(!j){return'null'}l+=p;i=[];if(Object.prototype.toString.apply(j)==='[object Array]'){g=j.length;for(c=0;c=200&&a<300)||a===304||a===1223)g.receive(JSON.parse(h.responseText));else d()}catch(e){d()}finally{Faye.Event.detach(Faye.ENV,'beforeunload',i);h.onreadystatechange=function(){};h=null}};var i=function(){h.abort()};Faye.Event.on(Faye.ENV,'beforeunload',i);h.send(Faye.toJSON(b))}}),{isUsable:function(a,b,c){b.call(c,Faye.URI.parse(a).isLocal())}});Faye.Transport.register('long-polling',Faye.Transport.XHR);Faye.Transport.CORS=Faye.extend(Faye.Class(Faye.Transport,{request:function(a,b){var c=Faye.ENV.XDomainRequest?XDomainRequest:XMLHttpRequest,d=new c(),f=this.retry(a,b),g=this;d.open('POST',this._a,true);d.onload=function(){try{g.receive(JSON.parse(d.responseText))}catch(e){f()}finally{d.onload=d.onerror=null;d=null}};d.onerror=f;d.onprogress=function(){};d.send('message='+encodeURIComponent(Faye.toJSON(a)))}}),{isUsable:function(a,b,c){if(Faye.URI.parse(a).isLocal())return b.call(c,false);if(Faye.ENV.XDomainRequest)return b.call(c,true);if(Faye.ENV.XMLHttpRequest){var d=new Faye.ENV.XMLHttpRequest();return b.call(c,d.withCredentials!==undefined)}return b.call(c,false)}});Faye.Transport.register('cross-origin-long-polling',Faye.Transport.CORS);Faye.Transport.JSONP=Faye.extend(Faye.Class(Faye.Transport,{request:function(b,c){var d={message:Faye.toJSON(b)},f=document.getElementsByTagName('head')[0],g=document.createElement('script'),h=Faye.Transport.JSONP.getCallbackName(),i=Faye.URI.parse(this._a,d),j=this;var k=function(){if(!g.parentNode)return false;g.parentNode.removeChild(g);return true};Faye.ENV[h]=function(a){Faye.ENV[h]=undefined;try{delete Faye.ENV[h]}catch(e){}if(!k())return;j.receive(a)};Faye.ENV.setTimeout(function(){if(!Faye.ENV[h])return;k();j.request(b,2*c)},1000*c);i.params.jsonp=h;g.type='text/javascript';g.src=i.toURL();f.appendChild(g)}}),{_y:0,getCallbackName:function(){this._y+=1;return'__jsonp'+this._y+'__'},isUsable:function(a,b,c){b.call(c,true)}});Faye.Transport.register('callback-polling',Faye.Transport.JSONP); --------------------------------------------------------------------------------