livestream beta commit

This commit is contained in:
Seth Call 2020-04-16 11:26:18 -05:00
parent 9691d815cd
commit b9681fd418
40 changed files with 1587 additions and 31 deletions

View File

@ -64,7 +64,7 @@ gem 'resque-lonely_job', '~> 1.0.0'
gem 'eventmachine', '1.2.3' gem 'eventmachine', '1.2.3'
gem 'amqp', '0.9.8' gem 'amqp', '0.9.8'
#gem 'logging-rails', :require => 'logging/rails' #gem 'logging-rails', :require => 'logging/rails'
gem 'pg_migrate' gem 'pg_migrate', '0.1.14'
gem 'ruby-protocol-buffers', '1.2.2' gem 'ruby-protocol-buffers', '1.2.2'
gem 'sendgrid', '1.2.0' gem 'sendgrid', '1.2.0'
gem 'geokit-rails' gem 'geokit-rails'
@ -84,7 +84,7 @@ gem 'stripe'
gem 'zip-codes' gem 'zip-codes'
gem 'email_validator' gem 'email_validator'
gem 'best_in_place' #, github: 'bernat/best_in_place' gem 'best_in_place' #, github: 'bernat/best_in_place'
gem 'auto_strip_attributes', '2.6.0'
#group :libv8 do #group :libv8 do
@ -126,9 +126,9 @@ end
group :test do group :test do
gem 'simplecov', '~> 0.7.1' gem 'simplecov', '~> 0.7.1'
gem 'simplecov-rcov' gem 'simplecov-rcov'
gem 'capybara-webkit' # gem 'capybara-webkit'
gem 'capybara-screenshot', '0.3.22' # 1.0.0 broke compat with rspec. maybe we need newer rspec # gem 'capybara-screenshot', '0.3.22' # 1.0.0 broke compat with rspec. maybe we need newer rspec
gem 'poltergeist' # gem 'poltergeist'
end end
gem 'pry' gem 'pry'

View File

@ -87,6 +87,8 @@ GEM
arel (6.0.4) arel (6.0.4)
arr-pm (0.0.10) arr-pm (0.0.10)
cabin (> 0) cabin (> 0)
auto_strip_attributes (2.6.0)
activerecord (>= 4.0)
aws-sdk (1.67.0) aws-sdk (1.67.0)
aws-sdk-v1 (= 1.67.0) aws-sdk-v1 (= 1.67.0)
aws-sdk-v1 (1.67.0) aws-sdk-v1 (1.67.0)
@ -114,13 +116,6 @@ GEM
rack (>= 1.0.0) rack (>= 1.0.0)
rack-test (>= 0.5.4) rack-test (>= 0.5.4)
xpath (~> 2.0) xpath (~> 2.0)
capybara-screenshot (0.3.22)
capybara (>= 1.0, < 3)
colored
launchy
capybara-webkit (1.14.0)
capybara (>= 2.3.0, < 2.14.0)
json
carrierwave (0.11.2) carrierwave (0.11.2)
activemodel (>= 3.2.0) activemodel (>= 3.2.0)
activesupport (>= 3.2.0) activesupport (>= 3.2.0)
@ -134,7 +129,6 @@ GEM
childprocess (0.8.0) childprocess (0.8.0)
ffi (~> 1.0, >= 1.0.11) ffi (~> 1.0, >= 1.0.11)
clamp (1.0.1) clamp (1.0.1)
cliver (0.3.2)
cocoon (1.2.11) cocoon (1.2.11)
coderay (1.1.2) coderay (1.1.2)
coffee-rails (4.2.2) coffee-rails (4.2.2)
@ -144,7 +138,6 @@ GEM
coffee-script-source coffee-script-source
execjs execjs
coffee-script-source (1.12.2) coffee-script-source (1.12.2)
colored (1.2)
concurrent-ruby (1.0.5) concurrent-ruby (1.0.5)
country-select (1.1.1) country-select (1.1.1)
crass (1.0.3) crass (1.0.3)
@ -403,10 +396,6 @@ GEM
insist insist
mustache (= 0.99.8) mustache (= 0.99.8)
stud stud
poltergeist (1.17.0)
capybara (~> 2.1)
cliver (~> 0.3.1)
websocket-driver (>= 0.2.0)
polyamorous (1.3.3) polyamorous (1.3.3)
activerecord (>= 3.0) activerecord (>= 3.0)
postgres-copy (0.6.0) postgres-copy (0.6.0)
@ -610,9 +599,6 @@ GEM
rack (>= 1.0.0) rack (>= 1.0.0)
warden (1.2.7) warden (1.2.7)
rack (>= 1.0) rack (>= 1.0)
websocket-driver (0.7.0)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3)
will_paginate (3.1.6) will_paginate (3.1.6)
xdan-datetimepicker-rails (2.5.4) xdan-datetimepicker-rails (2.5.4)
jquery-rails jquery-rails
@ -629,6 +615,7 @@ DEPENDENCIES
activeadmin activeadmin
activeadmin_addons activeadmin_addons
amqp (= 0.9.8) amqp (= 0.9.8)
auto_strip_attributes (= 2.6.0)
aws-sdk (~> 1) aws-sdk (~> 1)
bcrypt-ruby (= 3.0.1) bcrypt-ruby (= 3.0.1)
best_in_place best_in_place
@ -636,8 +623,6 @@ DEPENDENCIES
bootstrap-will_paginate (= 0.0.6) bootstrap-will_paginate (= 0.0.6)
bugsnag bugsnag
capybara capybara
capybara-screenshot (= 0.3.22)
capybara-webkit
carrierwave (= 0.11.2) carrierwave (= 0.11.2)
carrierwave_direct carrierwave_direct
cocoon cocoon
@ -669,8 +654,7 @@ DEPENDENCIES
launchy launchy
mime-types (= 1.25) mime-types (= 1.25)
net-ssh net-ssh
pg_migrate pg_migrate (= 0.1.14)
poltergeist
postgres-copy (= 0.6.0) postgres-copy (= 0.6.0)
postgres_ext postgres_ext
protected_attributes protected_attributes
@ -708,4 +692,4 @@ DEPENDENCIES
zip-codes zip-codes
BUNDLED WITH BUNDLED WITH
1.13.7 1.17.1

View File

@ -0,0 +1,10 @@
ActiveAdmin.register JamRuby::EventBriteOrder, :as => 'EventBriteOrder' do
menu :parent => 'Misc'
config.sort_order = 'created_at DESC'
filter :live_stream
filter :email
filter :order_id
end

View File

@ -0,0 +1,51 @@
ActiveAdmin.register_page "EventBriteOrderUploads" do
menu :label => 'Event Brite Order Upload', :parent => 'Misc'
page_action :upload_eventbriteorders, :method => :post do
EventBriteOrder.transaction do
puts params
live_stream = LiveStream.find_by_id!(params[:jam_ruby_event_brite_order][:live_stream_id])
file = params[:jam_ruby_event_brite_order][:csv]
upload = EventBriteOrderUpload.new
upload.upload_file_name = file.original_filename
upload.save!
array_of_arrays = CSV.read(file.tempfile.path, headers:true)
array_of_arrays.each do |row|
order_id = row['Order ID']
event_brite_order = EventBriteOrder.find_by_order_id(order_id)
if event_brite_order.nil?
event_brite_order = EventBriteOrder.new
end
event_brite_order.event_brite_order_upload = upload
event_brite_order.live_stream = live_stream
event_brite_order.event_name = row['Event Name']
event_brite_order.order_id = order_id
event_brite_order.ticket_count = row['Tickets']
event_brite_order.ticket_type = row['Type']
event_brite_order.first_name = row['First Name']
event_brite_order.last_name = row['Last Name']
event_brite_order.email = row['Email Address']
event_brite_order.save!
end
redirect_to admin_eventbriteorderuploads_path, :notice => "Created #{array_of_arrays.length} event brite orders!"
end
end
content do
active_admin_form_for EventBriteOrder.new, :url => admin_eventbriteorderuploads_upload_eventbriteorders_path, :builder => ActiveAdmin::FormBuilder do |f|
f.inputs "Upload Event Brite Orders" do
f.input :csv, as: :file, required: true, :label => "An event brite order CSV exactly as exported from Eventbrite"
f.input :live_stream, required:true, as: :select, :collection => LiveStream.upcoming
end
f.actions
end
end
end

View File

@ -0,0 +1,21 @@
ActiveAdmin.register JamRuby::LiveStream, :as => 'LiveStream' do
menu :parent => 'Misc'
config.sort_order = 'created_at DESC'
filter :listed
filter :event_id
before_build do |record|
if !record.event_type.nil?
puts "escape"
else
record.slug = 'please-do-this-sort-of-thing'
record.starts_at = 10.days.from_now.midnight
record.ends_at = 11.days.from_now.midnight
record.img_width = 200
record.event_type = 'eventbrite'
end
end
end

View File

@ -30,3 +30,11 @@
border:1px solid gray; border:1px solid gray;
padding:5px; padding:5px;
} }
.datetime_select li.fragment {
label {
display: block;
width:auto;
}
}

View File

@ -391,3 +391,4 @@ limit_counter_reminders.sql
amazon_v2.sql amazon_v2.sql
store_backend_details_rate_session.sql store_backend_details_rate_session.sql
invited_user_receiver.sql invited_user_receiver.sql
live_streams.sql

47
db/up/live_streams.sql Normal file
View File

@ -0,0 +1,47 @@
CREATE TABLE live_streams (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
slug VARCHAR(512) NOT NULL UNIQUE,
title TEXT,
description TEXT,
social_description TEXT,
listed BOOLEAN NOT NULL DEFAULT FALSE,
starts_at TIMESTAMP,
ends_at TIMESTAMP,
img_url VARCHAR(1024),
img_width INTEGER,
img_height INTEGER,
youtube_code VARCHAR(1024),
eventbriteid VARCHAR(1024),
event_type VARCHAR(100),
event_brite_registration_url VARCHAR(1024),
allow_in BOOLEAN NOT NULL DEFAULT FALSE,
white_label_player BOOLEAN NOT NULL DEFAULT TRUE,
user_id VARCHAR(64) REFERENCES users(id) ON DELETE SET NULL,
band_id VARCHAR(64) REFERENCES bands(id) ON DELETE SET NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE event_brite_orders (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
live_stream_id VARCHAR(64) REFERENCES live_streams(id) ON DELETE CASCADE,
event_brite_order_upload_id VARCHAR(64) REFERENCES event_brite_order_uploads(id) ON DELETE CASCADE,
event_name VARCHAR(100) NOT NULL,
order_id VARCHAR(100) NOT NULL UNIQUE,
ticket_count INTEGER,
ticket_type VARCHAR(100),
first_name VARCHAR(100),
last_name VARCHAR(100),
email VARCHAR(200),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
times_claimed INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE event_brite_order_uploads (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
upload_file_name VARCHAR(500) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

View File

@ -241,6 +241,9 @@ require "jam_ruby/models/jam_track_file"
require "jam_ruby/models/jam_track_mixdown" require "jam_ruby/models/jam_track_mixdown"
require "jam_ruby/models/jam_track_mixdown_package" require "jam_ruby/models/jam_track_mixdown_package"
require "jam_ruby/models/genre_jam_track" require "jam_ruby/models/genre_jam_track"
require "jam_ruby/models/live_stream"
require "jam_ruby/models/event_brite_order"
require "jam_ruby/models/event_brite_order_upload"
require "jam_ruby/app/mailers/async_mailer" require "jam_ruby/app/mailers/async_mailer"
require "jam_ruby/app/mailers/batch_mailer" require "jam_ruby/app/mailers/batch_mailer"
require "jam_ruby/app/mailers/progress_mailer" require "jam_ruby/app/mailers/progress_mailer"

View File

@ -0,0 +1,22 @@
class JamRuby::EventBriteOrder < ActiveRecord::Base
belongs_to :live_stream, class_name: 'JamRuby::LiveStream'
belongs_to :event_brite_order_upload, class_name: 'JamRuby::EventBriteOrderUpload'
validates :event_name, presence: true
validates :order_id, presence: true
validates :ticket_count, presence: true
validates :ticket_type, presence: true
before_validation :sanitize
def sanitize
self.first_name.strip! if self.first_name
self.last_name.strip! if self.last_name
self.email.strip! if self.email
self.ticket_type.strip! if self.ticket_type
self.order_id.strip! if self.order_id
self.event_name.strip! if self.event_name
end
end

View File

@ -0,0 +1,5 @@
class JamRuby::EventBriteOrderUpload < ActiveRecord::Base
has_many :event_brite_orders, class_name: 'JamRuby::EventBriteOrder'
validates :upload_file_name, presence: true
end

View File

@ -0,0 +1,37 @@
class JamRuby::LiveStream < ActiveRecord::Base
attr_accessible :user_id, :band_id, :starts_at, :ends_at, :img_url, :slug, :title, :description, :listed, :allow_in, :white_label_player, :youtube_code, :eventbriteid, :event_type, :social_description, :event_brite_registration_url, as: :admin
#belongs_to :user, class_name: 'JamRuby::User'
#belongs_to :band, class_name: 'JamRuby::Band'
#validate :one_of_user_band
validates :slug, uniqueness: true, presence: true
before_validation :sanitize_active_admin
def ready_display
self.starts_at && self.ends_at && (self.user_id || self.band_id)
end
def self.upcoming
LiveStream.where(listed: true).where("starts_at > ?", 2.days.ago).order('starts_at DESC')
end
def sanitize_active_admin
self.img_url = nil if self.img_url == ''
self.user_id = nil if self.user_id == ''
self.band_id = nil if self.band_id == ''
self.social_description = nil if self.social_description == ''
end
def one_of_user_band
if band && user
errors.add(:user, 'specify band, or user. not both')
end
end
def admin_name
"#{title} EB:#{eventbriteid} #{starts_at}"
end
end

View File

@ -0,0 +1,81 @@
(function (context) {
/**
* Javascript wrappers for the REST API
*/
"use strict";
var AuthorizedEventStorage = {}
context.JK = context.JK || {};
context.JK.AuthorizedEventStorage = AuthorizedEventStorage
var key = 'authorized_events'
AuthorizedEventStorage.storeAuthorizedEvent = (authorizedEvent) => {
console.log("storing authorized event", authorizedEvent)
var events = fetchFromStorage()
if(!events) {
events = {}
}
if(!events.list) {
events.list = {}
}
if(!authorizedEvent.event_id) {
console.error("no event_id on new event", authorizedEvent)
alert("Unable to store order (no event_id)! Please contact support@jamkazam.com.")
return
}
if(!authorizedEvent.order_id) {
console.error("no order_id on new event", authorizedEvent)
alert("Unable to store order (no order_id)! Please contact support@jamkazam.com.")
return
}
events.list[authorizedEvent.event_id] = authorizedEvent
try {
window.localStorage.setItem(key, JSON.stringify(events) )
}
catch(e) {
console.error("can't update localStorage")
alert("Could not update local storage ith order.\nPlease try a different browser. Please do not use incognito mode.")
}
}
AuthorizedEventStorage.locateEvent = (eventId) => {
var events = fetchFromStorage()
if(events && events.list) {
return events.list[eventId]
}
else {
console.log("no events found in storage")
return null
}
}
AuthorizedEventStorage.listAuthorizedEvents = () => {
var events = fetchFromStorage()
if(events) {
return events.list
}
else {
return {}
}
}
function fetchFromStorage() {
var events = window.localStorage.getItem(key)
if(events){
try {
return JSON.parse(events)
}
catch(e){
console.log("events not parseable", events)
return null;
}
}
}
})(window);

View File

@ -0,0 +1,9 @@
//= require ./jam_rest
//= require ./authorized_event_storage
//= require reflux
//= require react
//= require react_ujs
//= require react-init
//= require react-input-autosize
//= require react-select
//= require ./react-components

View File

@ -0,0 +1,43 @@
(function (context) {
/**
* Javascript wrappers for the REST API
*/
"use strict";
var Rest2 = {}
context.JK = context.JK || {};
context.JK.Rest2 = Rest2
Rest2.listLiveStreams = () => {
return fetch('/api/live_streams', {
method: 'get',
credentials: 'same-origin', // include, *same-origin, omit
headers: {
'Content-Type': 'application/json'
},
})
}
Rest2.getLiveStream = (id) => {
return fetch('/api/live_streams/' + id, {
method: 'get',
credentials: 'same-origin', // include, *same-origin, omit
headers: {
'Content-Type': 'application/json'
},
})
}
Rest2.authorizeLiveStream = (data) => {
return fetch('/api/live_streams/claim', {
method: 'post',
credentials: 'same-origin', // include, *same-origin, omit
headers: {
'Content-Type': 'application/json'
},
cache: 'no-cache',
body: JSON.stringify(data)
})
}
})(window);

View File

@ -0,0 +1,3 @@
//= require_directory ./react-components/actions
//= require ./react-components/stores/EventStore
//= require_directory ./react-components

View File

@ -0,0 +1,115 @@
context = window
EventActions = context.EventActions
context.EventList = React.createClass({
mixins: [Reflux.listenTo(EventStore, "onEventsChanged")],
events: null,
getInitialState: function () {
return {events: null}
},
componentDidMount: function () {
},
isModeYours: function() {
return this.props.mode == 'yours'
},
render: function () {
return this.renderAll();
},
renderEvents() {
if(!this.state.events || this.state.events.length == 0) {
return null;
}
var items = []
var isModeYours = this.isModeYours();
for(var i = 0; i < this.state.events.length; i++) {
var event = this.state.events[i]
if(isModeYours && (!!event.authorization || event.allow_in) || !isModeYours && (!event.authorization && !event.allow_in)) {
var dynamic = null
var thumb = null
if(event.img_url) {
thumb = <img className="thumb" src={event.img_url} />
dynamic = 'event'
}
else {
dynamic = 'event no-image'
}
var title = event.title ? event.title : 'Missing event title'
title = <a className="title-link" href={"/events/" + event.slug}>{title}</a>
var when = event.starts_at ? new Date(event.starts_at).toLocaleString() : 'Unknown start time'
var item = <div className={dynamic} key={event.id}>
{thumb}
<div className="detail">
<div className="title">{title}</div>
<div className="when">{when}</div>
</div>
</div>
items.push(item)
}
}
return items
},
renderAll: function() {
var events = this.renderEvents()
var headerText = this.isModeYours() ? 'Your Registered Events' : 'All Upcoming Events'
if(!events || events.length == 0) {
return null;
}
if(events) {
var response = <div className="EventList">
<div className="event-header">
{headerText}
</div>
<div className="event-section">
{events}
</div>
</div>
return response
}
else {
return <div className="EventList">
<div className="event-header">
{headerText}
</div>
<div className="event-section loading">
Loading ...
</div>
</div>;
}
},
onEventsChanged: function (allEvents) {
var scopedEvents = []
if(this.isModeYours()) {
allEvents.events.entries.forEach(function(event) {
if (event.authorization || event.allow_in) {
scopedEvents.push(event)
}
})
}
else {
scopedEvents = allEvents.events.entries
}
this.setState({events: scopedEvents})
}
})

View File

@ -0,0 +1,220 @@
context = window
EventActions = context.EventActions
context.EventPage = React.createClass({
mixins: [Reflux.listenTo(EventStore, "onEventsChanged")],
getInitialState: function () {
return {submitting: false, error: null, event: null}
},
parseSlug: function() {
var path = window.location.pathname
console.log("slug path: ", path)
var pathPart = path.substring('/events/'.length)
console.log("slug part", pathPart)
var query = pathPart.indexOf('?')
if(query > -1) {
var slug = pathPart.substring(0, query)
}
else {
var slug = pathPart
}
console.log("slug", slug)
return slug;
},
componentDidMount: function () {
EventActions.single(this.parseSlug())
// new Plyr('#video');
},
componentDidUpdate: function() {
},
authorizeDone: function(response) {
this.setState({submitting:false})
EventActions.addAuthorization(response)
},
authorizeFailed: function(e) {
if(e instanceof SyntaxError) {
this.setState({error: 'Server error. Please try again or contact support@jamkazam.com.'})
}
else if(e instanceof Error) {
this.setState({error: 'Please enter a valid Eventbrite Order ID'})
}
else {
console.log("heheh", e)
}
this.setState({submitting:false})
},
handleSubmit: function(event) {
var value = document.getElementById("order-input").value
if(value) {
context.JK.Rest2.authorizeLiveStream({order: value}).then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
return response.json()
}).then((response) => this.authorizeDone(response)).catch((jqXHR) => this.authorizeFailed(jqXHR))
this.setState({submitting: true, error:null})
}
event.preventDefault();
},
isErrored() {
return !!this.state.error
},
isReady() {
return !!this.state.event
},
isAuthorized() {
return this.state.event.authorization || this.state.event.allow_in
},
isNotAuthorized() {
return false
},
videoCode() {
return this.state.event.youtube_code
},
title() {
return this.state.event.title ? this.state.event.title : 'Unknown title'
},
description() {
return this.state.event.description ? this.state.event.description : ''
},
startsAt() {
return this.state.event.starts_at? "Starts at " + new Date(this.state.event.starts_at).toLocaleString() : 'Unknown start time'
},
header() {
if(this.isErrored()) {
return <div className="header">
<div className="title">No event found.</div>
</div>
}
else if(this.isReady()) {
var title = this.title()
var description = this.description()
var startsAt = this.startsAt()
return <div className="header">
<div className="title">{title}</div>
<div className="description">{description}</div>
<div className="starts-at">{startsAt}</div>
</div>
}
else {
return <div className="header">
<div className="title">Loading ...</div>
</div>
}
},
body: function() {
var video = null
if(this.isErrored()) {
video = null
}
else if(this.isReady()) {
if(this.isAuthorized()) {
//video = <div id="player" className="video player">
// <div id="video" data-plyr-provider="youtube" data-plyr-embed-id={this.videoCode()}></div>
//</div>
var videoCode = this.videoCode()
if(videoCode) {
var src = "https://www.youtube.com/embed/" + this.videoCode() + "?modestbranding=true&autoplay=0&rel=0"
video = <div id="player">
<iframe src={src} frameBorder="0" style={ { position:"absolute", top:0,left:0,width:"100%", height:"100%"} } allow="accelerometer; encrypted-media; gyroscope; autoplay" allowFullScreen={true}></iframe>
</div>
}
else {
video = <div id="no-player" className="no-code"><div>No video yet</div></div>
}
}
else {
var notRegistered = <span className="no-register">You are not registered for this event!</span>
var onceDone = <li>Enter your EventBrite order code at the <a href="https://www.jamkazam.com/events" target="_blank"> JamKazam Event Registration</a> page when done registering.</li>
var eventBriteUrl = this.state.event.event_brite_registration_url
if(eventBriteUrl) {
var meat = <div>
{notRegistered}
<ol>
<li>Please register at <a target="_blank" href={eventBriteUrl}>EventBrite</a> to see this video</li>
{onceDone}
</ol>
</div>
}
else {
var meat = <div>
{notRegistered}
<ol>
<li>Please find your event at <a target="_blank" href="https://www.eventbrite.com/d/online/jamkazam/">EventBrite</a> and register for this event.</li>
{onceDone}
</ol>
</div>
}
video = <div id="no-player" className="no-code">
{meat}
</div>
}
}
else {
video = <div id="no-player"></div>
}
return video
},
render: function () {
var header = this.header()
var body = this.body()
var response = <div className="EventPage">
<div id="header">
<div className="logo-holder"><img src="/assets/logo.png"/></div>
</div>
<div id="top-container">
{header}
</div>
<div className="listing">
{body}
</div>
</div>
return response
},
onEventsChanged: function (allEvents) {
var event = null;
if(allEvents && allEvents.events && allEvents.events.entries && allEvents.events.entries.length > 0){
event = allEvents.events.entries[0]
}
console.log("event change", event, allEvents)
this.setState({event: event, error:allEvents.events_error})
}
})

View File

@ -0,0 +1,87 @@
context = window
EventActions = context.EventActions
context.EventsPage = React.createClass({
getInitialState: function () {
return {submitting: false, error: null}
},
componentDidMount: function () {
EventActions.refresh()
},
authorizeDone: function(response) {
this.setState({submitting:false})
EventActions.addAuthorization(response)
},
authorizeFailed: function(e) {
if(e instanceof SyntaxError) {
this.setState({error: 'Server error. Please try again or contact support@jamkazam.com.'})
}
else if(e instanceof Error) {
this.setState({error: 'Please enter a valid Eventbrite Order ID'})
}
else {
console.log("heheh", e)
}
this.setState({submitting:false})
},
handleSubmit: function(event) {
var value = document.getElementById("order-input").value
if(value) {
context.JK.Rest2.authorizeLiveStream({order: value}).then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
return response.json()
}).then((response) => this.authorizeDone(response)).catch((jqXHR) => this.authorizeFailed(jqXHR))
this.setState({submitting: true, error:null})
}
event.preventDefault();
},
render: function () {
var ctaButtonClasses = "cta-button"
if(this.state.submitting) {
ctaButtonClasses = ctaButtonClasses + " submitting"
}
var errorClasses = "error"
if(this.state.error) {
errorClasses = errorClasses + " active"
}
var response = <div className="EventsPage">
<div id="header">
<div className="logo-holder"><img src="/assets/logo.png"/></div>
</div>
<div id="top-container">
<div className="header">
<p>Enter your order number from<img className="eventbrite-logo" src="/assets/event/eventbrite-logo.png"/>below:</p>
</div>
<div className="submit-row">
<div className="submit promo-start">
<form id="unlock" onSubmit={this.handleSubmit}>
<input type="text" name="order" className="order-input" id="order-input"/>
<button className={ctaButtonClasses}>Unlock Livestream</button>
</form>
<span className={errorClasses}>{this.state.error}</span>
</div>
</div>
</div>
<div className="listings">
<div className="preview-and-action-box">
<div id="upcoming-yours">
<EventList mode="yours"/>
</div>
<div id="upcoming-all">
<EventList mode="all"/>
</div>
</div>
</div>
</div>
return response
},
})

View File

@ -0,0 +1,6 @@
@EventActions = Reflux.createActions({
refresh: {},
single: {}
addAuthorization: {}
})

View File

@ -0,0 +1,70 @@
context = window
console.log(@EventActions)
console.log(window.EventActions)
@EventStore = Reflux.createStore(
{
listenables: window.EventActions
events: null
eventsError: null
eventStorage: null
AuthorizedEventsStorage: null
init: ->
@AuthorizedEventStorage = context.JK.AuthorizedEventStorage
changed: () ->
state = @getState()
@trigger(state)
addAuthorization: (authorization) ->
@AuthorizedEventStorage.storeAuthorizedEvent(authorization)
@collateEvents()
onRefresh: () ->
context.JK.Rest2.listLiveStreams().then((response) => response.json()).then((response) => @refreshDone(response)).catch((jqXHR) => @refreshFailed(jqXHR))
onSingle: (slug) ->
context.JK.Rest2.getLiveStream(slug).then((response) => response.json()).then((response) =>
console.log("single", response)
if(response.errors)
@refreshFailed(response)
else
matchedFormat = {entries:[ response ]}
@refreshDone(matchedFormat))
.catch((jqXHR) => @refreshFailed(jqXHR))
refreshDone: (events) ->
@eventsError = false
@events = events
@collateEvents()
refreshFailed: (events) ->
console.log("events error", events)
@eventsError = true
@changed()
collateEvents: () ->
authorizedEvents = @AuthorizedEventStorage.listAuthorizedEvents()
for event in @events.entries
authorization = authorizedEvents[event.id]
if authorization
console.log("event is authorized", event)
event.authorization = authorization
else
event.authorization = null
@changed()
getState:() ->
{events: @events, events_error: @eventsError}
}
)

View File

@ -0,0 +1,125 @@
var defaultURL = 'http://127.0.0.1:3000/signup'; //<---- CHANGE TO YOUR WEBSITE URL
//show loading graphic
function showLoader(id) {
$('#' + id + ' img').fadeIn('slow');
}
//hdie loading graphic
function hideLoader(id) {
$('#' + id + ' img').fadeOut('slow');
}
//function to check load state of each frame
function allLoaded(){
var results = [];
$('iframe').each(function(){
if(!$(this).data('loaded')){results.push(false)}
});
var result = (results.length > 0) ? false : true;
return result;
};
function loadPage($frame, url) {
if ( url.substr(0,7) !== 'http://' && url.substr(0,8) !== 'https://' && url.substr(0, 7) !== 'file://' ) {
url = 'http://'+url;
}
$('iframe').not($frame).each(function(){showLoader($(this).parent().attr('id'));})
$('iframe').not($frame).data('loaded', false);
$('iframe').not($frame).attr('src', url);
}
$('.frame').each(function(){showLoader($(this).attr('id'))});
//when document loads
$(document).ready(function(){
loadPage('', defaultURL);
//query string
var qsArray = window.location.href.split('?');
var qs = qsArray[qsArray.length-1];
if(qs != '' && qsArray.length > 1){
$('#url input[type=text]').val(qs);
loadPage('', qs);
}
//set slidable div width
$('#frames #inner').css('width', function(){
var width = 0;
$('.frame').each(function(){width += $(this).outerWidth() + 20});
return width;
});
//add event handlers for options radio buttons
$('input[type=radio]').change(function(){
$frames = $('#frames');
$inputs = $('input[type=radio]:checked').val();
if($inputs == '1'){
$frames.addClass('widthOnly');
} else {
$frames.removeClass('widthOnly');
}
});
//add event handlers for scrollbars checkbox
$('input[type=checkbox]').change(function(){
var scrollBarWidth = 15;
$frames = $('#frames');
$inputs = $('#scrollbar:checked');
if( $inputs.length == 0 ){
scrollBarWidth = -15;
}
$frames.find('iframe').each(function(i,el) {
$(el).attr('width', parseInt($(el).attr('width')) + scrollBarWidth);
});
});
//when the url textbox is used
$('form').submit(function(){
loadPage('' , $('#url input[type=text]').val());
return false;
});
//when frame loads
$('iframe').load(function(){
var $this = $(this);
var url = '';
var error = false;
try{
url = $this.contents().get(0).location.href;
} catch(e) {
error = true;
if($('#url input[type=text]').val() != ''){
url = $('#url input[type=text]').val();
} else {
url = defaultURL;
}
}
//load other pages with the same URL
if(allLoaded()){
if(error){
alert('Browsers prevent navigation from inside iframes across domains.\nPlease use the textbox at the top for external sites.');
loadPage('', defaultURL);
}else{
loadPage($this, url);
}
}
//when frame loads, hide loader graphic
else{
error = false;
hideLoader($(this).parent().attr('id'));
$(this).data('loaded',true);
}
});
});

View File

@ -232,8 +232,9 @@
} }
if(networkTest.length) { if(networkTest.length) {
stepNetworkTest.initialize($wizardSteps.filter($('.network-test')), self); // skip network test always
STEPS[dynamicStepCount++] = stepNetworkTest //stepNetworkTest.initialize($wizardSteps.filter($('.network-test')), self);
//STEPS[dynamicStepCount++] = stepNetworkTest
} }
STEPS[dynamicStepCount++]=stepSuccess STEPS[dynamicStepCount++]=stepSuccess

View File

@ -0,0 +1,21 @@
$base-font-family: 'arial', sans-serif;
$large-font-size: 1.5rem;
$standard-font-size: 1rem;
$smaller-font-size: .8rem;
$small-font-size: .7rem;
$input-font-size: 1.0rem;
$submit-button-width:9rem;
$checkbox-label-width:14rem;
$create-account-width:10rem;
$start-promo-btn-width:13rem;
$jamkazam-background:#323232;
$copy-color-on-dark: #b9b9b9;
$copy-color-on-white: #575757;
$cta-color: #e03d04;
$chunkyBorderWidth: 6px;

View File

@ -0,0 +1,26 @@
/**
*= require dialogs/banner
*= require_directory ./react-components
*/
@import "events/constants";
html,
body {
min-height: 100%;
overflow:auto;
background-color:$jamkazam-background;
color:white;
}
body {
min-height: 100%;
margin: 0;
padding: 0;
font-size: 16px;
line-height: 1.2;
font-family: Raleway, Arial, Helvetica, sans-serif;
}

View File

@ -0,0 +1,81 @@
@import "events/constants";
@import "client/common.scss";
.EventList {
.event-header {
background-color: $cta-color;
color: white;
font-size: 24px;
text-align: center;
padding: 20px 0;
border-width: $chunkyBorderWidth 0 $chunkyBorderWidth;
border-style: solid;
border-color: $copy-color-on-dark;
}
.event-section {
padding: 10px;
//s border-width: 0 0 $chunkyBorderWidth;
//border-style: solid;
//border-color: $copy-color-on-dark;
&.loading {
text-align:center;
}
}
.event {
display:grid;
grid-template-columns: 10rem 20rem;
align-content:center;
height:8rem;
border-style: solid;
border-width: 2px 0 2px 0;
border-color: $copy-color-on-dark;
&:first-child {
border-width:0 0 1px 0;
}
&:last-child {
border-width:1px 0 0 0;
}
&:only-child {
border-width:0;
}
&.no-image {
grid-template-columns: 30rem;
}
}
.title-link {
color:white;
}
img.thumb {
width:100%;
max-height:400px;
}
div.thumb {
display: grid;
justify-content: center;
align-content: center;
text-transform: uppercase;
padding-right:1rem;
}
.detail {
display:grid;
justify-content: center;
padding-left:1rem;
align-content:center;
}
.title {
font-size:1.5rem;
}
.when {
font-size:1rem;
}
}

View File

@ -0,0 +1,114 @@
@import "events/constants";
@import "client/common.scss";
[data-react-class="EventPage"] {
width:100%;
.EventPage {
width:100%;
}
#header {
display:grid;
justify-content: center;
width:100%;
background-color:$jamkazam-background;
padding:1.5rem 0;
}
.title {
font-size:2rem;
}
.description {
color:gray;
font-size:1rem;
margin-top:.2rem;
}
.starts-at {
color:gray;
font-size:1rem;
margin-top:.2rem;
}
#root {
min-height: 100%;
align-items: center;
//justify-content: center;
min-width: 50%;
display: flex;
flex-direction: column;
}
#player, #no-player {
position:relative;
padding-bottom:56.25%;
padding-top:30px;
height:0;
overflow:hidden;
top:-5rem;
border-width: $chunkyBorderWidth;
border-style: solid;
border-color: $copy-color-on-dark;
a {
color:#fc0;
}
}
#no-player {
display:grid;
align-content: center;
}
.no-register {
text-decoration: underline;
font-weight:bold;
margin-top:2rem;
display:block;
}
.no-code {
background-color: black;
padding-bottom:0 !important;
height:auto !important;
min-height:200px !important;
padding-top:0 !important;
text-align:center;
font-size:1rem;
ul,ol {
text-align:left;
}
}
#top-container {
background-color:black;
display: grid;
justify-content:center;
padding:2rem 0 7rem 0;
text-align:center;
}
div.root {
flex-grow: 1;
overflow: auto;
}
html {
font-size: 100%;
}
.listing {
width:50%;
margin:auto;
//display:grid;
//justify-content: center;
}
@media (min-width:1px) and (max-width: 1199px) {
html {
font-size: 100%;
}
}
}

View File

@ -0,0 +1,169 @@
@import "events/constants";
@import "client/common.scss";
[data-react-class="EventsPage"] {
width:100%;
.EventsPage {
width:100%;
}
#header {
display:grid;
justify-content: center;
width:100%;
background-color:$jamkazam-background;
padding:1.5rem 0;
}
#root {
min-height: 100%;
align-items: center;
//justify-content: center;
min-width: 50%;
display: flex;
flex-direction: column;
}
.order-input {
font-size:1rem;
padding:5px;
}
.cta-button {
cursor:pointer;
font-size: 1rem;
color: white;
background-color: $cta-color;
text-align: center;
padding: 5px;
margin-left:5px;
border-radius: 8px;
border: 1px outset buttonface;
font-family: Raleway, Arial, Helvetica, sans-serif;
&.submitting {
background-color: gray;
}
}
.error {
margin-top: 1rem;
#position: absolute;
display:none;
color:red;
font-weight:bold;
&.active {
display:inline;
}
}
.eventbrite-logo {
height:2rem;
vertical-align: middle;
}
#top-container {
background-color:black;
display: grid;
justify-content:center;
padding:5rem 0 7rem 0;
}
div.root {
flex-grow: 1;
overflow: auto;
}
.logo-holder {
max-width: 300px;
min-width:0;
img {
width: 100%;
}
}
.submit-row {
text-align:center;
}
p {
margin:.5rem 0;
}
p:first-child {
margin-top:0;
}
p.last-child {
margin-bottom:0;
}
.header {
font-size: $large-font-size;
margin-top: 1rem;
width: 100%;
max-width: 700px;
text-align: center;
}
html {
font-size: 100%;
}
.listings {
display:grid;
justify-content: center;
width:100%;
}
.preview-and-action-box {
background-color: black;
position:relative;
top: -2rem;
min-width: 400px;
@include border_box_sizing;
border-width: 0 $chunkyBorderWidth $chunkyBorderWidth;
border-style: solid;
border-color: $copy-color-on-dark;
z-index: 1;
}
@media (min-width:1px) and (max-width: 1199px) {
html {
font-size: 100%;
}
.code-input {
max-width:$submit-button-width;
}
.code-input input {
font-size: $standard-font-size;
}
.code-instructions {
width:$submit-button-width;
}
.text-input input {
width:$create-account-width;
}
.select-input select{
width:$create-account-width;
}
.instructions.create-account {
margin-top:1rem;
text-align:justify;
}
.success-msg {
margin-top: 1rem;
}
.logo-holder.create-account {
margin-top:2rem;
}
.code-form.create-account {
margin-top:1rem;
}
.powered-by.create-account {
margin-top:0.5rem;
}
}
}

View File

@ -0,0 +1,47 @@
class ApiLiveStreamsController < ApiController
respond_to :json
def log
@log || Logging.logger[ApiLiveStreamsController]
end
def index
@live_streams = LiveStream.upcoming
render "api_live_streams/index", :layout => nil
end
def claim
order = params[:order]
if order.nil?
render :json => {}, :status => 404, layout: nil
return
end
order.strip!
if order.start_with? "#"
order = order[1..-1]
end
event_brite_order = EventBriteOrder.find_by_order_id(order)
if event_brite_order.nil?
render :json => {}, :status => 404, layout: nil
else
EventBriteOrder.where(id: event_brite_order.id).update_all(times_claimed: event_brite_order.times_claimed + 1)
render :json => {event_id: event_brite_order.live_stream.id, order_id: event_brite_order.order_id, event_type: 'eventbrite'}, :status => :ok, layout: nil
end
end
def show
slug = params[:slug]
@live_stream = LiveStream.find_by_slug!(slug)
render "api_live_streams/show", :layout => nil
end
end

View File

@ -2,7 +2,20 @@ class EventsController < ApplicationController
respond_to :html respond_to :html
def list
render 'events', layout: 'events'
end
def show def show
@event = LiveStream.find_by_slug(params[:slug])
render 'event', :layout => 'events'
end
# 2014 lulz
def show_old
@event = Event.find_by_slug!(params[:slug]) @event = Event.find_by_slug!(params[:slug])
render 'event', :layout => "web" render 'event', :layout => "web"
end end

View File

@ -31,6 +31,10 @@ class SpikesController < ApplicationController
render :layout => false render :layout => false
end end
def responsive
render :layout => false
end
def subscription def subscription
#Notification.send_reload(MessageFactory::ALL_NATIVE_CLIENTS) #Notification.send_reload(MessageFactory::ALL_NATIVE_CLIENTS)

View File

@ -0,0 +1,4 @@
node :entries do |page|
partial "api_live_streams/show", object: @live_streams
end

View File

@ -0,0 +1,3 @@
object @live_stream
attributes :id, :slug, :title, :description, :starts_at, :allow_in, :ends_at, :img_url, :img_width, :img_height, :youtube_code, :eventbriteid, :event_type, :event_brite_registration_url, :white_label_player

View File

@ -0,0 +1,6 @@
- provide(:page_name, 'JamKazam Event')
- provide(:description, @description)
- provide(:title, @title)
#root
= react_component 'EventPage'

View File

@ -0,0 +1,6 @@
- provide(:page_name, 'Upcoming JamKazam Events')
- provide(:description, @description)
- provide(:title, @title)
#root
= react_component 'EventsPage'

View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<title><%= full_title(yield(:title)) %></title>
<!--[if IE]>
<link rel="stylesheet" type="text/css" href="css/ie.css" media="screen, projection"/>
<![endif]-->
<link href='http://fonts.googleapis.com/css?family=Raleway:100,200,300,400,500,600,700' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="https://cdn.plyr.io/3.5.10/plyr.css" />
<%= stylesheet_link_tag "events/events", media: "all" %>
<%= include_gon(:init => true) %>
<%= csrf_meta_tags %>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="description" content="<%= meta_description(yield(:description)) %>">
<% if content_for?(:social_meta) %>
<%= yield(:social_meta) %>
<% else %>
<%= render "layouts/social_meta" %>
<% end %>
<%= render "shared/ad_sense" %>
</head>
<body class="events <%= yield(:page_name) %>">
<div id="basic-container">
<!--<script src="https://cdn.plyr.io/3.5.10/plyr.polyfilled.js"></script>-->
<%= javascript_include_tag "events/events" %>
<div class="wrapper">
<%= yield %>
<div id="rt-e617b4394e0e49e1c234c63161bb2e15"></div> <script src="https://rumbletalk.com/client/?VHi@pnno"></script>
</div>
</div>
<%= render "clients/help" %>
<%= render 'dialogs/banner' %>
<%= render "shared/ga" %>
</body>
</html>

View File

@ -0,0 +1,70 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="description" content="Responsive design testing for the masses">
<title>Responsive Design Testing</title>
<style>
*{vertical-align:top;}
body{padding:20px;font-family:sans-serif;overflow-y:scroll;}
h2{margin:0 0 20px 0;}
span.small{font-size:60%;vertical-align:middle;}
#url{margin:0 0 20px 0px;display:block;}
#url input[type=text]{border:solid 1px #666;width:85%;margin:0 auto;font-size:2em;text-align:left;}
#url input[type=submit]{display:none;}
#url #options{float:right;line-height:25px;width:13%;}
#url #options input{margin-top:5px;}
#frames{overflow-x:scroll;width:100%;margin-bottom:10px;padding-bottom:20px;}
.frame{margin-right:20px;float:left;}
.frame:last-child{margin-right:0;}
.frame img{display:none;vertical-align:middle;}
iframe{border:solid 1px #000;}
.widthOnly {height:580px;}
.widthOnly h2 span{display:none;}
.widthOnly iframe{height:500px;}
</style>
</head>
<body id="container">
<div id="url">
<form method="post">
<input type="text" placeholder="Test your own site... type the url and hit enter" />
<input type="submit" value="submit">
<div id="options">
<label for="normal"><input id="normal" type="radio" name="option" value="1" checked>Width only</label><br />
<label for="accurate"><input id="accurate" type="radio" name="option" value="2">Device sizes</label><br />
<label for="scrollbar"><input id="scrollbar" type="checkbox" name="scrollbar" value="1" checked>Visible Scrollbars</label>
</div>
</form>
</div>
<div id="frames" class="widthOnly">
<div id="inner">
<div id="f1" class="frame">
<h2>240<span> x 320</span> <span class="small">(small phone)</span> <img src="http://mattkersley.com/wp-content/themes/mattkersley/images/loader_large.gif" /></h2>
<iframe sandbox="allow-same-origin allow-forms allow-scripts" seamless width="255" height="320"></iframe>
</div>
<div id="f2" class="frame">
<h2>320<span> x 480</span> <span class="small">(iPhone)</span> <img src="http://mattkersley.com/wp-content/themes/mattkersley/images/loader_large.gif" /></h2>
<iframe sandbox="allow-same-origin allow-forms allow-scripts" seamless width="335" height="480"></iframe>
</div>
<div id="f3" class="frame">
<h2>480<span> x 640</span> <span class="small">(small tablet)</span> <img src="http://mattkersley.com/wp-content/themes/mattkersley/images/loader_large.gif" /></h2>
<iframe sandbox="allow-same-origin allow-forms allow-scripts" seamless width="495" height="640"></iframe>
</div>
<div id="f4" class="frame">
<h2>768<span> x 1024</span> <span class="small">(iPad - Portrait)</span> <img src="http://mattkersley.com/wp-content/themes/mattkersley/images/loader_large.gif" /></h2>
<iframe sandbox="allow-same-origin allow-forms allow-scripts" seamless width="783" height="1024"></iframe>
</div>
<div id="f5" class="frame">
<h2>1024<span> x 768</span> <span class="small">(iPad - Landscape)</span> <img src="http://mattkersley.com/wp-content/themes/mattkersley/images/loader_large.gif" /></h2>
<iframe sandbox="allow-same-origin allow-forms allow-scripts" seamless width="1039" height="768"></iframe>
</div>
</div>
</div>
<span>A tool by <a href="http://mattkersley.com">Matt Kersley</a> - <a href="https://github.com/mattkersley/Responsive-Design-Testing">Fork it on Github</a></span><br />
<span>Note: The content width may not be pixel perfect - I have added 15px to the iframes to cater for the scrollbars</span>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6/jquery.min.js"></script>
<%= javascript_include_tag "spikes/responsive.js" %>
</body>
</html>

View File

@ -469,5 +469,5 @@ if defined?(Bundler)
config.rating_dialog_min_time = 60 config.rating_dialog_min_time = 60
config.rating_dialog_min_num = 1 config.rating_dialog_min_num = 1
end end
end end

View File

@ -131,6 +131,7 @@ Rails.application.routes.draw do
get '/gmail_contacts', to: 'gmail#gmail_contacts' get '/gmail_contacts', to: 'gmail#gmail_contacts'
get '/events/:slug', to: 'events#show', :as => 'event' get '/events/:slug', to: 'events#show', :as => 'event'
get '/events', to: 'events#list', as: 'events'
get '/endorse/:id/:service', to: 'users#endorse', :as => 'endorse' get '/endorse/:id/:service', to: 'users#endorse', :as => 'endorse'
@ -143,6 +144,7 @@ Rails.application.routes.draw do
require 'resque-retry/server' require 'resque-retry/server'
mount Resque::Server.new, :at => "/resque" if Rails.env == "development" mount Resque::Server.new, :at => "/resque" if Rails.env == "development"
get '/responsive', to: 'spikes#responsive'
# route to spike controller (proof-of-concepts) # route to spike controller (proof-of-concepts)
get '/listen_in', to: 'spikes#listen_in' get '/listen_in', to: 'spikes#listen_in'
get '/facebook_invite', to: 'spikes#facebook_invite' get '/facebook_invite', to: 'spikes#facebook_invite'
@ -229,6 +231,11 @@ Rails.application.routes.draw do
post '/auths/login' => 'api_auths#login' post '/auths/login' => 'api_auths#login'
# live streams
match '/live_streams' => 'api_live_streams#index', :via => :get
match '/live_streams/claim' => 'api_live_streams#claim', :via => :post
match '/live_streams/:slug' => 'api_live_streams#show', :via => :get
# music sessions # music sessions
match '/sessions/:id/participants/legacy' => 'api_music_sessions#participant_create_legacy', :via => :post # can be removed when new Create Session comes in match '/sessions/:id/participants/legacy' => 'api_music_sessions#participant_create_legacy', :via => :post # can be removed when new Create Session comes in
match '/sessions/:id/participants' => 'api_music_sessions#participant_create', :via => :post match '/sessions/:id/participants' => 'api_music_sessions#participant_create', :via => :post