Compare commits

..

No commits in common. "develop" and "paypal_checkout_flow" have entirely different histories.

194 changed files with 36318 additions and 4888 deletions

View File

@ -1,25 +0,0 @@
name: Build Admin
on:
push:
branches:
- develop
jobs:
build:
runs-on: dagger
steps:
- name: Checkout
uses: https://github.com/actions/checkout@v4
- name: Install Dagger
run: |
curl -L https://dl.dagger.io/dagger/install.sh | sh
sudo mv bin/dagger /usr/local/bin/
- name: Login to Gitea Registry
run: echo "${{ gitea.token }}" | docker login git.staging.jamkazam.com -u ${{ gitea.actor }} --password-stdin
- name: Build and Publish with Dagger
working-directory: ./admin
run: |
dagger call build-local --source=. --repo-root=../ publish --address=git.staging.jamkazam.com/seth/jam-cloud-admin:latest

View File

@ -1,41 +0,0 @@
name: Environment Orchestrator
on: [push]
jobs:
orchestrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Component Deployment Gatekeeper
run: |
# JAM_CLUSTER_ENV should be set to 'staging' or 'production' in the Gitea Runner
ENV="${JAM_CLUSTER_ENV:-staging}"
echo "🌐 Cluster Environment: $ENV"
# 1. Extract modes for this environment
ADMIN_MODE=$(jq -r ".environments.$ENV.admin" .jk-deploy.json)
WEB_MODE=$(jq -r ".environments.$ENV.web" .jk-deploy.json)
WS_MODE=$(jq -r ".environments.$ENV.[\"websocket-gateway\"]" .jk-deploy.json)
# 2. Conditional Execution
if [ "$ADMIN_MODE" == "short-circuit" ]; then
echo "⚡ ADMIN: Short-circuit detected. Deploying immediately..."
cd admin && dagger call ship --source=.
else
echo "⏸️ ADMIN: Mode is $ADMIN_MODE. Skipping short-circuit deploy."
fi
if [ "$WEB_MODE" == "short-circuit" ]; then
echo "⚡ WEB: Short-circuit detected. Deploying immediately..."
cd web && dagger call ship --source=.
else
echo "⏸️ WEB: Mode is $WEB_MODE. Skipping short-circuit deploy."
fi
if [ "$WS_MODE" == "short-circuit" ]; then
echo "⚡ WS-GATEWAY: Short-circuit detected. Deploying immediately..."
cd websocket-gateway && just ship
else
echo "⏸️ WS-GATEWAY: Mode is $WS_MODE. Skipping short-circuit deploy."
fi

View File

@ -1,8 +0,0 @@
name: Test Runner
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo "Runner is working!"

View File

@ -2,7 +2,7 @@ ActiveAdmin.register JamRuby::AffiliateQuarterlyPayment, :as => 'Affiliate Quart
menu :label => 'Quarterly Reports', :parent => 'Affiliates'
config.sort_order = 'year desc, quarter desc, due_amount_in_cents desc'
config.sort_order = 'due_amount_in_cents DESC'
config.batch_actions = false
config.clear_action_items!
config.filters = true
@ -14,32 +14,18 @@ ActiveAdmin.register JamRuby::AffiliateQuarterlyPayment, :as => 'Affiliate Quart
filter :quarter
filter :closed
filter :paid
filter :jamtracks_sold
filter :subscriptions_count
filter :due_amount_in_cents
form :partial => 'form'
scope("Sorted By Due Amount", default: true) { |scope| scope.order('year desc, quarter desc, due_amount_in_cents desc') }
scope("Sorted By Jamtracks Sold", default: false) { |scope| scope.order('year desc, quarter desc, jamtracks_sold desc') }
scope("Sorted By Subs", default: false) { |scope| scope.order('year desc, quarter desc, subscriptions_count desc') }
scope("Sorted By Newest First") { |scope| scope.order('year desc, quarter desc, id desc') }
scope("Any") { |scope| scope.order('year desc, quarter desc, due_amount_in_cents desc') }
index do
# default_actions # use this for all view/edit/delete links
column 'Year' do |oo| oo.year end
column 'Quarter' do |oo| oo.quarter end
column 'Partner Id' do |oo| oo.affiliate_partner.id end
column 'Partner' do |oo| link_to(oo.affiliate_partner.display_name, oo.affiliate_partner.admin_url, {:title => oo.affiliate_partner.display_name}) end
column "Tot ($)" do |oo| sprintf("$%.2f", oo.due_amount_in_cents.to_f / 100.to_f) end
column "Sub ($)" do |oo| sprintf("$%.2f", oo.subscription_due_amount_in_cents.to_f / 100.to_f) end
column "Jam ($)" do |oo| sprintf("$%.2f", oo.jamtrack_due_amount_in_cents.to_f / 100.to_f) end
column 'JamTracks' do |oo| oo.jamtracks_sold end
column 'Subscriptions' do |oo| oo.subscriptions_count end
column "Due (\u00A2)" do |oo| oo.due_amount_in_cents end
column 'JamTracks Sold' do |oo| oo.jamtracks_sold end
column 'Paid' do |oo| oo.paid end
column 'Closed' do |oo| oo.paid end

View File

@ -23,9 +23,7 @@ ActiveAdmin.register JamRuby::AffiliateTrafficTotal, :as => 'Affiliate Daily Sta
# default_actions # use this for all view/edit/delete links
column 'Day' do |oo| oo.day end
column 'Partner ID' do |oo| oo.affiliate_partner.id end
column 'Partner Name' do |oo| oo.affiliate_partner.display_name end
column 'Partner User' do |oo| link_to(oo.affiliate_partner.partner_user.name, admin_user_path(oo.affiliate_partner.partner_user.id), { :title => oo.affiliate_partner.partner_user.name }) end
column 'Partner' do |oo| link_to(oo.affiliate_partner.display_name, oo.affiliate_partner.admin_url, {:title => oo.affiliate_partner.display_name}) end
column 'Signups' do |oo| oo.signups end
column 'Visits' do |oo| oo.visits end
@ -33,16 +31,6 @@ ActiveAdmin.register JamRuby::AffiliateTrafficTotal, :as => 'Affiliate Daily Sta
controller do
def scoped_collection
rel = end_of_association_chain
.includes([:affiliate_partner])
.order('day DESC')
if (ref_id = params[AffiliatePartner::PARAM_REFERRAL]).present?
qq = ['affiliate_partner_id = ?', ref_id]
else
qq = ['affiliate_partner_id IS NOT NULL']
end
@users ||= rel.where(qq)
end
end
end

View File

@ -8,32 +8,27 @@ ActiveAdmin.register JamRuby::User, :as => 'Referrals' do
config.filters = true
filter :affiliate_referral
filter :email
## scope("Has Signups", default: true) { |scope| scope.where('visits != 0 or signups != 0').order('day desc') }
index do
column 'User' do |oo| link_to(oo.name, oo.admin_url, {:title => oo.name}) end
column 'User Email' do |oo| oo.email end
column 'Email' do |oo| oo.email end
column 'Created' do |oo| oo.created_at end
column 'Partner ID' do |oo| oo.affiliate_referral.id end
column 'Partner Name' do |oo| oo.affiliate_referral.display_name end
column 'Partner User' do |oo| link_to(oo.affiliate_referral.partner_user.name, admin_user_path(oo.affiliate_referral.partner_user.id), { :title => oo.affiliate_referral.partner_user.name }) end
column 'Partner' do |oo| oo.affiliate_referral.display_name end
end
controller do
def scoped_collection
rel = end_of_association_chain
.includes([:affiliate_referral])
.order('created_at DESC')
if (ref_id = params[AffiliatePartner::PARAM_REFERRAL]).present?
qq = ['affiliate_referral_id = ?', ref_id]
else
qq = ['affiliate_referral_id IS NOT NULL']
def scoped_collection
rel = end_of_association_chain
.includes([:affiliate_referral])
.order('created_at DESC')
if (ref_id = params[AffiliatePartner::PARAM_REFERRAL]).present?
qq = ['affiliate_referral_id = ?', ref_id]
else
qq = ['affiliate_referral_id IS NOT NULL']
end
@users ||= rel.where(qq)
end
@users ||= rel.where(qq)
end
end
end

View File

@ -5,35 +5,19 @@ ActiveAdmin.register JamRuby::AffiliatePartner, :as => 'Affiliates' do
config.sort_order = 'referral_user_count DESC'
config.batch_actions = false
# config.clear_action_items!
config.filters = true
config.per_page = 100
config.filters = false
config.per_page = 50
config.paginate = true
#form :partial => 'form'
#filter :partner_user
filter :partner_name
filter :id
filter :current_quarter_in_cents
filter :cumulative_earnings_in_cents
filter :jamtracks_sold
filter :subscriptions_count
filter :referral_user_count
scope("Sorted By Current Quarter", default: true) { |scope| scope.where('partner_user_id IS NOT NULL').order('current_quarter_in_cents desc') }
scope("Sorted By Jamtracks Sold", default: false) { |scope| scope.where('partner_user_id IS NOT NULL').order('jamtracks_sold desc') }
scope("Sorted By Subs", default: false) { |scope| scope.where('partner_user_id IS NOT NULL').order('subscriptions_count desc') }
scope("Sorted By Signups", default: false) { |scope| scope.where('partner_user_id IS NOT NULL').order('referral_user_count desc') }
scope("Sorted By Newest First") { |scope| scope.where('partner_user_id IS NOT NULL').order('id desc') }
scope("Any") { |scope| scope.where('partner_user_id IS NOT NULL').order('referral_user_count desc') }
scope("Active", default: true) { |scope| scope.where('partner_user_id IS NOT NULL').order('referral_user_count desc') }
scope("Unpaid") { |partner| partner.unpaid }
controller do
helper 'active_admin/subscription'
end
form do |f|
f.inputs 'Fields' do
f.input(:partner_name, :input_html => { :maxlength => 128 })
@ -61,47 +45,14 @@ ActiveAdmin.register JamRuby::AffiliatePartner, :as => 'Affiliates' do
column 'Code' do |oo|
oo.id
end
column 'Signups' do |oo|
column 'Referral Count' do |oo|
oo.referral_user_count
end
column 'JamTracks' do |oo|
oo.jamtracks_sold
end
column 'Subs' do |oo|
oo.subscriptions_count
end
column 'Cum Earnings' do |oo|
div do
sprintf("Tot $%.2f", oo.cumulative_earnings_in_dollars)
end
div do
sprintf("Jam $%.2f", oo.jamtrack_cumulative_earnings_in_dollars)
end
div do
sprintf("Sub $%.2f", oo.subscriptions_cumulative_earnings_in_dollars)
end
end
column 'Current Quarter' do |oo|
div do
sprintf("Tot $%.2f", oo.current_quarter_in_dollars)
end
div do
sprintf("Jam $%.2f", oo.jamtrack_current_quarter_in_dollars)
end
div do
sprintf("Sub $%.2f", oo.subscriptions_current_quarter_in_dollars)
end
column 'Earnings' do |oo|
sprintf("$%.2f", oo.cumulative_earnings_in_dollars)
end
column 'Amount Owed' do |oo|
div do
sprintf("Tot $%.2f", oo.due_amount_in_cents.to_f / 100.to_f)
end
div do
sprintf("Jam $%.2f", oo.jamtrack_due_amount_in_cents.to_f / 100.to_f)
end
div do
sprintf("Sub $%.2f", oo.subscription_due_amount_in_cents.to_f / 100.to_f)
end
sprintf("$%.2f", oo.due_amount_in_cents.to_f / 100.to_f)
end
column 'Pay Actions' do |oo|
link_to('Mark Paid', mark_paid_admin_affiliate_path(oo.id), :confirm => "Mark this affiliate as PAID?") if oo.unpaid
@ -120,15 +71,6 @@ ActiveAdmin.register JamRuby::AffiliatePartner, :as => 'Affiliates' do
row :address
row :tax_identifier
row :paypal_id
row :venmo_user_id
row :jamtracks_sold
row :subscriptions_count
row :cumulative_earnings_in_dollars
row :jamtrack_cumulative_earnings_in_dollars
row :subscriptions_cumulative_earnings_in_dollars
row :current_quarter_in_dollars
row :jamtrack_current_quarter_in_dollars
row :subscriptions_current_quarter_in_dollars
end

View File

@ -640,12 +640,7 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do
autocomplete :user, :email, :full => true, :display_value => :autocomplete_display_name, extra_data: [:last_jam_addr]
def get_autocomplete_items(parameters)
term = parameters[:term]
if term.include?('@')
User.select("email, first_name, last_name, id, last_jam_addr").where(["email = ?", term]).limit(5)
else
User.select("email, first_name, last_name, id, last_jam_addr").where(["email ILIKE ? OR first_name ILIKE ? OR last_name ILIKE ?", "%#{term}%", "%#{term}%", "%#{term}%"]).limit(40)
end
User.select("email, first_name, last_name, id, last_jam_addr").where(["email ILIKE ? OR first_name ILIKE ? OR last_name ILIKE ?", "%#{parameters[:term]}%", "%#{parameters[:term]}%", "%#{parameters[:term]}%"])
end

View File

@ -17,14 +17,6 @@ ActiveAdmin.register JamRuby::JamTrackRight, :as => 'JamTrackRights' do
filter :jam_track
controller do
def create
jt_params = params[:jam_ruby_jam_track_right]
jt_params[:jam_track] =JamRuby::JamTrack.where("id=?", jt_params[:jam_track_id_val]).first # jt_params[:jam_track_id_val]
jt_params[:user] = JamRuby::User.where("id=?", jt_params[:user_id_val]).first # jt_params[:user_id_val]
create!
end
end
index do
actions
@ -55,14 +47,9 @@ ActiveAdmin.register JamRuby::JamTrackRight, :as => 'JamTrackRights' do
form do |f|
f.inputs 'New Jam Track Right' do
#f.input :jam_track, :required=>true, collection: JamTrack.all, include_blank: false
f.input :jam_track_id_val, :required=>true, :as => :hidden
f.input :jam_track, :required=>true, :as => :autocomplete, :url => autocomplete_jam_track_name_admin_jam_tracks_path, hint: 'Select a jamtrack to give to this user'
#f.input :user, :required=>true, collection: User.all, include_blank: false
f.input :user_id_val, :required=>true, :as => :hidden
f.input :user, :required=>true, :as => :autocomplete, :url => autocomplete_user_email_admin_users_path, hint: 'Give a free jamtrack to this user'
f.input :can_download, :required => true, as: :boolean, :input_html => { :checked => 'checked' }
f.input :jam_track, :required=>true, collection: JamTrack.all, include_blank: false
f.input :user, :required=>true, collection: User.all, include_blank: false
f.input :can_download, :required => true, as: :boolean
end
f.actions
end

View File

@ -1,7 +1,5 @@
ActiveAdmin.register JamRuby::JamTrack, :as => 'JamTracks' do
collection_action :autocomplete_jam_track_name, :method => :get
menu :label => 'JamTracks', :parent => 'JamTracks'
config.sort_order = 'name_asc'
@ -21,19 +19,6 @@ ActiveAdmin.register JamRuby::JamTrack, :as => 'JamTracks' do
form :partial => 'form'
controller do
# this actually searches on first name, last name, and email, because of get_autocomplete_items defined below
autocomplete :jam_track, :name, :full => true, :display_value => :autocomplete_display_name
def get_autocomplete_items(parameters)
JamTrack.select("name, original_artist, id").where(["name ILIKE ? OR original_artist ILIKE ?", "%#{parameters[:term]}%", "%#{parameters[:term]}%"])
end
end
index do
# actions # use this for all view/edit/delete links

View File

@ -1,185 +0,0 @@
class Spacer
def self.spacer(val, row)
percentage = ((val * 100) / row.total.to_f).round(1).to_s
('%-5.5s' % percentage).gsub(' ', ' ') + '% - ' + val.to_s
end
end
=begin
select
count(id) as total,
count(first_downloaded_client_at) as downloaded,
count(first_ran_client_at) as ran_client,
count(first_certified_gear_at) as ftue,
count(first_music_session_at) as any_session,
count(first_real_music_session_at) as real_session,
count(first_good_music_session_at) as good_session,
count(first_invited_at) as invited,
count(first_friended_at) as friended,
count(first_subscribed_at) as subscribed
from users where users.created_at >= '2024-11-01' AND users.created_at < '2025-04-01'
select first_name, last_name, email
from users where users.created_at >= '2024-11-01' AND users.created_at < '2025-04-01'
AND first_music_session_at is NULL;
=end
ActiveAdmin.register_page "Jammers Subscription Cohorts" do
menu :parent => 'Reports'
content :title => "Jammers Subscription Cohorts" do
filter_type = params[:filter_type] || 'All'
filter_campaign = params[:filter_campaign]
filter_campaign_id = params[:filter_campaign_id]
filter_ad_set = params[:filter_ad_set]
filter_ad_name = params[:filter_ad_name]
campaigns = User.where("origin_utm_medium = 'cpc'").distinct.pluck(:origin_utm_campaign).compact.sort
campaign_ids = User.where("origin_utm_medium = 'cpc'").distinct.pluck(:origin_id).compact.sort
ad_sets = User.where("origin_utm_medium = 'cpc'").distinct.pluck(:origin_term).compact.sort
ad_names = User.where("origin_utm_medium = 'cpc'").distinct.pluck(:origin_content).compact.sort
div style: "margin-bottom: 20px; padding: 10px; background-color: #f4f4f4; border-radius: 4px;" do
form action: admin_jammers_subscription_cohorts_path, method: :get do
span "Source: ", style: "font-weight: bold; margin-right: 5px;"
select name: 'filter_type', onchange: 'this.form.submit()', style: "margin-right: 15px;" do
option "All", value: 'All', selected: filter_type == 'All'
option "Organic", value: 'Organic', selected: filter_type == 'Organic'
option "Advertising", value: 'Advertising', selected: filter_type == 'Advertising'
end
if filter_type == 'Advertising'
div style: "margin-top: 10px;" do
span "Campaign Name: ", style: "font-weight: bold; margin-right: 5px;"
select name: 'filter_campaign', onchange: 'this.form.submit()', style: "margin-right: 15px;" do
option "All", value: ''
option "Null", value: 'NULL', selected: filter_campaign == 'NULL'
campaigns.each do |c|
option c, value: c, selected: filter_campaign == c
end
end
end
div style: "margin-top: 10px;" do
span "Campaign ID: ", style: "font-weight: bold; margin-right: 5px;"
select name: 'filter_campaign_id', onchange: 'this.form.submit()', style: "margin-right: 15px;" do
option "All", value: ''
option "Null", value: 'NULL', selected: filter_campaign_id == 'NULL'
campaign_ids.each do |c|
option c, value: c, selected: filter_campaign_id == c
end
end
end
div style: "margin-top: 10px;" do
span "Ad Set: ", style: "font-weight: bold; margin-right: 5px;"
select name: 'filter_ad_set', onchange: 'this.form.submit()', style: "margin-right: 15px;" do
option "All", value: ''
option "Null", value: 'NULL', selected: filter_ad_set == 'NULL'
ad_sets.each do |c|
option c, value: c, selected: filter_ad_set == c
end
end
div style: "margin-top: 10px;" do
span "Ad Name: ", style: "font-weight: bold; margin-right: 5px;"
select name: 'filter_ad_name', onchange: 'this.form.submit()', style: "margin-right: 15px;" do
option "All", value: ''
option "Null", value: 'NULL', selected: filter_ad_name == 'NULL'
ad_names.each do |c|
option c, value: c, selected: filter_ad_name == c
end
end
end
end
end
noscript { input type: :submit, value: "Filter" }
end
end
h2 "Users Grouped By Month as Paying Subscribers"
query = User.select(%Q{date_trunc('month', users.created_at) as month,
count(id) as total,
count(first_downloaded_client_at) as downloaded,
count(first_ran_client_at) as ran_client,
count(first_certified_gear_at) as ftue,
count(first_music_session_at) as any_session,
count(first_real_music_session_at) as real_session,
count(first_good_music_session_at) as good_session,
count(first_invited_at) as invited,
count(first_friended_at) as friended,
count(first_subscribed_at) as subscribed,
count(first_played_jamtrack_at) as played_jamtrack
})
.joins(%Q{LEFT JOIN LATERAL (
SELECT
j.created_at
FROM
jam_track_rights as j
WHERE
j.user_id = users.id
ORDER BY
j.created_at
LIMIT 1 -- Select only that single row
) j ON TRUE })
if filter_type == 'Organic'
query = query.where("users.origin_utm_source = 'organic'")
elsif filter_type == 'Advertising'
query = query.where("origin_utm_medium = 'cpc'")
if filter_campaign.present?
if filter_campaign == 'NULL'
query = query.where("users.origin_utm_campaign IS NULL")
elsif filter_campaign != 'All'
query = query.where("users.origin_utm_campaign = ?", filter_campaign)
end
end
if filter_campaign_id.present?
if filter_campaign_id == 'NULL'
query = query.where("users.origin_id IS NULL")
elsif filter_campaign_id != 'All'
query = query.where("users.origin_id = ?", filter_campaign_id)
end
end
if filter_ad_set.present?
if filter_ad_set == 'NULL'
query = query.where("users.origin_term IS NULL")
elsif filter_ad_set != 'All'
query = query.where("users.origin_term = ?", filter_ad_set)
end
end
if filter_ad_name.present?
if filter_ad_name == 'NULL'
query = query.where("users.origin_content IS NULL")
elsif filter_ad_name != 'All'
query = query.where("users.origin_content = ?", filter_ad_name)
end
end
end
table_for query.group("date_trunc('month', users.created_at)")
.where("j.created_at IS NULL OR (j.created_at - users.created_at) >= INTERVAL '2 hours'")
.order("date_trunc('month', users.created_at) DESC") do |row|
column "Month", Proc.new { |user| user.month.strftime('%B %Y') }
column "Total", :total
column "Subscribed", Proc.new { |user| raw(Spacer.spacer(user.subscribed, user)) }
column "Downloaded", Proc.new { |user| raw(Spacer.spacer(user.downloaded, user)) }
column "Ran Client", Proc.new { |user| raw(Spacer.spacer(user.ran_client, user)) }
column "FTUE", Proc.new { |user| raw(Spacer.spacer(user.ftue, user)) }
column "Any Session", Proc.new { |user| raw(Spacer.spacer(user.any_session, user)) }
column "2+ Session", Proc.new { |user| raw(Spacer.spacer(user.real_session, user)) }
column "Good Session", Proc.new { |user| raw(Spacer.spacer(user.good_session, user)) }
column "Invited", Proc.new { |user| raw(Spacer.spacer(user.invited, user)) }
column "Friended", Proc.new { |user| raw(Spacer.spacer(user.friended, user)) }
column "Played JT", Proc.new { |user| raw(Spacer.spacer(user.played_jamtrack, user)) }
end
end
end

View File

@ -1,73 +0,0 @@
class Spacer
def self.spacer(val, row)
percentage = ((val * 100) / row.total.to_f).round(1).to_s
('%-5.5s' % percentage).gsub(' ', '&nbsp;') + '%&nbsp;-&nbsp;' + val.to_s
end
end
=begin
select
count(id) as total,
count(first_downloaded_client_at) as downloaded,
count(first_ran_client_at) as ran_client,
count(first_certified_gear_at) as ftue,
count(first_music_session_at) as any_session,
count(first_real_music_session_at) as real_session,
count(first_good_music_session_at) as good_session,
count(first_invited_at) as invited,
count(first_friended_at) as friended,
count(first_subscribed_at) as subscribed
from users where users.created_at >= '2024-11-01' AND users.created_at < '2025-04-01'
select first_name, last_name, email
from users where users.created_at >= '2024-11-01' AND users.created_at < '2025-04-01'
AND first_music_session_at is NULL;
=end
ActiveAdmin.register_page "JamTrack Subscription Cohorts" do
menu :parent => 'Reports'
content :title => "JamTrack Subscription Cohorts" do
h2 "Users Grouped By Month as Paying Subscribers"
table_for User.select(%Q{date_trunc('month', users.created_at) as month,
count(id) as total,
count(first_downloaded_client_at) as downloaded,
count(first_ran_client_at) as ran_client,
count(first_certified_gear_at) as ftue,
count(first_music_session_at) as any_session,
count(first_real_music_session_at) as real_session,
count(first_good_music_session_at) as good_session,
count(first_invited_at) as invited,
count(first_friended_at) as friended,
count(first_subscribed_at) as subscribed,
count(first_played_jamtrack_at) as played_jamtrack
})
.joins(%Q{INNER JOIN LATERAL (
SELECT
j.created_at
FROM
jam_track_rights as j
WHERE
j.user_id = users.id
ORDER BY
j.created_at
LIMIT 1 -- Select only that single row
) j ON (j.created_at - users.created_at) < INTERVAL '2 hours' })
.group("date_trunc('month', users.created_at)").order("date_trunc('month', users.created_at) DESC") do |row|
column "Month", Proc.new { |user| user.month.strftime('%B %Y') }
column "Total", :total
column "Subscribed", Proc.new { |user| raw(Spacer.spacer(user.subscribed, user)) }
column "Downloaded", Proc.new { |user| raw(Spacer.spacer(user.downloaded, user)) }
column "Ran Client", Proc.new { |user| raw(Spacer.spacer(user.ran_client, user)) }
column "FTUE", Proc.new { |user| raw(Spacer.spacer(user.ftue, user)) }
column "Any Session", Proc.new { |user| raw(Spacer.spacer(user.any_session, user)) }
column "2+ Session", Proc.new { |user| raw(Spacer.spacer(user.real_session, user)) }
column "Good Session", Proc.new { |user| raw(Spacer.spacer(user.good_session, user)) }
column "Invited", Proc.new { |user| raw(Spacer.spacer(user.invited, user)) }
column "Friended", Proc.new { |user| raw(Spacer.spacer(user.friended, user)) }
column "Played JT", Proc.new { |user| raw(Spacer.spacer(user.played_jamtrack, user)) }
end
end
end

View File

@ -0,0 +1,41 @@
class Spacer
def self.spacer(val, row)
percentage = ((val * 100) / row.total.to_f).round(1).to_s
('%-5.5s' % percentage).gsub(' ', '&nbsp;') + '%&nbsp;-&nbsp;' + val.to_s
end
end
ActiveAdmin.register_page "Subscription Cohorts" do
menu :parent => 'Reports'
content :title => "Subscription Cohorts" do
h2 "Users Grouped By Month as Paying Subscribers"
table_for User.select(%Q{date_trunc('month', created_at) as month,
count(id) as total,
count(first_downloaded_client_at) as downloaded,
count(first_ran_client_at) as ran_client,
count(first_certified_gear_at) as ftue,
count(first_music_session_at) as any_session,
count(first_real_music_session_at) as real_session,
count(first_good_music_session_at) as good_session,
count(first_invited_at) as invited,
count(first_friended_at) as friended,
count(first_subscribed_at) as subscribed
}).group("date_trunc('month', created_at)").order("date_trunc('month', created_at) DESC") do |row|
column "Month", Proc.new { |user| user.month.strftime('%B %Y') }
column "Total", :total
column "Subscribed", Proc.new { |user| raw(Spacer.spacer(user.subscribed, user)) }
column "Downloaded", Proc.new { |user| raw(Spacer.spacer(user.downloaded, user)) }
column "Ran Client", Proc.new { |user| raw(Spacer.spacer(user.ran_client, user)) }
column "FTUE", Proc.new { |user| raw(Spacer.spacer(user.ftue, user)) }
column "Any Session", Proc.new { |user| raw(Spacer.spacer(user.any_session, user)) }
column "2+ Session", Proc.new { |user| raw(Spacer.spacer(user.real_session, user)) }
column "Good Session", Proc.new { |user| raw(Spacer.spacer(user.good_session, user)) }
column "Invited", Proc.new { |user| raw(Spacer.spacer(user.invited, user)) }
column "Friended", Proc.new { |user| raw(Spacer.spacer(user.friended, user)) }
end
end
end

View File

@ -6,18 +6,15 @@ ActiveAdmin.register JamRuby::User, :as => 'UserSource' do
config.batch_actions = false
config.clear_action_items!
config.filters = false
config.per_page = 250
scope("Paid", default: true) { |scope| scope.unscoped.where(:origin_utm_medium => 'cpc').order('created_at desc') }
scope("Inorganic Source") { |scope| scope.unscoped.where("origin_utm_source != 'organic' OR origin_utm_source IS NULL").order('created_at desc') }
scope("Include Organic") { |scope| scope.unscoped.order('created_at desc') }
scope("Most Recent First", default: true) { |scope| scope.unscoped.order('created_at desc')}
index do
column "Email" do |user|
user.email
end
column "Signup (CST)" do |user|
user.created_at.in_time_zone("Central Time (US & Canada)")
column "Bought TestDrive" do |user|
!user.most_recent_test_drive_purchase.nil? ? "Yes" : "No"
end
column "UTM Source" do |user|
user.origin_utm_source
@ -28,23 +25,8 @@ ActiveAdmin.register JamRuby::User, :as => 'UserSource' do
column "UTM Campaign" do |user|
user.origin_utm_campaign
end
column "UTM ID" do |user|
user.origin_id
end
column "UTM Term" do |user|
user.origin_term
end
column "UTM Content" do |user|
user.origin_content
end
column "Referrer" do |user|
user.origin_referrer
end
column "FB Click ID" do |user|
user.facebook_click_id
end
column "FB Browser ID" do |user|
user.facebook_browser_id
end
end
end

View File

@ -7,38 +7,6 @@ function intToIP(int) {
return part4 + "." + part3 + "." + part2 + "." + part1;
}
function handleJamTrackRightsForm() {
var $jamTrackRights = $('form#new_jam_ruby_jam_track_right');
var $jamTrack = $jamTrackRights.find('#jam_ruby_jam_track_right_jam_track_id');
var $jamTrackVal = $jamTrackRights.find('#jam_ruby_jam_track_right_jam_track_id_val')
var $user = $jamTrackRights.find('#jam_ruby_jam_track_right_user_id');
var $userVal = $jamTrackRights.find('#jam_ruby_jam_track_right_user_id_val');
$jamTrack.on('change', function(){
console.log("change jam track");
});
/**
$user.on('change', function(){
console.log("change user");
});
$jamTrack.on('focus', function(){
$userVal.val('')
});*/
$jamTrack.bind('railsAutocomplete.select', function(event, data){
$jamTrackVal.val('');
$jamTrackVal.val(data.item.id);
console.log("jam track selected with id " + data.item.id);
});
$user.bind('railsAutocomplete.select', function(event, data){
$userVal.val('');
$userVal.val(data.item.id);
console.log("user selected with id " + data.item.id);
});
}
function handleUserLatencyForm(){
var $userLatenciesForm = $('form#user_latencies_form');
var $latenciesMyUser = $userLatenciesForm.find('#latencies_my_user');
@ -106,5 +74,4 @@ function handleLatencyRecommendationForm(){
$(document).ready(function() {
handleUserLatencyForm();
handleLatencyRecommendationForm();
handleJamTrackRightsForm();
});

View File

@ -2,97 +2,55 @@ class ArsesController < ApplicationController
respond_to :json
def index
if params[:code] != Rails.application.config.data_dump_code
render :json => {error: "Unauthorized"}, :status => 401
return
end
@arses = JamRuby::Ars.all
render :json => @arses
end
def update
if params[:code] != Rails.application.config.data_dump_code
render :json => {error: "Unauthorized"}, :status => 401
return
end
begin
# Primary ID lookup
@ars = JamRuby::Ars.find_by_id(params[:id])
# Explicit secondary lookups if primary ID fails
@ars ||= JamRuby::Ars.find_by_id_int(params[:id_int]) if params[:id_int]
@ars ||= JamRuby::Ars.find_by_name(params[:name]) if params[:name]
if @ars.nil?
render :json => {error: "Not Found"}, :status => 404
return
end
allowed = [:password, :username, :active, :beta, :name, :provider, :id_int, :ip, :port, :continent, :country, :city, :subdivision, :latitude, :longitude]
update_hash = {}
allowed.each do |attr|
update_hash[attr] = params[attr] if params.has_key?(attr)
end
if @ars.update_attributes(update_hash, as: :admin)
render :json => @ars, :status => :ok
else
render :json => @ars.errors, :status => :unprocessable_entity
end
rescue => e
render :json => {error: e.message, backtrace: e.backtrace.first(5)}, :status => 500
end
end
# create or update a client_artifact row
def get_or_create
begin
name = params[:name]
provider = params[:provider]
active = params[:active]
beta = params.has_key?(:beta) ? params[:beta] : true
ip = params[:ip]
username = params[:username]
password = params[:password]
topology = params[:topology]
ars_id = params[:ars_id]
name = params[:name]
provider = params[:provider]
active = params[:active]
ip = params[:ip]
username = params[:username]
password = params[:password]
topology = params[:topology]
ars_id = params[:ars_id]
puts "TOPOLOGY #{topology}"
# Explicit field-based lookups
ars = nil
ars = JamRuby::Ars.find_by_id_int(ars_id) if ars_id
ars ||= JamRuby::Ars.find_by_name(name) if name
if ars.nil?
ars = JamRuby::Ars.new
ars.name = name
end
ars.id_int = ars_id if !ars_id.nil?
ars.provider = provider
ars.active = active
ars.beta = params[:beta]
ars.beta = beta
ars.ip = ip
ars.password = password
ars.username = username
if topology
ars.city = topology['city']
ars.country = topology['country']
ars.continent = topology['continent']
ars.latitude = topology['latitude']
ars.longitude = topology['longitude']
ars.subdivision = topology['subdivision']
end
ars.save!
@ars = ars
render :json => {id_int: @ars.id_int, id: @ars.id, name: @ars.name, provider: @ars.provider, active: @ars.active, beta: @ars.beta, ip: @ars.ip}, :status => :ok
rescue => e
render :json => {error: e.message, backtrace: e.backtrace.first(5)}, :status => 500
if ars_id
ars = Ars.find_by_id_int(ars_id)
end
if ars.nil?
ars = Ars.new
ars.name = name
ars.id_int = ars_id if !ars_id.nil?
end
ars.provider = provider
ars.active = active
ars.ip = ip
ars.password = password
ars.username = username
if topology
ars.city = topology['city']
ars.country = topology['country']
ars.continent = topology['continent']
ars.latitude = topology['latitude']
ars.longitude = topology['longitude']
ars.subdivision = topology['subdivision']
end
ars.save
@ars = ars
unless @ars.errors.any?
if ars_id.nil?
ars.reload
ars_id = ars.id_int
end
@ars = Ars.find_by_id_int(ars_id)
render :json => {id_int: @ars.id_int, id: @ars.id, name: @ars.name, provider: @ars.provider, active: @ars.active, ip: @ars.ip}, :status => :ok
else
response.status = :unprocessable_entity
respond_with @ars
end
end
end

View File

@ -112,19 +112,6 @@ class Cohort < ActiveRecord::Base
def self.cohort_users(cohort)
User.where(created_at: cohort.group_start..cohort.group_end)
end
=begin
SELECT played.user_id FROM
(SELECT user_id, COUNT(*) cnt FROM music_sessions_user_history msuh1
WHERE
msuh1.created_at >= '2024-11-01' AND
msuh1.created_at <= '202' AND
EXTRACT(EPOCH FROM (msuh1.session_removed_at - msuh1.created_at)) >= 900 AND
(SELECT COUNT(*) FROM music_sessions_user_history msuh2
WHERE msuh1.music_session_id = msuh2.music_session_id
) > 1
GROUP BY user_id
) played
=end
def _played_online_subquery(constraint)
where = if constraint.is_a?(Range)

View File

@ -1 +0,0 @@
BUNDLE_GEMFILE=Gemfile.alt RAILS_ENV=development LOCAL_DEV=1 MODERN_OS=1 JAM_RUBY_VERSION=2.4.1 bundle _1.17.3_ exec rails server -b 0.0.0.0 -p 3333

View File

@ -1,18 +0,0 @@
class JamRuby::JamTrackRight
attr_accessible :jam_track, :user, :jam_track_id_val, :user_id_val, as: :admin
def jam_track_id_val
end
def jam_track_id_val=(val)
end
def user_id_val
end
def user_id_val=(val)
end
end

View File

@ -3,8 +3,4 @@ class JamRuby::JamTrack
# add a custom validation
def autocomplete_display_name
"#{original_artist} - #{name}"
end
end

View File

@ -20,10 +20,6 @@ JamAdmin::Application.routes.draw do
post :user_latencies, on: :collection
post :user_latency_recommendation, on: :collection
end
resources :jam_tracks do
get :autocomplete_jam_track_name, :on => :collection
end
end
namespace :admin do
@ -44,8 +40,6 @@ JamAdmin::Application.routes.draw do
match '/api/jam_tracks/released' => 'jam_track#dump_released', :via => :get, as: 'released_jamtracks_csv'
match '/api/arses/register' => 'arses#get_or_create', :via => :post
match '/api/arses' => 'arses#index', :via => :get
match '/api/arses/:id' => 'arses#update', :via => :post
mount Resque::Server.new, :at => "/resque"

View File

@ -1 +0,0 @@
# trigger build

View File

@ -11,10 +11,10 @@ pipelines:
- popd
- pushd jam-ui/cicd
- npm install
- NODE_ENV=production PUBLIC_URL=https://staging.jamkazam.com REACT_APP_ORIGIN=staging.jamkazam.com REACT_APP_BASE_URL=https://staging.jamkazam.com REACT_APP_CLIENT_BASE_URL=https://staging.jamkazam.com REACT_APP_API_BASE_URL=https://staging.jamkazam.com/api REACT_APP_BITBUCKET_BUILD_NUMBER=$BITBUCKET_BUILD_NUMBER REACT_APP_BITBUCKET_COMMIT=$BITBUCKET_COMMIT REACT_APP_GOOGLE_ANALYTICS_ID=G-8W0GTL53NT ENVIRONMENT=staging ./generate.sh
- NODE_ENV=production REACT_APP_ORIGIN=staging.jamkazam.com REACT_APP_CLIENT_BASE_URL=https://staging.jamkazam.com REACT_APP_API_BASE_URL=https://staging.jamkazam.com/api REACT_APP_BITBUCKET_BUILD_NUMBER=$BITBUCKET_BUILD_NUMBER REACT_APP_BITBUCKET_COMMIT=$BITBUCKET_COMMIT ENVIRONMENT=staging ./generate.sh
- popd
- cd jam-ui
- NODE_ENV=production CI=false PUBLIC_URL=https://staging.jamkazam.com REACT_APP_ORIGIN=staging.jamkazam.com REACT_APP_CLIENT_BASE_URL=https://staging.jamkazam.com REACT_APP_BASE_URL=https://staging.jamkazam.com REACT_APP_API_BASE_URL=https://staging.jamkazam.com/api REACT_APP_BITBUCKET_BUILD_NUMBER=$BITBUCKET_BUILD_NUMBER REACT_APP_BITBUCKET_COMMIT=$BITBUCKET_COMMIT REACT_APP_GOOGLE_ANALYTICS_ID=G-8W0GTL53NT npm run build
- NODE_ENV=production CI=false REACT_APP_ORIGIN=staging.jamkazam.com REACT_APP_CLIENT_BASE_URL=https://staging.jamkazam.com REACT_APP_API_BASE_URL=https://staging.jamkazam.com/api REACT_APP_BITBUCKET_BUILD_NUMBER=$BITBUCKET_BUILD_NUMBER REACT_APP_BITBUCKET_COMMIT=$BITBUCKET_COMMIT npm run build
artifacts:
- jam-ui/build/**
- step:
@ -58,10 +58,10 @@ pipelines:
- popd
- pushd jam-ui/cicd
- npm install
- NODE_ENV=production ENVIRONMENT=production PUBLIC_URL=https://www.jamkazam.com REACT_APP_ORIGIN=jamkazam.com REACT_APP_BASE_URL=https://www.jamkazam.com REACT_APP_CLIENT_BASE_URL=https://www.jamkazam.com REACT_APP_API_BASE_URL=https://www.jamkazam.com/api REACT_APP_BITBUCKET_BUILD_NUMBER=$BITBUCKET_BUILD_NUMBER REACT_APP_BITBUCKET_COMMIT=$BITBUCKET_COMMIT REACT_APP_GOOGLE_ANALYTICS_ID=G-SPTNJRW7WB ./generate.sh
- NODE_ENV=production ENVIRONMENT=production REACT_APP_ORIGIN=jamkazam.com REACT_APP_CLIENT_BASE_URL=https://www.jamkazam.com REACT_APP_API_BASE_URL=https://www.jamkazam.com/api REACT_APP_BITBUCKET_BUILD_NUMBER=$BITBUCKET_BUILD_NUMBER REACT_APP_BITBUCKET_COMMIT=$BITBUCKET_COMMIT ./generate.sh
- popd
- cd jam-ui
- NODE_ENV=production CI=false PUBLIC_URL=https://www.jamkazam.com REACT_APP_ORIGIN=jamkazam.com REACT_APP_BASE_URL=https://www.jamkazam.com REACT_APP_CLIENT_BASE_URL=https://www.jamkazam.com REACT_APP_API_BASE_URL=https://www.jamkazam.com/api REACT_APP_BITBUCKET_BUILD_NUMBER=$BITBUCKET_BUILD_NUMBER REACT_APP_BITBUCKET_COMMIT=$BITBUCKET_COMMIT REACT_APP_GOOGLE_ANALYTICS_ID=G-SPTNJRW7WB npm run build
- NODE_ENV=production CI=false REACT_APP_ORIGIN=jamkazam.com REACT_APP_CLIENT_BASE_URL=https://www.jamkazam.com REACT_APP_API_BASE_URL=https://www.jamkazam.com/api REACT_APP_BITBUCKET_BUILD_NUMBER=$BITBUCKET_BUILD_NUMBER REACT_APP_BITBUCKET_COMMIT=$BITBUCKET_COMMIT npm run build
artifacts:
- jam-ui/build/**

View File

@ -9,8 +9,5 @@ REACT_APP_BITBUCKET_COMMIT=dev
REACT_APP_ENV=development
REACT_APP_RECAPTCHA_ENABLED=false
REACT_APP_SITE_KEY=6Let8dgSAAAAAFheKGWrs6iaq_hIlPOZ2f3Bb56B
REACT_APP_GOOGLE_ANALYTICS_ID=G-MC9BTWXWY4
PUBLIC_URL=
REACT_APP_COOKIE_DOMAIN=.jamkazam.local
REACT_APP_RECURLY_PUBLIC_API_KEY=ewr1-hvDV1xQxDw0HPaaRFP4KNE
REACT_APP_BRAINTREE_TOKEN=sandbox_pgjp8dvs_5v5rwm94m2vrfbms

View File

@ -7,7 +7,4 @@ REACT_APP_API_BASE_URL=http://www.jamkazam.local:3000/api
REACT_APP_BITBUCKET_BUILD_NUMBER=dev
REACT_APP_BITBUCKET_COMMIT=dev
REACT_APP_ENV=development
REACT_APP_COOKIE_DOMAIN=.jamkazam.com
REACT_APP_GOOGLE_ANALYTICS_ID=G-MC9BTWXWY4
REACT_APP_RECURLY_PUBLIC_API_KEY=
REACT_APP_BRAINTREE_TOKEN=
REACT_APP_COOKIE_DOMAIN=.jamkazam.com

View File

@ -8,6 +8,3 @@ REACT_APP_ENV=production
REACT_APP_RECAPTCHA_ENABLED=true
REACT_APP_SITE_KEY=6Let8dgSAAAAAFheKGWrs6iaq_hIlPOZ2f3Bb56B
REACT_APP_COOKIE_DOMAIN=.jamkazam.com
REACT_APP_GOOGLE_ANALYTICS_ID=G-SPTNJRW7WB
REACT_APP_RECURLY_PUBLIC_API_KEY=ewr1-hvDV1xQxDw0HPaaRFP4KNE
REACT_APP_BRAINTREE_TOKEN=production_hc7z69yq_pwwc6zm3d478kfrh

View File

@ -8,6 +8,3 @@ REACT_APP_ENV=staging
REACT_APP_RECAPTCHA_ENABLED=false
REACT_APP_SITE_KEY=6Let8dgSAAAAAFheKGWrs6iaq_hIlPOZ2f3Bb56B
REACT_APP_COOKIE_DOMAIN=.staging.jamkazam.com
REACT_APP_GOOGLE_ANALYTICS_ID=G-8W0GTL53NT
REACT_APP_RECURLY_PUBLIC_API_KEY=ewr1-AjUHUfcLtIsPdtetD4mj2x
REACT_APP_BRAINTREE_TOKEN=sandbox_pgjp8dvs_5v5rwm94m2vrfbms

View File

@ -38,19 +38,3 @@ npm run start
This will open it in a borwser window at http://beta.jamkazam.local:3000. Of course for it to work you also need Rails (web) app and websocket app (websocket-gateway) running.
## Working with JamTracks
if you have the latest from develop, you can go:
```
cd cicd
npm install
./export_personal_jamtracks.sh
./generate.js
open http://beta.jamkazam.local:4000/backing-tracks/ac-dc/back-in-black.html
```
You can also do none of the above, and go straight to:
http://beta.jamkazam.local:4000/public/backing-tracks/ac-dc/back-in-black
I tried to make it so the SPA has 'secret routes' to these pages, which is convenient for us dev & testing
but also tried to make it convenient to run the cicd approach of actually generating separate pages for each landing page (which is what those 5 steps cover)

View File

@ -10,49 +10,6 @@ const ArtistTemplatePageModule = require("./build/components/jamtracks/JKJamTrac
var csvFilePath = `jamtracks-for-env/jam_tracks_for_jam_ui.${process.env.USER}.csv`
var artistCsvFilePath = `jamtracks-for-env/jam_tracks_for_jam_ui_artists.${process.env.USER}.csv`
var sitemapPath = path.join(__dirname, "..", "public", "sitemap.xml");
const clear_sitemap = () => {
fs.writeFileSync(sitemapPath, "");
// Add the root element
fs.writeFileSync(sitemapPath, `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n`, { flag: 'a' });
// Add the root url
fs.writeFileSync(sitemapPath, `<url><loc>${process.env.REACT_APP_BASE_URL}</loc></url>\n`, { flag: 'a' });
// Add standard URLs specific to this site, such as:
// All prefix with /public
fs.writeFileSync(sitemapPath, `<url><loc>${process.env.REACT_APP_BASE_URL}/</loc></url>\n`, { flag: 'a' });
fs.writeFileSync(sitemapPath, `<url><loc>${process.env.REACT_APP_BASE_URL}/public/privacy</loc></url>\n`, { flag: 'a' } );
fs.writeFileSync(sitemapPath, `<url><loc>${process.env.REACT_APP_BASE_URL}/public/help</loc></url>\n`, { flag: 'a' } );
fs.writeFileSync(sitemapPath, `<url><loc>${process.env.REACT_APP_BASE_URL}/public/knowledge-base</loc></url>\n`, { flag: 'a' } );
fs.writeFileSync(sitemapPath, `<url><loc>${process.env.REACT_APP_BASE_URL}/public/help-desk</loc></url>\n`, { flag: 'a' } );
fs.writeFileSync(sitemapPath, `<url><loc>${process.env.REACT_APP_BASE_URL}/public/forum</loc></url>\n`, { flag: 'a' } );
fs.writeFileSync(sitemapPath, `<url><loc>${process.env.REACT_APP_BASE_URL}/public/unsubscribe</loc></url>\n`, { flag: 'a' } );
fs.writeFileSync(sitemapPath, `<url><loc>${process.env.REACT_APP_BASE_URL}/public/downloads</loc></url>\n`, { flag: 'a' } );
fs.writeFileSync(sitemapPath, `<url><loc>${process.env.REACT_APP_BASE_URL}/public/downloads-legacy</loc></url>\n`, { flag: 'a' } );
fs.writeFileSync(sitemapPath, `<url><loc>${process.env.REACT_APP_BASE_URL}/auth/login</loc></url>\n`, { flag: 'a' });
fs.writeFileSync(sitemapPath, `<url><loc>${process.env.REACT_APP_BASE_URL}/auth/signup</loc></url>\n`, { flag: 'a' } );
fs.writeFileSync(sitemapPath, `<url><loc>${process.env.REACT_APP_BASE_URL}/auth/forget-password</loc></url>\n`, { flag: 'a' } );
// Add the closing root element
}
const add_song_to_sitemap = (artistSlug, songSlug) => {
fs.writeFileSync(sitemapPath, `<url><loc>${process.env.REACT_APP_BASE_URL}/backing-tracks/${artistSlug}/${songSlug}</loc></url>\n`, { flag: 'a' });
}
const add_artist_to_sitemap = (artistSlug) => {
fs.writeFileSync(sitemapPath, `<url><loc>${process.env.REACT_APP_BASE_URL}/backing-tracks/${artistSlug}</loc></url>\n`, { flag: 'a' });
}
const close_sitemap = () => {
fs.writeFileSync(sitemapPath, "</urlset>", { flag: 'a' } );
}
/**
* Loads a CSV file into an array of objects.
* @param {string} csvPath - The path to the CSV file.
@ -99,11 +56,9 @@ const init = () => {
if (!process.env.PUBLIC_URL) {
console.log("setting public url", process.env.REACT_APP_BASE_URL);
process.env.PUBLIC_URL = process.env.REACT_APP_BASE_URL;
process.env.PUBLIC_URL = "";
}
clear_sitemap();
//const __dirname = path.resolve(path.dirname(''));
@ -139,8 +94,6 @@ const generateSongPages = async (render) => {
const fullPath = process.env.REACT_APP_BASE_URL + location;
const logoPath = process.env.REACT_APP_BASE_URL + "/favicon.svg";
add_song_to_sitemap(original_artist_slug, name_slug);
console.log(`Generating ${artist} - ${song}`);
const html = render
@ -256,7 +209,6 @@ const generateArtistPages = async (render) => {
const fullPath = process.env.REACT_APP_BASE_URL + location;
const logoPath = process.env.REACT_APP_BASE_URL + "/favicon.svg";
add_artist_to_sitemap(original_artist_slug);
console.log(`Generating ${artist}`);
const songs = matchingSongs.map((song) => {
@ -320,8 +272,6 @@ const generateArtistPages = async (render) => {
console.log(`Generated: ${outputFilePath}`);
}
close_sitemap();
console.log("All pages generated!");
});
};

View File

@ -4,7 +4,7 @@ const Dotenv = require("dotenv-webpack");
const environment = process.env.ENVIRONMENT || 'development';
const node_env = process.env.NODE_ENV || 'development';
const debug = node_env == 'development';
module.exports = {
entry: "./client-hydrate.js",
output: {
@ -42,7 +42,7 @@ module.exports = {
react: path.resolve('./node_modules/react'),
}
},
devtool: debug ? 'eval-source-map' : 'source-map',
devtool: 'eval-source-map',
mode: node_env,
plugins: [
new Dotenv({

View File

@ -1,29 +0,0 @@
/// <reference types="cypress" />
import makeFakeUser from '../../factories/user';
describe('Change Email Confirm Page', () => {
beforeEach(() => {
// Log in to the application or navigate to the account page
// where the change email feature is available
const currentUser = makeFakeUser({
email: 'sam@example.com'
});
cy.stubAuthenticate({ ...currentUser });
cy.intercept('POST', /\S+\/update_email/, { statusCode: 200, body: { ok: true } });
});
it('should display the confirm page when visiting the confirm URL', () => {
// Replace with a realistic token for your app if needed
const token = 'dummy-confirm-token';
// Visit the confirm URL
cy.visit(`/confirm-email-change?token=${token}`);
// Assert that the JKChangeEmailConfirm page is rendered
// Adjust selectors/texts as per your actual component
cy.contains('Change Email Confirmation').should('be.visible');
cy.contains('Loading...').should('be.visible');
// Optionally, check for the success message after the email update
cy.wait(1000); // Wait for the email update to complete
cy.contains('Your email has been successfully updated.').should('be.visible');
});
});

View File

@ -1,31 +1,23 @@
/// <reference types="cypress" />
import makeFakeUser from '../../factories/user';
describe('Unsubscribe from email link', () => {
beforeEach(() => {
// cy.intercept('POST', /\S+\/unsubscribe_user_match\/\S+/, { statusCode: 200, body: { ok: true } });
const currentUser = makeFakeUser({
email: 'sam@example.com'
});
cy.stubAuthenticate({ ...currentUser });
cy.intercept('POST', /\S+\/unsubscribe\/\S+/, { statusCode: 200, body: { ok: true } });
beforeEach(() =>{
cy.intercept('POST', /\S+\/unsubscribe_user_match\/\S+/, { statusCode: 200, body: { ok: true } });
})
it("redirect to home page if tok is not provided", () => {
cy.visit('/unsubscribe');
cy.location('pathname').should('eq', '/errors/404');
cy.location('pathname').should('eq', '/');
});
it("show unsubscribed message", () => {
cy.visit('/unsubscribe/123');
// cy.location('search')
// .should('equal', '?tok=123')
// .then((s) => new URLSearchParams(s))
// .invoke('get', 'tok')
// .should('equal', '123')
cy.contains("You have successfully unsubscribed from JamKazam emails.").should('be.visible');
cy.contains("Loading...").should('not.exist');
it.only("show unsubscribed message", () => {
cy.visit('/unsubscribe?tok=123');
cy.location('search')
.should('equal', '?tok=123')
.then((s) => new URLSearchParams(s))
.invoke('get', 'tok')
.should('equal', '123')
cy.contains("successfully unsubscribed")
});
})

View File

@ -2790,8 +2790,6 @@
},
"@fingerprintjs/fingerprintjs": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@fingerprintjs/fingerprintjs/-/fingerprintjs-4.6.0.tgz",
"integrity": "sha512-g2z4lF2saGxVT+AQSmJhPSwW/hBn8vnFJMW6UYOMl9ipJT7re0RZbr2+lB2eCZj/lJ89wWc21FMA14v9iOKroQ==",
"requires": {
"tslib": "^2.4.1"
}
@ -16873,18 +16871,11 @@
},
"react-flatpickr": {
"version": "3.10.6",
"resolved": "https://registry.npmjs.org/react-flatpickr/-/react-flatpickr-3.10.6.tgz",
"integrity": "sha512-Gg2zYFs+EoY7PNSk6lkiRyXV3nYmCp+ps29B4XFgZ1O4txOu6TvfHK2YCKT9zCpXbk5CcIGKMlvbupivWzy1+A==",
"requires": {
"flatpickr": "^4.6.2",
"prop-types": "^15.5.10"
}
},
"react-ga4": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/react-ga4/-/react-ga4-2.1.0.tgz",
"integrity": "sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ=="
},
"react-google-recaptcha": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz",

View File

@ -61,7 +61,6 @@
"react-dropzone": "^10.2.2",
"react-es6-progressbar.js": "^1.1.0",
"react-flatpickr": "^3.10.6",
"react-ga4": "^2.1.0",
"react-google-recaptcha": "^3.1.0",
"react-hook-form": "^7.11.1",
"react-https-redirect": "^1.1.0",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

View File

@ -1,20 +1,17 @@
import React, { useEffect } from 'react';
import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import Layout from './layouts/JKLayout';
import AnalyticsTracker from "./components/utilities/AnalyticsTracker";
import 'react-toastify/dist/ReactToastify.min.css';
import 'react-datetime/css/react-datetime.css';
import 'react-image-lightbox/style.css';
const App = () => {
return (
<>
<AnalyticsTracker />
<Layout />
</>
return (
<Router basename={process.env.PUBLIC_URL}>
<Layout />
</Router>
);
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

View File

@ -18,7 +18,7 @@ const JKRegistrationForm = ({ hasLabel, jamTrack, jamTrackArtistName }) => {
const [password, setPassword] = useState('');
const [isDisabled, setIsDisabled] = useState(true);
const [showPassword, setShowPassword] = useState(false);
const { capchaToken, recaptchaRef, handleRecaptcha } = useRecaptcha();
const { addToCart } = useJamTrackShopping();
@ -52,31 +52,25 @@ const JKRegistrationForm = ({ hasLabel, jamTrack, jamTrackArtistName }) => {
toast.error(`Registration failed. Please try again.`);
setIsDisabled(false);
}
};
useEffect(() => {
const addJamTrackToCart = async () => {
try {
try{
await addToCart(jamTrack);
} catch (error) {
}catch(error){
console.error(error);
}
}
console.log('currentUser', currentUser);
console.log('jamTrack', jamTrack);
console.log('jamTrackArtistName', jamTrackArtistName);
if (currentUser) {
if (jamTrack) {
console.log('adding jamtrack to cart');
if(jamTrack){
addJamTrackToCart();
} else if (jamTrackArtistName) {
console.log('redirecting to jamtracks artist landing');
}else if(jamTrackArtistName){
history.push(`/jamtracks?artist=${jamTrackArtistName}`);
} else {
console.log('redirecting to profile');
history.push('/profile');
}else{
history.push('/public/downloads');
}
}
}, [currentUser, jamTrack, jamTrackArtistName]);
@ -136,9 +130,9 @@ const JKRegistrationForm = ({ hasLabel, jamTrack, jamTrackArtistName }) => {
{process.env.REACT_APP_RECAPTCHA_ENABLED === 'true' && (
<FormGroup>
<ReCAPTCHA
sitekey={process.env.REACT_APP_SITE_KEY}
ref={recaptchaRef}
<ReCAPTCHA
sitekey={process.env.REACT_APP_SITE_KEY}
ref={recaptchaRef}
onChange={handleRecaptcha}
/>
</FormGroup>
@ -146,7 +140,7 @@ const JKRegistrationForm = ({ hasLabel, jamTrack, jamTrackArtistName }) => {
<FormGroup>
<Button color="primary" block className="mt-3" disabled={isDisabled}>
{t('signup')}
{t('signup')}
</Button>
</FormGroup>

View File

@ -33,9 +33,7 @@ const Registration = () => {
try {
const response = await getJamTrackBySlug({ slug });
const jamTrack = await response.json();
console.log('jamTrack', jamTrack);
setJamTrack(jamTrack);
setJamTrackArtistName(jamTrack.original_artist);
setLoading(false);
} catch (error) {
console.error(error);

View File

@ -1,27 +1,11 @@
import React, { Fragment, useEffect } from 'react';
import React, { Fragment } from 'react';
import { Col, Row } from 'reactstrap';
import { Link, useHistory, useLocation } from 'react-router-dom';
import { Link } from 'react-router-dom';
import LoginForm from '../LoginForm';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../../context/UserAuth';
const Login = () => {
const { t } = useTranslation('auth');
const { isAuthenticated, isLoading } = useAuth();
const history = useHistory();
const location = useLocation();
useEffect(() => {
if (!isLoading && isAuthenticated) {
const { from } = location.state || { from: { pathname: '/profile' } };
const finalDestination = from.pathname === '/' ? { pathname: '/profile' } : from;
history.replace(finalDestination);
}
}, [isAuthenticated, isLoading, history, location]);
if (isLoading || isAuthenticated) {
return null; // Or a loading spinner
}
return (
<Fragment>

View File

@ -37,7 +37,6 @@ import JKEditProfile from '../page/JKEditProfile';
import JKEditAccount from '../page/JKEditAccount';
import JKAccountSubscription from '../page/JKAccountSubscription';
import JKPaymentHistory from '../page/JKPaymentHistory';
import JKPaymentMethod from '../page/JKPaymentMethod';
import JKAccountPreferences from '../page/JKAccountPreferences';
import JKAffiliateProgram from '../affiliate/JKAffiliateProgram';
@ -55,9 +54,6 @@ import JKMyJamTracks from '../jamtracks/JKMyJamTracks';
import JKJamTrackShow from '../jamtracks/JKJamTrackShow';
import JKPayPalConfirmation from '../shopping-cart/JKPayPalConfirmation';
import JKUnsubscribe from '../public/JKUnsubscribe';
import JKConfirmEmailChange from '../public/JKConfirmEmailChange';
//import loadable from '@loadable/component';
//const DashboardRoutes = loadable(() => import('../../layouts/JKDashboardRoutes'));
@ -133,6 +129,7 @@ function JKDashboardMain() {
return;
}
const app = window.JK.JamKazam();
const jamServer = new window.JK.JamServer(app, function (event_type) {
@ -144,8 +141,6 @@ function JKDashboardMain() {
const clientInit = new window.JK.ClientInit();
clientInit.init();
app.initialize();
window.JK.JamServer.connect() // singleton here defined in JamServer.js
.done(function () {
console.log('Jamserver connected...');
@ -280,10 +275,6 @@ function JKDashboardMain() {
}
useScript(`${process.env.REACT_APP_CLIENT_BASE_URL}/client_scripts`, initJKScripts);
useScript('https://js.recurly.com/v4/recurly.js', () => {
console.log('Recurly.js script loaded');
window.recurly.configure(process.env.REACT_APP_RECURLY_PUBLIC_API_KEY);
});
return (
<div className={isFluid || isKanban ? 'container-fluid' : 'container'}>
@ -305,7 +296,6 @@ function JKDashboardMain() {
<PrivateRoute path="/account/identity" component={JKEditAccount} />
<PrivateRoute path="/account/subscription" component={JKAccountSubscription} />
<PrivateRoute path="/account/payments" component={JKPaymentHistory} />
<PrivateRoute path="/account/payment-method" component={JKPaymentMethod} />
<PrivateRoute path="/account/preferences" component={JKAccountPreferences} />
<PrivateRoute path="/affiliate/program" component={JKAffiliateProgram} />
<PrivateRoute path="/affiliate/payee" component={JKAffiliatePayee} />
@ -322,8 +312,6 @@ function JKDashboardMain() {
<PrivateRoute path="/checkout/success" component={JKCheckoutSuccess} />
<PrivateRoute path="/checkout" component={JKCheckout} />
<PrivateRoute path="/applaunch" component={JKAppLaunch} />
<PrivateRoute path="/unsubscribe/:tok" exact component={JKUnsubscribe} />
<PrivateRoute path="/confirm-email-change" exact component={JKConfirmEmailChange} />
{/*Redirect*/}
<Redirect to="/errors/404" />
</Switch>

View File

@ -7,15 +7,15 @@ import {JamTrackPreviewProvider} from '../../context/JamTrackPreviewContext'
import {useHistory} from "react-router-dom";
import useJamTrackShopping from "../../hooks/useJamTrackShopping";
import UserAuth, {useAuth} from '../../context/UserAuth';
const awesome1 = '/img/landing/jamtracks/Top 10 Image - Number 1.webp';
const awesome2 = '/img/landing/jamtracks/Top 10 Image - Number 2.webp';
const awesome3 = '/img/landing/jamtracks/Top 10 Image - Number 3.webp';
const awesome4 = '/img/landing/jamtracks/Top 10 Image - Number 4.webp';
const awesome5 = '/img/landing/jamtracks/Top 10 Image - Number 5.webp';
const awesome6 = '/img/landing/jamtracks/Top 10 Image - Number 6.webp';
const awesome7 = '/img/landing/jamtracks/Top 10 Image - Number 7.webp';
const awesome8 = '/img/landing/jamtracks/Top 10 Image - Number 8.webp';
const awesome9 = '/img/landing/jamtracks/Top 10 Image - Number 9.webp';
const awesome1 = '/img/landing/jamtracks/Top 10 Image - Number 1.png';
const awesome2 = '/img/landing/jamtracks/Top 10 Image - Number 2.png';
const awesome3 = '/img/landing/jamtracks/Top 10 Image - Number 3.png';
const awesome4 = '/img/landing/jamtracks/Top 10 Image - Number 4.png';
const awesome5 = '/img/landing/jamtracks/Top 10 Image - Number 5.png';
const awesome6 = '/img/landing/jamtracks/Top 10 Image - Number 6.png';
const awesome7 = '/img/landing/jamtracks/Top 10 Image - Number 7.png';
const awesome8 = '/img/landing/jamtracks/Top 10 Image - Number 8.png';
const awesome9 = '/img/landing/jamtracks/Top 10 Image - Number 9.png';
// To test this component in isolation, go to:
// http://beta.jamkazam.local:4000/public/backing-tracks/artist/song
// But this is only for development
@ -97,14 +97,6 @@ const ArtistBodyComponent = ({
//coming to signup page after clicking on jamtrack or artist
useEffect(() => {
window.addEventListener('load', () => {
document
.querySelectorAll('iframe[data-src]')
.forEach((iframe) => {
iframe.src = iframe.dataset.src;
});
});
const checkOverflow = () => {
if (jamtrackListingRef.current) {
const element = jamtrackListingRef.current;
@ -593,7 +585,7 @@ use their favorite plugins to power their percussive audio.</span></p>
<div className="card-body bg-light">
<div className="video-wrapper awesome-image-right">
<div className="video-container">
<iframe data-src="//www.youtube.com/embed/euxDykIqUdo" border="0"
<iframe src="//www.youtube.com/embed/euxDykIqUdo" border="0"
allowFullScreen></iframe>
</div>
</div>

View File

@ -8,7 +8,6 @@ import {Navbar} from "reactstrap";
import {topNavbarBreakpoint} from "../../config";
import Logo from "../navbar/Logo";
import {BrowserQueryProvider} from "../../context/BrowserQuery";
import {initGA, logPageView } from "../../helpers/analytics";
import Footer from "../footer/JKFooter";
const ArtistTemplatePage = ({ original_artist_slug, artist, songs}) => {
@ -24,8 +23,6 @@ const ArtistTemplatePage = ({ original_artist_slug, artist, songs}) => {
const paddingClass = greaterThan.sm ? "px-6" : "px-1";
useEffect(() => {
initGA();
//logPageView(window.location.pathname + window.location.search);
//see if there is affiliate in query string and save it as cookie
const urlParams = new URLSearchParams(window.location.search);
const affiliate = urlParams.get('affiliate');

View File

@ -7,15 +7,15 @@ import {JamTrackPreviewProvider} from '../../context/JamTrackPreviewContext'
import {useHistory} from "react-router-dom";
import useJamTrackShopping from "../../hooks/useJamTrackShopping";
import UserAuth, {useAuth} from '../../context/UserAuth';
const awesome1 = '/img/landing/jamtracks/Top 10 Image - Number 1.webp';
const awesome2 = '/img/landing/jamtracks/Top 10 Image - Number 2.webp';
const awesome3 = '/img/landing/jamtracks/Top 10 Image - Number 3.webp';
const awesome4 = '/img/landing/jamtracks/Top 10 Image - Number 4.webp';
const awesome5 = '/img/landing/jamtracks/Top 10 Image - Number 5.webp';
const awesome6 = '/img/landing/jamtracks/Top 10 Image - Number 6.webp';
const awesome7 = '/img/landing/jamtracks/Top 10 Image - Number 7.webp';
const awesome8 = '/img/landing/jamtracks/Top 10 Image - Number 8.webp';
const awesome9 = '/img/landing/jamtracks/Top 10 Image - Number 9.webp';
const awesome1 = '/img/landing/jamtracks/Top 10 Image - Number 1.png';
const awesome2 = '/img/landing/jamtracks/Top 10 Image - Number 2.png';
const awesome3 = '/img/landing/jamtracks/Top 10 Image - Number 3.png';
const awesome4 = '/img/landing/jamtracks/Top 10 Image - Number 4.png';
const awesome5 = '/img/landing/jamtracks/Top 10 Image - Number 5.png';
const awesome6 = '/img/landing/jamtracks/Top 10 Image - Number 6.png';
const awesome7 = '/img/landing/jamtracks/Top 10 Image - Number 7.png';
const awesome8 = '/img/landing/jamtracks/Top 10 Image - Number 8.png';
const awesome9 = '/img/landing/jamtracks/Top 10 Image - Number 9.png';
// To test this component in isolation, go to:
// http://beta.jamkazam.local:4000/public/backing-tracks/artist/song
// But this is only for development
@ -23,7 +23,7 @@ const awesome9 = '/img/landing/jamtracks/Top 10 Image - Number 9.webp';
const BodyComponent = ({
id = "1",
plan_code = "jamtrack-acdc-backinblack",
slug = "acdc-back-in-black",
slug = "ac-dc-back-in-black",
artist = "AC/DC (defaulted)",
song = "Back in Black (defaulted)",
provided_jam_track = null
@ -107,14 +107,6 @@ const BodyComponent = ({
//coming to signup page after clicking on jamtrack or artist
useEffect(() => {
window.addEventListener('load', () => {
document
.querySelectorAll('iframe[data-src]')
.forEach((iframe) => {
iframe.src = iframe.dataset.src;
});
});
console.log("useEffect, currentUser", currentUser)
updateHeight(); // Run after initial render
@ -555,7 +547,7 @@ use their favorite plugins to power their percussive audio.</span></p>
<div className="card-body bg-light">
<div className="video-wrapper awesome-image-right">
<div className="video-container">
<iframe data-src="//www.youtube.com/embed/euxDykIqUdo" border="0"
<iframe src="//www.youtube.com/embed/euxDykIqUdo" border="0"
allowFullScreen></iframe>
</div>
</div>

View File

@ -10,7 +10,6 @@ import {topNavbarBreakpoint} from "../../config";
import Logo from "../navbar/Logo";
import {BrowserQueryProvider} from "../../context/BrowserQuery";
import Footer from "../footer/JKFooter";
import {initGA, logPageView } from "../../helpers/analytics";
const TemplatePage = ({ id, plan_code, slug, artist, song, location}) => {
console.log('TemplatePage', id, plan_code, slug, artist, song, location);
@ -26,9 +25,6 @@ const TemplatePage = ({ id, plan_code, slug, artist, song, location}) => {
const paddingClass = greaterThan.sm ? "px-6" : "px-1";
useEffect(() => {
initGA();
//logPageView( window.location.pathname + window.location.search);
//see if there is affiliate in query string and save it as cookie
const urlParams = new URLSearchParams(window.location.search);
const affiliate = urlParams.get('affiliate');

View File

@ -23,24 +23,12 @@ const ProfileDropdown = () => {
const handleLogout = async event => {
event.preventDefault();
const cookieDomain = `.${process.env.REACT_APP_ORIGIN}`;
console.log('handleLogout: cookie: ', cookieDomain);
removeCookie('remember_token', {
domain: cookieDomain,
path: '/'
domain: `.${process.env.REACT_APP_ORIGIN}`
});
setCurrentUser(null);
// This will cause the server to remove the cookie TOO (overkill),
// but this code has a side effect because something, somewhere,
// client-side history pushes to /auth/login because of this function.
// ...
await logout();
// But I don't want to count on that side effect (defensive against future changes), so I force a page location hit here
window.location.href = "/auth/login";
// And here's the final bit; force a full page refresh ANYWAY.
// This is the only way we seem to really clear
// 'login state' cleanly at this point
window.location.reload();
};
return (

View File

@ -4,7 +4,7 @@ import { Link } from 'react-router-dom';
import FalconCardHeader from '../common/FalconCardHeader';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { fetchSessions, fetchFriendsSessions, fetchPublicSessions, fetchInactiveSessions } from '../../store/features/sessionsSlice';
import { fetchSessions } from '../../store/features/sessionsSlice';
import { isIterableArray } from '../../helpers/utils';
import { useResponsive } from '@farfetch/react-context-responsive';
import JKModalDialog from '../common/JKModalDialog';
@ -23,10 +23,7 @@ function JKMusicSessions() {
const { nativeAppUnavailable, setNativeAppUnavailable } = useNativeApp();
useEffect(() => {
// dispatch(fetchSessions());
dispatch(fetchFriendsSessions());
dispatch(fetchPublicSessions());
//dispatch(fetchInactiveSessions());
dispatch(fetchSessions());
}, []);
const toggleAppUnavilableModel = () => {
@ -40,7 +37,7 @@ function JKMusicSessions() {
<CardBody className="pt-0">
{loadingStatus === 'loading' && sessions.length === 0 ? (
<Loader />
) : loadingStatus === "succeeded" && sessions.length > 0 ? (
) : isIterableArray(sessions) ? (
<>
{greaterThan.sm ? (
<Row className="mb-3 justify-content-between d-none d-md-block">
@ -54,7 +51,7 @@ function JKMusicSessions() {
</Row>
)}
</>
) : loadingStatus === "succeeded" && sessions.length <= 0 ? (
) : (
<Row className="p-card">
<Col>
{t('list.no_records_1', { ns: 'sessions' })}
@ -62,13 +59,7 @@ function JKMusicSessions() {
{t('list.no_records_2', { ns: 'sessions' })}
</Col>
</Row>
) : loadingStatus === "failed" ? (
<Row className="p-card">
<Col>
{t('list.error', { ns: 'sessions' })}
</Col>
</Row>
) : null }
)}
</CardBody>
</Card>
<JKModalDialog

View File

@ -1,480 +0,0 @@
import React, { useState, useEffect, useRef } from 'react';
import {
Card,
CardBody,
Col,
Button,
Row,
CustomInput,
Label
} from 'reactstrap';
import Flex from '../common/Flex';
import FalconCardHeader from '../common/FalconCardHeader';
import { useTranslation } from 'react-i18next';
import iconPaymentMethodsGrid from '../../assets/img/icons/icon-payment-methods-grid.png';
import iconPaypalFull from '../../assets/img/icons/icon-paypal-full.png';
import { toast } from 'react-toastify';
import { updatePayment } from '../../helpers/rest';
import { useAuth } from '../../context/UserAuth';
import { getBillingInfo, getUserDetail, getCountries } from '../../helpers/rest';
import { useForm, Controller } from 'react-hook-form';
import Select from 'react-select';
import { useResponsive } from '@farfetch/react-context-responsive';
const JKPaymentMethod = () => {
const { t } = useTranslation('account');
const [billingInfo, setBillingInfo] = useState({});
const [hasStoredCreditCard, setHasStoredCreditCard] = useState(false);
const [paymentMethod, setPaymentMethod] = useState('credit-card');
const { currentUser } = useAuth();
const [countries, setCountries] = useState([]);
const labelClassName = 'ls text-600 font-weight-semi-bold mb-0';
const [submitting, setSubmitting] = useState(false);
const [isCardValid, setIsCardValid] = useState(false);
const { greaterThan } = useResponsive();
const elementsRef = useRef(null);
const formRef = useRef(null);
const recurlyConfigured = useRef(false);
const paypal = useRef(null);
const {
register,
control,
handleSubmit,
setValue,
formState: { errors }
} = useForm({
defaultValues: {
first_name: '',
last_name: '',
address1: '',
address2: '',
city: '',
state: '',
zip: '',
country: 'US',
}
});
useEffect(() => {
if (currentUser) {
populateUserData();
}
}, [currentUser]);
const populateUserData = async () => {
const options = {
id: currentUser.id
};
try {
const userResp = await getUserDetail(options);
const userData = await userResp.json();
if (userData.has_recurly_account) {
setHasStoredCreditCard(userData['has_stored_credit_card?']);
await populateBillingAddress();
}
} catch (error) {
console.error('Failed to get user details:', error);
}
};
const populateBillingAddress = async () => {
try {
const resp = await getBillingInfo();
const data = await resp.json();
const bi = data.billing_info;
setBillingInfo(bi);
} catch (error) {
console.error('Failed to get billing info:', error);
}
};
useEffect(() => {
if (currentUser) {
fetchCountries();
}
}, [currentUser]);
const fetchCountries = () => {
getCountries()
.then(response => {
if (response.ok) {
return response.json();
}
})
.then(data => {
setCountries(data.countriesx);
})
.catch(error => console.log(error));
};
useEffect(() => {
if (billingInfo) {
setValue('first_name', billingInfo.first_name || '');
setValue('last_name', billingInfo.last_name || '');
setValue('address1', billingInfo.address1 || '');
setValue('address2', billingInfo.address2 || '');
setValue('city', billingInfo.city || '');
setValue('state', billingInfo.state || '');
setValue('zip', billingInfo.zip || '');
setValue('country', billingInfo.country || 'US');
}
}, [billingInfo, setValue]);
const handleCountryChange = selectedOption => {
setValue('country', selectedOption.value);
};
const recurlyContainerRef = useRef();
useEffect(() => {
if (!recurlyContainerRef.current || !window.recurly || recurlyConfigured.current) return;
window.recurly.configure({ publicKey: process.env.REACT_APP_RECURLY_PUBLIC_API_KEY });
const elements = window.recurly.Elements();
const cardElement = elements.CardElement();
cardElement.attach(recurlyContainerRef.current);
cardElement.on('change', (event) => {
setIsCardValid(event.complete && !event.error);
});
//then load paypal:
const paypalInst = window.recurly.PayPal({ braintree: { clientAuthorization: process.env.REACT_APP_BRAINTREE_TOKEN } })
paypal.current = paypalInst;
paypal.current.on('error', onPayPalError);
paypal.current.on('token', onPayPalToken);
elementsRef.current = elements;
recurlyConfigured.current = true;
return () => {
// Optional cleanup if the component unmounts
recurlyContainerRef.current.innerHTML = '';
recurlyConfigured.current = false;
};
}, [recurlyContainerRef.current]);
const onPayPalError = (error) => {
console.error('PayPal Error:', error);
toast.error('PayPal Error: ' + (error.message || t('payment_method.alerts.try_again')));
setSubmitting(false);
}
const onPayPalToken = (token) => {
handleUpdatePayment(token);
}
const handleUpdatePayment = (token) => {
updatePayment({ recurly_token: token.id }).then((response) => {
setHasStoredCreditCard(true);
toast.success(t('payment_method.alerts.payment_method_updated'));
}).catch((error) => {
console.error('Error updating payment with PayPal token:', error);
if (error.response && error.response.data && error.response.data.message) {
toast.error(error.response.data.message);
} else {
console.error('Error updating payment with PayPal token:', error);
toast.error(t('payment_method.alerts.card_update_error'));
}
}).finally(() => {
setSubmitting(false);
});
};
const onSubmit = async (data) => {
setSubmitting(true);
if (paymentMethod === 'paypal') { // PayPal payment method
handoverToPaypal();
return;
} else { // Credit Card payment method
if (!elementsRef.current) {
console.error('Recurly elementsRef.current is not ready');
setSubmitting(false);
return;
}
if (!formRef.current) {
console.error('formRef.current is not ready');
setSubmitting(false);
return;
}
window.recurly.token(elementsRef.current, formRef.current, (err, token) => {
if (err) {
console.error('Recurly token error:', err);
toast.error(t('payment_method.alerts.card_processing_error'));
setSubmitting(false);
} else {
console.log('Recurly token:', token.id);
// send token.id to backend
handleUpdatePayment(token);
}
});
}
};
const handoverToPaypal = () => {
// Handover to Paypal
setSubmitting(true);
paypal.current.start()
};
return (
<Card>
<FalconCardHeader title={t('payment_method.page_title')} titleClass="font-weight-bold" />
<CardBody className="pt-3" style={{ backgroundColor: '#edf2f9' }}>
<div className='mb-3'>
{hasStoredCreditCard ? (
<span>
<strong>{t('payment_method.help_text_has_card')}</strong>
</span>
) : (
<span>
<strong>{t('payment_method.help_text_no_card')}</strong>
</span>
)}
&nbsp;{t('payment_method.help_text')}
</div>
<form onSubmit={handleSubmit(onSubmit)} ref={formRef}>
<Card style={{ width: greaterThan.sm ? "90%" : '100%' }} className='mx-auto'>
<FalconCardHeader title={t('payment_method.header')} titleTag="h5" />
<CardBody>
<Row>
<Col className="mb-2" xs={12} md={6}>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="first_name" className={labelClassName}>
{t('payment_method.first_name')}
</Label>
</Col>
<Col>
<input {...register('first_name', { required: t('payment_method.validations.first_name.required') })} className="form-control" data-recurly="first_name" />
{errors.first_name && (
<div className="text-danger">
<small>{errors.first_name.message}</small>
</div>
)}
</Col>
</Row>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="last_name" className={labelClassName}>
{t('payment_method.last_name')}
</Label>
</Col>
<Col>
<input {...register('last_name', { required: t('payment_method.validations.last_name.required') })} className="form-control" data-recurly="last_name" />
{errors.last_name && (
<div className="text-danger">
<small>{errors.last_name.message}</small>
</div>
)}
</Col>
</Row>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="address1" className={labelClassName}>
{t('payment_method.address1')}
</Label>
</Col>
<Col>
<input {...register('address1', { required: t('payment_method.validations.address1.required') })} className="form-control" data-recurly="address1" />
{errors.address1 && (
<div className="text-danger">
<small>{errors.address1.message}</small>
</div>
)}
</Col>
</Row>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="address2" className={labelClassName}>
{t('payment_method.address2')}
</Label>
</Col>
<Col>
<input {...register('address2')} className="form-control" data-recurly="address2" />
{errors.address2 && (
<div className="text-danger">
<small>{errors.address2.message}</small>
</div>
)}
</Col>
</Row>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="city" className={labelClassName}>
{t('payment_method.city')}
</Label>
</Col>
<Col>
<input {...register('city', { required: t('payment_method.validations.city.required') })} className="form-control" data-recurly="city" />
{errors.city && (
<div className="text-danger">
<small>{errors.city.message}</small>
</div>
)}
</Col>
</Row>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="state" className={labelClassName}>
{t('payment_method.state')}
</Label>
</Col>
<Col>
<input {...register('state', { required: t('payment_method.validations.state.required') })} className="form-control" data-recurly="state" />
{errors.state && (
<div className="text-danger">
<small>{errors.state.message}</small>
</div>
)}
</Col>
</Row>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="zip" className={labelClassName}>
{t('payment_method.zip_code')}
</Label>
</Col>
<Col>
<input
{...register('zip', { required: t('payment_method.validations.zip_code.required') })}
className="form-control" data-recurly="postal_code"
/>
{errors.zip && (
<div className="text-danger">
<small>{errors.zip.message}</small>
</div>
)}
</Col>
</Row>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="country" className={labelClassName}>
{t('payment_method.country')}
</Label>
</Col>
<Col>
<Controller
name="country"
control={control}
rules={{ required: t('payment_method.validations.country.required') }}
render={({ field: { onChange, value } }) => {
const country = countries.find(country => country.countrycode === value);
if (!country) {
return (
<Select
data-testid="countrySelect"
data-recurly="country"
onChange={handleCountryChange}
options={countries.map(c => {
return { value: c.countrycode, label: c.countryname };
})}
/>
);
}
return (
<Select
data-testid="countrySelect"
data-recurly="country"
value={{ value: country.countrycode, label: country.countryname }}
onChange={handleCountryChange}
options={countries.map(c => {
return { value: c.countrycode, label: c.countryname };
})}
/>
);
}}
/>
<input type="hidden" name="country" data-recurly="country" {...register('country')} />
{errors.country && (
<div className="text-danger">
<small>{errors.country.message}</small>
</div>
)}
</Col>
</Row>
</Col>
<Col xs={12} md={6} className="mb-2 pl-5">
<Row>
<Col xs={12}>
<CustomInput
label={
<Flex align="center" className="mb-2 fs-1">
<span>{t('payment_method.credit_card')}</span>
</Flex>
}
id="credit-card"
value="credit-card"
checked={paymentMethod === 'credit-card'}
onChange={({ target }) => setPaymentMethod(target.value)}
type="radio"
/>
</Col>
</Row>
<Row>
<Col sm={8}>
<div id="recurly-elements" ref={recurlyContainerRef}></div>
{!isCardValid && errors.recurly && (
<div className="text-danger">
<small>{errors.recurly.message}</small>
</div>
)}
<input type="hidden" name="recurly-token" data-recurly="token" />
</Col>
<div className="col-4 text-center pt-2 d-none d-sm-block">
<div className="rounded p-2 mt-3 bg-100">
<div className="text-uppercase fs--2 font-weight-bold">{t('payment_method.we_accept')}</div>
<img src={iconPaymentMethodsGrid} alt="" width="120" />
</div>
</div>
</Row>
<hr />
<Row className="mt-3">
<Col xs={12}>
<CustomInput
label={<img className="pull-right" src={iconPaypalFull} height="20" alt="" />}
id="paypal"
value="paypal"
checked={paymentMethod === 'paypal'}
onChange={({ target }) => setPaymentMethod(target.value)}
type="radio"
/>
</Col>
</Row>
<div className="d-flex justify-content-center">
<Button
color="primary"
type="submit"
disabled={submitting}
className="mt-3"
>
{submitting ? t('payment_method.submitting') : t('payment_method.save_payment_info')}
</Button>
</div>
<div className="text-center">
<p className="fs--1 mt-3 mb-0">
{t('payment_method.aggreement.text1')} <strong>{t('payment_method.aggreement.text2')} </strong>{t('payment_method.aggreement.text3')}{' '}
<br />
<a href="https://www.jamkazam.com/corp/terms" target='_blank'>{t('payment_method.aggreement.terms_of_service')}</a>
</p>
</div>
</Col>
</Row>
</CardBody>
</Card>
</form>
</CardBody>
</Card>
);
};
export default JKPaymentMethod;

View File

@ -1,55 +0,0 @@
import React from 'react'
import { useLocation } from "react-router-dom";
import { Card, CardBody, CardText, CardTitle } from 'reactstrap';
import { useState, useEffect } from 'react';
import { updateEmail } from '../../helpers/rest';
import { useTranslation } from 'react-i18next';
const JKConfirmEmailChange = () => {
const { t } = useTranslation("account");
const location = useLocation();
const params = new URLSearchParams(location.search);
const token = params.get('token');
const [success, setSuccess] = useState(false);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (token) {
updateEmail(token)
.then(response => {
if (response.status === 200) {
setSuccess(true);
} else {
setSuccess(false);
}
})
.catch(() => {
setSuccess(false);
setError(t('identity.changed_email_confirmation.error'));
}).finally(() => {
setLoading(false);
});
}
}, [token]);
return (
<Card style={{ width: '25rem', margin: '2rem auto' }}>
<CardBody>
<CardTitle className="mb-2">
{t('identity.changed_email_confirmation.title')}
</CardTitle>
<>
{loading && <div className="text-muted"><span className="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>{t('identity.changed_email_confirmation.loading')}</div>}
{success && <div className="text-success">{t('identity.changed_email_confirmation.success')}</div>}
{error && <div className="text-danger">{error}</div>}
</>
</CardBody>
</Card>
)
}
export default JKConfirmEmailChange

View File

@ -116,17 +116,17 @@ const JKDownloads = () => {
<div>
<h5>Download the free JamKazam app</h5>
{/* <p>{currentOS}</p> */}
<p className="mb-lg-4">
<p>
You must use the JamKazam app to get into online sessions with other musicians. Our app also gives you the most feature-rich experience for JamTracks, recordings, live broadcasting, and other features. Click the button below to download the JamKazam app installer, then double click the installer to run it. For more detailed instructions,&nbsp;
{isMacOS ? <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000130025" target='_blank'>see this help article</a> : isWindows && <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000130024" target="_blank">see this help article</a>}.
</p>
<div className='mt-2 d-flex flex-column flex-md-row'>
<div style={ { flexGrow: 0, flexShrink: 0, flexBasis: '25%' } }>
<a href={downloadLink} target='_blank' style={{ cursor: 'pointer' }}>
<a href={downloadLink} target='_blank'>
<img src={downloadImageUrl} alt="Download JamKazam" />
</a>
</div>
<div className='mt-3 mt-lg-0 ml-lg-5' style={ { flexGrow: 1, flexShrink: 1, flexBasis: '75%' } }>
<div className='mt-3 mt-lg-0 ml-lg-3' style={ { flexGrow: 1, flexShrink: 1, flexBasis: '75%' } }>
<div>Need a different version?</div>
<ul className='list-unstyled'>
{availablePlatforms.filter(platform => platform !== currentOS).map(platform => (

View File

@ -72,9 +72,8 @@ const JKDownloadsLegacy = () => {
const downloadLink = React.useMemo(() => {
if (!currentOS) return null
if (!selectedPlatform) return null
return downloads[`JamClient/${selectedPlatform}`]
}, [currentOS, selectedPlatform]);
return downloads[`JamClient/${currentOS}`]
}, [currentOS]);
const selectPlatform = (platform) => {
setSelectedPlatform(platform)
@ -121,17 +120,17 @@ const JKDownloadsLegacy = () => {
<h5>Download the older legacy JamKazam app</h5>
{/* <p>{currentOS}</p>
<p>{selectedPlatform}</p> */}
<p className="mb-lg-4">
<p>
You must use the JamKazam app to get into online sessions with other musicians. Our app also gives you the most feature-rich experience for JamTracks, recordings, live broadcasting, and other features. Click the button below to download the JamKazam app installer, then double click the installer to run it. For more detailed instructions,&nbsp;
{isMacOS ? <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000130025" target="_blank">see this help article</a> : isWindows && <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000130024" target="_blank">see this help article</a>}.
</p>
<div className='mt-2 d-flex flex-column flex-md-row'>
<div style={ { flexGrow: 0, flexShrink: 0, flexBasis: '25%' } }>
<a href={downloadLink} target='_blank' style={ { cursor: 'pointer' } }>
<a href={downloadLink} target='_blank'>
<img src={downloadImageUrl} alt="Download JamKazam" />
</a>
</div>
<div className='mt-3 mt-lg-0 ml-lg-5' style={ { flexGrow: 1, flexShrink: 1, flexBasis: '75%' } }>
<div className='mt-3 mt-lg-0 ml-lg-3' style={ { flexGrow: 1, flexShrink: 1, flexBasis: '75%' } }>
<div>Need a different version?</div>
<ul className='list-unstyled'>
{availablePlatforms.filter(platform => platform !== selectedPlatform).map(platform => (
@ -143,7 +142,7 @@ const JKDownloadsLegacy = () => {
</ul>
</div>
</div>
<div className='mt-lg-4'>
<div className='mt-1 mt-lg-4'>
<div><strong>System Requirements</strong></div>
{selectedPlatform === 'MacOSX' ? (
<p>

View File

@ -1,174 +0,0 @@
import React, { useState, useEffect } from 'react'
import { detectOS, isAppleSilicon } from '../../helpers/utils'
import {getClientDownloads, getObsPluginDownloads} from '../../helpers/rest'
import { Link } from 'react-router-dom'
const DownloadButtonMacAppleMx = '/img/downloads/Download-Button-Obs-Mac-Apple-Mx.png'
const DownloadButtonMacIntel = '/img/downloads/Download-Button-Obs-Mac-Intel.png'
const DownloadButtonWindows = '/img/downloads/Download-Button-Obs-Windows.png'
const JKObsDownloads = () => {
const [currentOS, setCurrentOS] = useState(null)
const [downloads, setDownloads] = useState({})
const availablePlatforms = React.useMemo(() => {
const keys = Object.keys(downloads)
//only show JamClientModern versions
const sortedStrings = keys.filter(key => key.startsWith('OBSPlugin')).map(key => key.substring('OBSPlugin/'.length))
return sortedStrings
}, [downloads])
const detectAndSetOS = () => {
let os = detectOS()
if (os == "MacOSX") {
const silicon = isAppleSilicon();
if (silicon == true) {
os = "MacOSX-M";
}
else {
os = "MacOSX-Intel";
}
}
if (!os) {
os = "Win32"
}
setCurrentOS(os)
}
const fetchClientDownloads = () => {
getObsPluginDownloads()
.then(resp => {
if (resp.status === 200) {
return resp.json()
}
}).then(data => {
const platforms = Object.keys(data)
const downloadUris = {}
platforms.filter(p => p === 'OBSPlugin/MacOSX-M' || p === 'OBSPlugin/MacOSX-Intel' || p === 'OBSPlugin/Win32').forEach(platform => {
downloadUris[platform] = data[platform].uri
})
setDownloads(downloadUris)
detectAndSetOS()
})
.catch(err => {
console.error(err)
})
}
useEffect(() => {
fetchClientDownloads()
}, [])
const downloadLink = React.useMemo(() => {
if (!currentOS) return null
return downloads[`OBSPlugin/${currentOS}`]
}, [currentOS]);
const selectPlatform = (platform) => {
console.log('_DEBUG_ platform', platform)
setCurrentOS(platform)
}
const downloadImageUrl = React.useMemo(() => {
if (!currentOS) return null
switch (currentOS) {
case 'MacOSX-M':
return DownloadButtonMacAppleMx
case 'MacOSX-Intel':
return DownloadButtonMacIntel
case 'Win32':
return DownloadButtonWindows
}
}, [currentOS])
const isMacOS = React.useMemo(() => {
if (!currentOS) return false
return currentOS.startsWith('MacOSX')
}, [currentOS])
const isWindows = React.useMemo(() => {
if (!currentOS) return false
return currentOS === 'Win32'
}, [currentOS])
const platformDisplayName = (platform) => {
switch (platform) {
case 'MacOSX-M':
return 'JamKazam for Mac (Apple Silicon)'
case 'MacOSX-Intel':
return 'JamKazam for Mac (Intel)'
case 'Win32':
return 'JamKazam for Windows'
}
}
const onClickPlatform = (event, platform) => {
event.preventDefault()
selectPlatform(platform)
}
return (
<>
{Object.keys(downloads).length > 0 && currentOS ? (
<div>
<h5>Download and install OBS Studio</h5>
{/* <p>{currentOS}</p> */}
<p>
Before installing the JamKazam plugin for OBS, you must first download and install OBS Studio.
If you have not already done so, <a target="_blank" href="https://jamkazam.freshdesk.com/support/solutions/articles/66000525817">follow the instructions in this help article</a> to download
and install OBS Studio version 30.2.3. Please note that you cannot use OBS video features with the legacy
JamKazam application - only with the current version of our app.
</p>
<h5 className="mt-lg-4">Download and install JamKazam plugin for OBS</h5>
<p className="mb-lg-4">
After installing OBS Studio as explained above, you'll need to install the JamKazam
plugin for OBS. This plugin lets JamKazam and OBS Studio work together. Click the button
below to download the plugin installer. When the download is complete, double click the
installer, and then follow the on-screen instructions to install the plugin. For more detailed
instructions, <a target="_blank" href="https://jamkazam.freshdesk.com/support/solutions/articles/66000525817">see this help article</a>.
</p>
<div className='mt-2 d-flex flex-column flex-md-row'>
<div style={ { flexGrow: 0, flexShrink: 0, flexBasis: '25%' } }>
<a href={downloadLink} target='_blank'>
<img src={downloadImageUrl} alt="Download JamKazam" />
</a>
</div>
<div className='mt-3 mt-lg-0 ml-lg-5' style={ { flexGrow: 1, flexShrink: 1, flexBasis: '75%' } }>
<div>Need a different version?</div>
<ul className='list-unstyled'>
{availablePlatforms.filter(platform => platform !== currentOS).map(platform => (
<li key={platform}><a href="#" onClick={(e) => onClickPlatform(e, platform)}>{platformDisplayName(platform)}</a></li>
))}
</ul>
</div>
</div>
<div className='mt-lg-4'>
<h5>System Requirements</h5>
{isMacOS ? (
<p>
To install the JamKazam plugin for OBS Studio:<br />
- OBS Studio version 30.2.3 must already be installed on your computer <br />
- Your Mac must be running macOS 10.15 (Catalina) or higher <br />
</p>
) : isWindows ? (
<p>
To install the JamKazam plugin for OBS Studio<br />
- OBS Studio version 30.2.3 must already be installed on your computer <br />
- Your computer must be running Windows 10 or 11, 64-bit (32-bit not supported)
</p>
) : null}
</div>
</div>
) : (
<div>Loading...</div>
)}
</>
)
}
export default JKObsDownloads

View File

@ -1,18 +1,16 @@
import React, { useEffect, useState } from 'react';
import { Card, CardBody, CardText, CardTitle } from 'reactstrap';
import { useTranslation } from 'react-i18next';
import { Card, CardBody, CardText, CardTitle } from 'reactstrap';
import { useTranslation } from "react-i18next";
import { useBrowserQuery } from '../../context/BrowserQuery';
import { useHistory, useParams } from "react-router-dom";
import { useHistory } from "react-router-dom";
const unsubscribeFromNewUsersWeeklyEmail = (token) => {
const baseUrl = process.env.REACT_APP_CLIENT_BASE_URL
const baseUrl = process.env.REACT_APP_LEGACY_BASE_URL
return new Promise((resolve, reject) => {
fetch(`${baseUrl}/unsubscribe_user_match/${token}`,
fetch(`${baseUrl}/unsubscribe_user_match/${token}`,
{ method: 'POST' }
).then(response => {
if (response.ok) {
@ -24,64 +22,43 @@ const unsubscribeFromNewUsersWeeklyEmail = (token) => {
})
}
const unsubscribe = (token) => {
const baseUrl = process.env.REACT_APP_CLIENT_BASE_URL
return new Promise((resolve, reject) => {
fetch(`${baseUrl}/unsubscribe/${token}`, { method: 'POST', headers: { 'Content-Type': 'application/json', accept: 'application/json' } })
.then(response => {
if (response.ok) {
resolve(response)
} else {
reject(response)
}
}).catch(error => {
reject(error);
});
})
}
function JKUnsubscribe() {
const { t } = useTranslation("unsubscribe");
const {t} = useTranslation()
const queryObj = useBrowserQuery();
const history = useHistory()
const [loading, setLoading] = useState(true)
const [success, setSuccess] = useState(false)
const [error, setError] = useState(null)
const { tok } = useParams();
useEffect(() => {
if (tok) {
unsubscribe(tok)
.then((resp) => {
if (resp.ok) {
setSuccess(true)
} else {
setSuccess(false)
}
})
.catch(error => {
setError(error)
}).finally(() => {
setLoading(false)
});
} else {
const token = queryObj.get('tok')
if(token){
unsubscribeFromNewUsersWeeklyEmail(token)
.then((resp) => {
if(resp.ok){
setSuccess(true)
}
})
.catch(error => console.error(error))
}else{
history.push('/')
}
}, [])
return (
<Card style={{ width: '25rem', margin: '2rem auto' }}>
<Card color={ success ? 'success' : 'light' } style={{ width: '25rem', margin: '2rem auto' }}>
<CardBody>
<CardTitle className="mb-2">{t('page_title')}</CardTitle>
<>
{loading && <div className="text-muted"><span className="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>{t('loading')}</div>}
{success && <div className="text-success">{t('success')}</div>}
{error && <div className="text-danger">{t('error')}</div>}
</>
<CardTitle className="mb-2">Unsubscribe From Weekly Email</CardTitle>
<CardText>
{
success?
'You have successfully unsubscribed from weekly emails on newly joined musicians having low internet latency to you.' :
'Unsubscribing...'
}
</CardText>
</CardBody>
</Card>
)
}

View File

@ -13,7 +13,7 @@ import useNativeAppCheck from '../../hooks/useNativeAppCheck';
import { useNativeApp } from '../../context/NativeAppContext';
import EnterIcon from '../../icons/enter.svg';
import JKInstrumentIcon from '../profile/JKInstrumentIcon';
import { useHistory } from 'react-router-dom';
import {useHistory} from 'react-router-dom';
import useSessionHelper from './JKUseSessionHelper';
import JKModalDialog from '../common/JKModalDialog';
import JKAppLaunch from '../page/JKAppLaunch';
@ -25,10 +25,10 @@ function JKSession({ session }) {
const { greaterThan } = useResponsive();
const { setNativeAppUnavailable } = useNativeApp();
const { sessionDescription } = useSessionHelper(session);
useEffect(() => {
const otherUserIds = session.active_music_session ? session.active_music_session.participants.map(p => p.user.id) : [];
const otherUserIds = session.participants.map(p => p.user.id);
const options = { currentUserId: currentUser.id, otherUserIds };
dispatch(fetchUserLatencies(options));
}, [session.id]);
@ -40,7 +40,7 @@ function JKSession({ session }) {
};
const hasFriendNote = session => {
if (session.active_music_session && session.active_music_session.participants.find(p => p.user.is_friend)) {
if (session.participants.find(p => p.user.is_friend)) {
return t('list.notes.has_friend', { ns: 'sessions' });
}
};
@ -71,62 +71,48 @@ function JKSession({ session }) {
</div>
<div>{sessionDescription}</div>
</td>
<td>
{session.active_music_session && session.active_music_session.participants.length > 0 && (
<>
{session.active_music_session.participants.map(participant => (
<Row style={musicianRowStyle} key={participant.id}>
<Col>
<JKSessionUser user={participant.user} />
</Col>
</Row>
))}
</>
)}
{session.participants.map(participant => (
<Row style={musicianRowStyle} key={participant.id}>
<Col>
<JKSessionUser user={participant.user} />
</Col>
</Row>
))}
</td>
<td className="text-center">
{session.active_music_session && session.active_music_session.participants.length > 0 && (
<>
{session.active_music_session.participants.map(participant => (
<Row key={participant.id} style={musicianRowStyle}>
<Col>
<JKUserLatencyBadge key={participant.id} user={participant.user} showBadgeOnly={true} />
</Col>
</Row>
))}
</>
)}
{session.participants.map(participant => (
<Row key={participant.id} style={musicianRowStyle}>
<Col>
<JKUserLatencyBadge key={participant.id} user={participant.user} showBadgeOnly={true} />
</Col>
</Row>
))}
</td>
<td>
{session.active_music_session && session.active_music_session.participants.length > 0 && (
<>
{session.active_music_session.participants.map(participant => (
<Row style={musicianRowStyle} key={participant.id} data-testid={`Participant${participant.id}Tracks`}>
<Col>
{participant.tracks.map(track => (
<span key={track.id} className="mr-1 mb-1" title={track.instrment}>
<a
id={`Participant${participant.id}Track${track.id}Instrument`}
data-testid={`Track${track.id}Instrument`}
>
{/* <InstrumentIcon track={track} /> */}
<JKInstrumentIcon instrumentId={track.instrument_id} instrumentName={track.instrument} />
</a>
<UncontrolledTooltip
placement="top"
target={`Participant${participant.id}Track${track.id}Instrument`}
>
{track.instrument}
</UncontrolledTooltip>
</span>
))}
</Col>
</Row>
))}
</>
)}
{session.participants.map(participant => (
<Row style={musicianRowStyle} key={participant.id} data-testid={`Participant${participant.id}Tracks`}>
<Col>
{participant.tracks.map(track => (
<span key={track.id} className="mr-1 mb-1" title={track.instrment}>
<a
id={`Participant${participant.id}Track${track.id}Instrument`}
data-testid={`Track${track.id}Instrument`}
>
{/* <InstrumentIcon track={track} /> */}
<JKInstrumentIcon instrumentId={track.instrument_id} instrumentName={track.instrument} />
</a>
<UncontrolledTooltip
placement="top"
target={`Participant${participant.id}Track${track.id}Instrument`}
>
{track.instrument}
</UncontrolledTooltip>
</span>
))}
</Col>
</Row>
))}
</td>
<td className="text-center">
<JoinSessionButton session={session} />
@ -156,13 +142,11 @@ function JKSession({ session }) {
<strong>{t('list.header.latency', { ns: 'sessions' })}</strong>
</div>
</div>
{session.active_music_session && session.active_music_session.participants.length > 0 && (
<div>
{session.active_music_session.participants.map(participant => (
<JKSessionUser key={participant.id} user={participant.user} />
))}
</div>
)}
<div>
{session.participants.map(participant => (
<JKSessionUser key={participant.id} user={participant.user} />
))}
</div>
<div className="mt-4 d-flex flex-row justify-content-center">
<JoinSessionButton session={session} />
</div>
@ -203,23 +187,23 @@ function JoinSessionButton({ session }) {
return (
<>
<div>
<Button
data-testid="joinBtn"
color="primary"
onClick={joinSession}
className="btn-join btn-sm mr-1 mb-1 pt-1 pb-1 pl-1 pr-1"
style={{ cursor: 'pointer' }}
>
<img src={EnterIcon} alt="enter" className="mr-1" style={{ width: "23px" }} />
</Button>
<div className='d-none d-sm-block'>
<a href="#" onClick={joinSession}>
Join Session
</a>
</div>
<div>
<Button
data-testid="joinBtn"
color="primary"
onClick={joinSession}
className="btn-join btn-sm mr-1 mb-1 pt-1 pb-1 pl-1 pr-1"
style={{ cursor: 'pointer' }}
>
<img src={EnterIcon} alt="enter" className="mr-1" style={ { width: "23px" } } />
</Button>
<div className='d-none d-sm-block'>
<a href="#" onClick={joinSession}>
Join Session
</a>
</div>
<JKModalDialog
</div>
<JKModalDialog
show={showAppLauncher}
onToggle={toggleAppLauncher}
title={t('new.page_title', { ns: 'sessions' })}

View File

@ -1,19 +0,0 @@
// AnalyticsTracker.js
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
import { initGA, logPageView } from "../../helpers/analytics";
const AnalyticsTracker = () => {
const location = useLocation();
useEffect(() => {
initGA();
}, []);
useEffect(() => {
logPageView(location.pathname + location.search);
}, [location]);
return null; // it doesn't render anything
};
export default AnalyticsTracker;

View File

@ -1,99 +0,0 @@
/**
* meta_tracking.js
* A standalone module to capture and persist Meta attribution signals (fbclid, _fbp) in cookies.
*
* Logic adapted from web/app/assets/javascripts/meta_tracking.js for React environment.
* - Checks URL for `fbclid` and sets `_fbc` cookie.
* - Checks for `_fbp` cookie; if missing, generates and sets it.
*/
const MetaTracking = {
init: function () {
const location = window.location;
this.handleFbc(location.search);
this.handleFbp();
this.handleUtm(location.search);
},
// 1. Parsing and storing _fbc (Click ID)
handleFbc: function (searchParams) {
const fbclid = this.getQueryParam('fbclid', searchParams);
if (fbclid) {
const version = 'fb';
const subdomainIndex = 1; // 1 = example.com
const creationTime = new Date().getTime(); // Unix timestamp in ms
// Format: fb.1.timestamp.id
const fbcValue = `${version}.${subdomainIndex}.${creationTime}.${fbclid}`;
this.setCookie('_fbc', fbcValue, 90);
}
},
handleUtm: function (searchParams) {
if (!searchParams) return;
const query = searchParams.substring(1);
const vars = query.split('&');
vars.forEach(v => {
const pair = v.split('=');
if (pair.length === 2) {
const key = decodeURIComponent(pair[0]);
const value = decodeURIComponent(pair[1]);
if (key.indexOf('utm_') === 0) {
this.setCookie(key, value, 90);
}
}
});
},
// 2. Handling _fbp (Browser ID)
handleFbp: function () {
if (!this.getCookie('_fbp')) {
const version = 'fb';
const subdomainIndex = 1;
const creationTime = new Date().getTime();
const randomInt = Math.floor(Math.random() * 10000000000); // 10-digit random number
// Format: fb.1.timestamp.randomDigits
const fbpValue = `${version}.${subdomainIndex}.${creationTime}.${randomInt}`;
this.setCookie('_fbp', fbpValue, 90);
}
},
// Helper: Get query param by name
getQueryParam: function (name, search) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
const regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
const results = regex.exec(search);
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
},
// Helper: Set cookie
setCookie: function (name, value, days) {
let expires = "";
if (days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
// Ensure path is root and domain is included if needed (defaults to current host)
document.cookie = name + "=" + (value || "") + expires + "; path=/";
},
// Helper: Get cookie
getCookie: function (name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
};
export default MetaTracking;

View File

@ -1,38 +0,0 @@
import ReactGA from "react-ga4";
var initialized = false;
// Noticed that ReactGA.initialize logs a page view,
// so the 1st logPageView will end up double-counting the initial path landed on
var ignoredFirstPageView = false;
export const initGA = () => {
try {
if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) {
ReactGA.initialize(process.env.REACT_APP_GOOGLE_ANALYTICS_ID);
initialized = true;
} else {
initialized = false;
console.log("no GA ID found");
}
}
catch (error) {
console.log("Error initializing GA", error);
}
};
export const logPageView = (path) => {
if(initialized) {
if(ignoredFirstPageView) {
try {
ReactGA.send({hitType: "pageview", page: path});
}
catch(error) {
console.log("Error logging page view", error);
}
}
else {
ignoredFirstPageView = true;
}
}
};

View File

@ -101,8 +101,8 @@ export const getInstruments = () => {
export const getCurrentUser = () => {
return new Promise((resolve, reject) => {
apiFetch('/me')
.then(response => resolve(response))
.catch(error => reject(error))
.then(response => resolve(response))
.catch(error => reject(error))
})
}
@ -213,30 +213,6 @@ export const getSessions = () => {
});
};
export const getFriendsSessions = () => {
return new Promise((resolve, reject) => {
apiFetch(`/sessions/friends`)
.then(response => resolve(response))
.catch(error => reject(error));
});
};
export const getPublicSessions = () => {
return new Promise((resolve, reject) => {
apiFetch(`/sessions/public`)
.then(response => resolve(response))
.catch(error => reject(error));
});
};
export const getInactiveSessions = () => {
return new Promise((resolve, reject) => {
apiFetch(`/sessions/inactive`)
.then(response => resolve(response))
.catch(error => reject(error));
});
};
export const getSessionsHistory = (options = {}) => {
return new Promise((resolve, reject) => {
apiFetch(`/sessions/history?${new URLSearchParams(options)}`)
@ -289,7 +265,7 @@ export const getCities = (countryId, regionId) => {
export const postUpdateAccountEmail = (userId, options) => {
const { email, current_password } = options;
return new Promise((resolve, reject) => {
apiFetch(`/users/${userId}/update_email_alt`, {
apiFetch(`/users/${userId}/update_email`, {
method: 'POST',
body: JSON.stringify({ update_email: email, current_password })
})
@ -339,7 +315,7 @@ export const requstResetForgotPassword = email => {
})
.then(response => resolve(response))
.catch(error => reject(error));
});
});
};
export const resetForgotPassword = (options = {}) => {
@ -491,16 +467,18 @@ export const getJamTrackPublic = options => {
const { plan_code } = options;
return new Promise((resolve, reject) => {
// This does not make sense; historical reasons here
apiFetch(`/jamtracks/band/${plan_code}?${new URLSearchParams({ plan_code })}`)
.then(response => resolve(response))
.catch(error => reject(error));
apiFetch(`/jamtracks/band/${plan_code}?${new URLSearchParams({plan_code})}`)
.then(response => resolve(response))
.catch(error => reject(error));
});
};
export const getJamTrackBySlug = options => {
const { slug } = options;
return new Promise((resolve, reject) => {
apiFetch(`/jamtracks/public/${slug}`)
apiFetch(`/jamtracks/${slug}`, {
baseUrl: process.env.REACT_APP_CLIENT_BASE_URL
})
.then(response => resolve(response))
.catch(error => reject(error));
});
@ -552,18 +530,6 @@ export const getBillingInfo = () => {
});
};
export const updateBillingInfo = (options = {}) => {
const params = { billing_info: options };
return new Promise((resolve, reject) => {
apiFetch(`/recurly/update_billing_info`, {
method: 'PUT',
body: JSON.stringify(params)
})
.then(response => resolve(response))
.catch(error => reject(error));
});
};
export const createRecurlyAccount = (options = {}) => {
return new Promise((resolve, reject) => {
apiFetch(`/recurly/create_account`, {
@ -681,7 +647,7 @@ export const createAlert = (subject, data) => {
return new Promise((resolve, reject) => {
apiFetch(`/alerts`, {
method: 'POST',
body: JSON.stringify({ subject, data })
body: JSON.stringify({subject, data})
})
.then(response => resolve(response))
.catch(error => reject(error));
@ -695,13 +661,6 @@ export const getClientDownloads = () => {
.catch(error => reject(error));
});
}
export const getObsPluginDownloads = () => {
return new Promise((resolve, reject) => {
apiFetch(`/artifacts/OBSPlugin`)
.then(response => resolve(response))
.catch(error => reject(error));
});
}
//paypalPlaceOrder
export const paypalPlaceOrder = (options = {}) => {
@ -714,61 +673,3 @@ export const paypalPlaceOrder = (options = {}) => {
.catch(error => reject(error));
});
};
export const submitStripe = (options = {}) => {
return new Promise((resolve, reject) => {
apiFetch(`/stripe`, {
method: 'POST',
body: JSON.stringify(options)
})
.then(response => resolve(response))
.catch(error => reject(error));
})
}
// function updatePayment(options) {
// options = options || {}
// return $.ajax({
// type: "POST",
// url: '/api/recurly/update_payment',
// dataType: "json",
// contentType: 'application/json',
// data: JSON.stringify(options)
// })
// }
export const updatePayment = (options = {}) => {
return new Promise((resolve, reject) => {
apiFetch(`/recurly/update_payment`, {
method: 'POST',
body: JSON.stringify(options)
})
.then(response => resolve(response))
.catch(error => reject(error));
});
};
// function postUpdateEmail(email, current_password) {
// var url = "/api/users/" + context.JK.currentUserId + "/update_email";
// return $.ajax({
// type: "POST",
// dataType: "json",
// contentType: 'application/json',
// url: url,
// data: JSON.stringify({ update_email: email, current_password: current_password }),
// processData: false
// });
// }
export const updateEmail = (userId, email, current_password) => {
return new Promise((resolve, reject) => {
apiFetch(`/users/${userId}/update_email`, {
method: 'POST',
body: JSON.stringify({ update_email: email, current_password })
})
.then(response => resolve(response))
.catch(error => reject(error));
});
}

View File

@ -1,7 +1,6 @@
const insertStylesheet = ({ isRTL, isDark }, cb) => {
const link = document.createElement('link');
//link.href = `${process.env.PUBLIC_URL}/css/theme${isDark ? '-dark' : ''}${isRTL ? '-rtl' : ''}.css`;
link.href = `/css/theme${isDark ? '-dark' : ''}${isRTL ? '-rtl' : ''}.css`;
link.href = `${process.env.PUBLIC_URL}/css/theme${isDark ? '-dark' : ''}${isRTL ? '-rtl' : ''}.css`;
link.type = 'text/css';
link.rel = 'stylesheet';
link.className = 'theme-stylesheet';

View File

@ -26,12 +26,6 @@
"confirmation_email_sent": "A confirmation email has been sent to your email address. Please click the link in the email to confirm your email address."
}
},
"changed_email_confirmation": {
"title": "Change Email Confirmation",
"loadding": "Loading...",
"success": "Your email has been successfully changed.",
"error": "An error occurred while confirming your email change. Please try again later."
},
"password_form": {
"title": "Password",
"help_text": "To update the password associated with your account, enter your current password (for security reasons) and the new password, and click the \"Save Password\" button.",
@ -115,68 +109,5 @@
"no_payments": "No payments found.",
"load_more": "Load More",
"loading": "Loading..."
},
"payment_method": {
"page_title": "Payment Method",
"header": "Address and Payment Method",
"help_text_no_card": "You do not currently have a payment method on file.",
"help_text_has_card": "You currently have a payment method on file.",
"help_text": "To update your payment method, first enter your billing address and click the save button. If credit card, enter your card information and click the Save button. If PayPal, click the save button and follow PayPal's on-screen instructions to sign into your account and authorize payment to JamKazam.",
"credit_card_number": "Credit Card Number",
"expiration_date": "Expiration Date (MM/YY)",
"cvv": "CVV",
"submit": "Save Payment Method",
"first_name": "First Name",
"last_name": "Last Name",
"address1": "Address 1",
"address2": "Address 2",
"city": "City",
"state": "State or Region",
"zip_code": "Zip/Postal Code",
"country": "Country",
"credit_card": "Credit Card",
"paypal": "PayPal",
"we_accept": "We accept",
"submitting": "Submitting...",
"save_payment_info": "Save Payment Information",
"validations": {
"first_name": {
"required": "First Name is required"
},
"last_name": {
"required": "Last Name is required"
},
"address1": {
"required": "Address Line 1 is required"
},
"city": {
"required": "City is required"
},
"state": {
"required": "State or Region is required"
},
"zip_code": {
"required": "Zip/Postal Code is required"
},
"country": {
"required": "Country is required"
},
"card": {
"invalid": "Credit card details are invalid"
}
},
"aggreement": {
"text1": "By clicking",
"text2": "Save Payment Information",
"text3": "you agree to JamKazam's",
"terms_of_service": "Terms of Service"
},
"alerts": {
"try_again": "Please try again.",
"payment_method_updated": "Your payment method has been successfully updated.",
"card_update_error": "Failed to update payment method. Please try again later.",
"card_processing_error": "There was an error processing your card. Please check your details and try again.",
"billing_update_error": "There was an error processing your billing information. Please check your details and try again."
}
}
}

View File

@ -40,8 +40,7 @@
},
"no_records_1": "There are no public, open sessions currently available for you to join. We suggest you ",
"create_session": "create a session",
"no_records_2": " that others can join now, or wait a bit and then refresh this page in your browser to see if new public sessions have been started.",
"error": "There was an error retrieving the session list. Please try again."
"no_records_2": " that others can join now, or wait a bit and then refresh this page in your browser to see if new public sessions have been started."
},
"lobby": {
"page_title": "Lobby",

View File

@ -1,6 +1,3 @@
{
"page_title": "Unsubscribe from JamKazam emails",
"success": "You have successfully unsubscribed from JamKazam emails.",
"error": "An error occurred while unsubscribing. Please try again later.",
"loading": "Loading..."
"page_title": "Unsubscribe"
}

View File

@ -4,8 +4,6 @@ import 'react-app-polyfill/stable';
import React from 'react';
import ReactDOM from 'react-dom';
import HttpsRedirect from 'react-https-redirect';
import { BrowserRouter as Router } from 'react-router-dom';
import App from './App';
import Main from './Main';
import './helpers/initFA';
@ -14,9 +12,7 @@ import './i18n/config';
ReactDOM.render(
<HttpsRedirect disabled={process.env.NODE_ENV === 'development'}>
<Main>
<Router>
<App />
</Router>
<App />
</Main>
</HttpsRedirect>,
document.getElementById('main')

View File

@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import { Route, Switch } from 'react-router-dom';
import React, {useEffect} from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { toast, ToastContainer } from 'react-toastify';
import { CloseButton, Fade } from '../components/common/Toast';
@ -8,7 +8,6 @@ import ErrorLayout from './ErrorLayout';
import BuildMeta from "./JKBuildMeta";
import loadable from '@loadable/component';
import MetaTracking from "../helpers/MetaTracking";
const AuthBasicLayout = loadable(() => import('./JKAuthBasicLayout'));
const PublicLayout = loadable(() => import('./JKPublicLayout'));
@ -16,7 +15,6 @@ const Layout = () => {
useEffect(() => {
AuthBasicLayout.preload();
PublicLayout.preload();
MetaTracking.init();
//see if there is affiliate in query string and save it as cookie
const urlParams = new URLSearchParams(window.location.search);
@ -30,7 +28,7 @@ const Layout = () => {
}, []);
return (
<>
<Router fallback={<span />}>
<Switch>
<Route path="/errors" component={ErrorLayout} />
<Route path="/auth" component={AuthBasicLayout} />
@ -39,7 +37,7 @@ const Layout = () => {
</Switch>
<ToastContainer transition={Fade} closeButton={<CloseButton />} position={toast.POSITION.BOTTOM_RIGHT} />
<BuildMeta />
</>
</Router>
);
};

View File

@ -10,8 +10,6 @@ import JKForum from '../components/public/help/JKForum';
import JKUnsubscribe from '../components/public/JKUnsubscribe';
import JKDownloads from '../components/public/JKDownloads';
import JKDownloadsLegacy from '../components/public/JKDownloadsLegacy';
import JKObsDownloads from '../components/public/JKObsDownloads';
import JKConfirmEmailChange from '../components/public/JKConfirmEmailChange';
import JKJamTracksLanding from '../components/jamtracks/JKJamTracksLandingDev';
import JKJamTracksArtistLanding from '../components/jamtracks/JKJamTracksArtistLandingDev';
@ -23,11 +21,9 @@ const JKPublicRoutes = ({ match: { url } }) => (
<Route path={`${url}/knowledge-base`} component={JKKnowledgeBase} />
<Route path={`${url}/help-desk`} component={JKHelpDesk} />
<Route path={`${url}/forum`} component={JKForum} />
{/* <Route path={`${url}/unsubscribe/:tok`} exact component={JKUnsubscribe} />
<Route path={`${url}/confirm-email-change`} exact component={JKConfirmEmailChange} /> */}
<Route path={`${url}/unsubscribe`} exact component={JKUnsubscribe} />
<Route path={`${url}/downloads`} exact component={JKDownloads} />
<Route path={`${url}/downloads-legacy`} exact component={JKDownloadsLegacy} />
<Route path={`${url}/obs-plugin-download`} exact component={JKObsDownloads} />
<Route path={`${url}/backing-tracks/:artist/:song`} component={JKJamTracksLanding} />
<Route path={`${url}/backing-tracks/:artist`} component={JKJamTracksArtistLanding} />

View File

@ -1,5 +1,5 @@
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { getSessions, getFriendsSessions, getPublicSessions, getInactiveSessions, getPersonById } from '../../helpers/rest'
import { getSessions, getPersonById } from '../../helpers/rest'
const initialState = {
sessions: [],
@ -9,50 +9,27 @@ const initialState = {
}
export const fetchSessions = createAsyncThunk(
"session/fetchSessions",
"session/fetchSessions",
async (options, thunkAPI) => {
const response = await getSessions();
return response.json();
}
)
export const fetchFriendsSessions = createAsyncThunk(
"session/fetchFriendsSessions",
async (options, thunkAPI) => {
const response = await getFriendsSessions(options);
return response.json();
}
)
export const fetchPublicSessions = createAsyncThunk(
"session/fetchPublicSessions",
async (options, thunkAPI) => {
const response = await getPublicSessions(options);
return response.json();
}
)
export const fetchInactiveSessions = createAsyncThunk(
"session/fetchInactiveSessions",
async (options, thunkAPI) => {
const response = await getInactiveSessions(options);
return response.json();
}
)
export const fetchPerson = createAsyncThunk(
'session/fetchPerson',
'session/fetchPerson',
async (options, thunkAPI) => {
const { userId } = options
const {userId} = options
const response = await getPersonById(userId)
return response.json()
}
, {
condition: (options, { getState, extra }) => {
const { session } = getState()
const { userId } = options
,{
condition: (options, {getState, extra}) => {
const {session} = getState()
const {userId} = options
const person = session.people.find(person => person.id === userId)
if (person && person.website) {
if(person && person.website){
//only proceed if full data set for user has not been fetched. person.website is not included in the initial data fetching (i.e: in friends listing ).
return false;
}
@ -64,9 +41,9 @@ export const SessionSlice = createSlice({
name: "session",
initialState,
reducers: {
addSession: (state) => { },
updateSession: (state) => { },
deleteSession: (state) => { },
addSession: (state) => {},
updateSession: (state) => {},
deleteSession: (state) => {},
},
extraReducers: (builder) => {
builder
@ -74,68 +51,28 @@ export const SessionSlice = createSlice({
state.status = "loading";
})
.addCase(fetchSessions.fulfilled, (state, action) => {
// add unique sessions to the array
const records = new Set([...state.sessions, ...action.payload]);
const unique = [];
records.map(x => unique.filter(p => p.id === x.id).length > 0 ? null : unique.push(x))
state.sessions = unique;
state.error = null;
console.log(action.payload);
state.status = "succeeded";
state.sessions = action.payload;
})
.addCase(fetchSessions.rejected, (state, action) => {
state.status = 'failed'
state.error = action.error.message
})
.addCase(fetchFriendsSessions.pending, (state, action) => {
state.status = "loading";
})
.addCase(fetchFriendsSessions.fulfilled, (state, action) => {
// add unique sessions to the array
const records = new Set([...state.sessions, ...action.payload.sessions]);
const unique = [];
records.map(x => unique.filter(p => p.id === x.id).length > 0 ? null : unique.push(x))
state.sessions = unique;
state.error = null;
state.status = "succeeded";
})
.addCase(fetchPublicSessions.pending, (state, action) => {
state.status = "loading";
})
.addCase(fetchPublicSessions.fulfilled, (state, action) => {
// add unique sessions to the array
const records = new Set([...state.sessions, ...action.payload.sessions]);
const unique = [];
records.map(x => unique.filter(p => p.id === x.id).length > 0 ? null : unique.push(x))
state.sessions = unique;
state.error = null;
state.status = "succeeded";
})
.addCase(fetchInactiveSessions.pending, (state, action) => {
state.status = "loading";
})
.addCase(fetchInactiveSessions.fulfilled, (state, action) => {
// add unique sessions to the array
const records = new Set([...state.sessions, ...action.payload.sessions]);
const unique = [];
records.map(x => unique.filter(p => p.id === x.id).length > 0 ? null : unique.push(x))
state.sessions = unique;
state.error = null;
state.status = "succeeded";
state.error = action.error.message
})
.addCase(fetchPerson.fulfilled, (state, action) => {
const person = state.people.find(person => person.id === action.payload.id)
if (person) {
if(person){
const updated = {
...person,
...person,
...action.payload
}
const objIndex = state.people.findIndex((p => p.id === updated.id));
state.people[objIndex] = updated
} else {
}else{
state.people.push(action.payload)
}
})
}
}
})
export default SessionSlice.reducer;

View File

@ -1,23 +0,0 @@
class AddProfileCompleteColumnsToUsers < ActiveRecord::Migration
def self.up
execute "ALTER TABLE users ADD COLUMN profile_completed_at TIMESTAMP"
#add index on profile_completed_at
execute "CREATE INDEX index_users_on_profile_completed_at ON users USING btree (profile_completed_at)"
execute "ALTER TABLE users ADD COLUMN profile_complete_reminder1_sent_at TIMESTAMP"
execute "ALTER TABLE users ADD COLUMN profile_complete_reminder2_sent_at TIMESTAMP"
execute "ALTER TABLE users ADD COLUMN profile_complete_reminder3_sent_at TIMESTAMP"
User.find_each(batch_size:100) do |user|
User.where(id:user.id).update_all(profile_completed_at: Time.now)
end
#User.where('users.id IN (SELECT player_id FROM musicians_instruments) OR users.id IN (SELECT player_id FROM genre_players)').update_all(profile_completed_at: Time.now)
end
def self.down
execute "ALTER TABLE users DROP COLUMN profile_completed_at"
execute "ALTER TABLE users DROP COLUMN profile_complete_reminder1_sent_at"
execute "ALTER TABLE users DROP COLUMN profile_complete_reminder2_sent_at"
execute "ALTER TABLE users DROP COLUMN profile_complete_reminder3_sent_at"
end
end

View File

@ -1,60 +0,0 @@
# Adds
# affiliate_partners.jamtrack_cumulative_earnings_in_cents
# affiliate_partners.subscriptions_cumulative_earnings_in_cents
# affiliate_partners.subscriptions_count
# affiliate_quarterly_payments.jamtrack_due_amount_in_cents
# affiliate_quarterly_payments.subscription_due_amount_in_cents
# affiliate_monthly_payments.jamtrack_due_amount_in_cents
# affiliate_monthly_payments.subscription_due_amount_in_cents
class AffiliateTrackingTotals < ActiveRecord::Migration
def self.up
execute "ALTER TABLE affiliate_partners ADD COLUMN jamtrack_cumulative_earnings_in_cents INTEGER NOT NULL DEFAULT 0"
execute "ALTER TABLE affiliate_partners ADD COLUMN subscriptions_cumulative_earnings_in_cents INTEGER NOT NULL DEFAULT 0"
execute "ALTER TABLE affiliate_partners ADD COLUMN current_quarter_in_cents INTEGER NOT NULL DEFAULT 0"
execute "ALTER TABLE affiliate_partners ADD COLUMN jamtrack_current_quarter_in_cents INTEGER NOT NULL DEFAULT 0"
execute "ALTER TABLE affiliate_partners ADD COLUMN subscriptions_current_quarter_in_cents INTEGER NOT NULL DEFAULT 0"
execute "ALTER TABLE affiliate_partners ADD COLUMN jamtracks_sold INTEGER NOT NULL DEFAULT 0"
execute "ALTER TABLE affiliate_partners ADD COLUMN subscriptions_count INTEGER NOT NULL DEFAULT 0"
# execute "ALTER TABLE affiliate_quarterly_payments ADD COLUMN subscriptions_count INTEGER NOT NULL DEFAULT 0"
# execute "ALTER TABLE affiliate_monthy_payments ADD COLUMN subscriptions_count INTEGER NOT NULL DEFAULT 0"
# execute "ALTER TABLE affiliate_quarterly_payments ADD COLUMN jamtrack_due_amount_in_cents INTEGER NOT NULL DEFAULT 0"
# execute "ALTER TABLE affiliate_quarterly_payments ADD COLUMN subscription_due_amount_in_cents INTEGER NOT NULL DEFAULT 0"
#execute "ALTER TABLE affiliate_monthly_payments ADD COLUMN jamtrack_due_amount_in_cents INTEGER NOT NULL DEFAULT 0"
#execute "ALTER TABLE affiliate_monthly_payments ADD COLUMN subscription_due_amount_in_cents INTEGER NOT NULL DEFAULT 0"
execute "CREATE INDEX affiliate_partner_user_id_idx ON affiliate_partners USING btree (partner_user_id);"
# execute "CREATE INDEX affiliate_quarterly_payments_closed_index ON affiliate_quarterly_payments USING btree (paid);"
# execute "CREATE INDEX affiliate_quarterly_payments_paid_index ON affiliate_quarterly_payments USING btree (closed);"
# execute "CREATE INDEX affiliate_monthly_payments_paid_index ON affiliate_monthly_payments USING btree (closed);"
end
=begin
ALTER TABLE affiliate_partners ADD COLUMN jamtrack_cumulative_earnings_in_cents INTEGER NOT NULL DEFAULT 0;
ALTER TABLE affiliate_partners ADD COLUMN subscriptions_cumulative_earnings_in_cents INTEGER NOT NULL DEFAULT 0;
ALTER TABLE affiliate_partners ADD COLUMN current_quarter_in_cents INTEGER NOT NULL DEFAULT 0;
ALTER TABLE affiliate_partners ADD COLUMN jamtrack_current_quarter_in_cents INTEGER NOT NULL DEFAULT 0;
ALTER TABLE affiliate_partners ADD COLUMN subscriptions_current_quarter_in_cents INTEGER NOT NULL DEFAULT 0;
ALTER TABLE affiliate_partners ADD COLUMN jamtracks_sold INTEGER NOT NULL DEFAULT 0;
ALTER TABLE affiliate_partners ADD COLUMN subscriptions_count INTEGER NOT NULL DEFAULT 0;
ALTER TABLE affiliate_quarterly_payments ADD COLUMN jamtrack_due_amount_in_cents INTEGER NOT NULL DEFAULT 0;
ALTER TABLE affiliate_quarterly_payments ADD COLUMN subscription_due_amount_in_cents INTEGER NOT NULL DEFAULT 0;
ALTER TABLE affiliate_monthly_payments ADD COLUMN jamtrack_due_amount_in_cents INTEGER NOT NULL DEFAULT 0;
ALTER TABLE affiliate_monthly_payments ADD COLUMN subscription_due_amount_in_cents INTEGER NOT NULL DEFAULT 0;
ALTER TABLE affiliate_quarterly_payments ADD COLUMN subscriptions_count INTEGER NOT NULL DEFAULT 0;
ALTER TABLE affiliate_monthly_payments ADD COLUMN subscriptions_count INTEGER NOT NULL DEFAULT 0;
CREATE INDEX affiliate_partner_user_id_idx ON affiliate_partners USING btree (partner_user_id);
CREATE INDEX affiliate_quarterly_payments_closed_index ON affiliate_quarterly_payments USING btree (paid);
CREATE INDEX affiliate_quarterly_payments_paid_index ON affiliate_quarterly_payments USING btree (closed);
CREATE INDEX affiliate_monthly_payments_paid_index ON affiliate_monthly_payments USING btree (closed);
=end
def self.down
execute "ALTER TABLE affiliate_partners DROP COLUMN jamtrack_cumulative_earnings_in_cents"
execute "ALTER TABLE affiliate_partners DROP COLUMN subscriptions_cumulative_earnings_in_cents"
execute "ALTER TABLE affiliate_partners DROP COLUMN subscriptions_count"
# execute "ALTER TABLE affiliate_quarterly_payments DROP COLUMN jamtrack_due_amount_in_cents"
# execute "ALTER TABLE affiliate_quarterly_payments DROP COLUMN subscription_due_amount_in_cents"
end
end

View File

@ -1,13 +0,0 @@
class AddGearSetupReminderColumnsToUsers < ActiveRecord::Migration
def self.up
execute "ALTER TABLE users ADD COLUMN gear_setup_reminder1_sent_at TIMESTAMP"
execute "ALTER TABLE users ADD COLUMN gear_setup_reminder2_sent_at TIMESTAMP"
execute "ALTER TABLE users ADD COLUMN gear_setup_reminder3_sent_at TIMESTAMP"
end
def self.down
execute "ALTER TABLE users DROP COLUMN gear_setup_reminder1_sent_at"
execute "ALTER TABLE users DROP COLUMN gear_setup_reminder2_sent_at"
execute "ALTER TABLE users DROP COLUMN gear_setup_reminder3_sent_at"
end
end

View File

@ -1,19 +0,0 @@
class AddSignupSurveySentAtToUsers < ActiveRecord::Migration
def self.up
execute "ALTER TABLE users ADD COLUMN signup_survey_sent_at TIMESTAMP"
User.find_each(batch_size:100) do |user|
User.where(id:user.id).update_all(signup_survey_sent_at: Time.now)
end
end
def self.down
execute "ALTER TABLE users DROP COLUMN signup_survey_sent_at"
end
end
=begin
ALTER TABLE users ADD COLUMN signup_survey_sent_at TIMESTAMP;
CREATE INDEX index_users_on_signup_survey_sent_at ON users USING btree (signup_survey_sent_at);
User.find_each(batch_size:100) do |user|
User.where(id:user.id).update_all(signup_survey_sent_at: Time.now)
end
=end

View File

@ -1,13 +0,0 @@
class AddTestGearReminderColumnsToUsers < ActiveRecord::Migration
def self.up
execute "ALTER TABLE users ADD COLUMN test_gear_reminder1_sent_at TIMESTAMP"
execute "ALTER TABLE users ADD COLUMN test_gear_reminder2_sent_at TIMESTAMP"
execute "ALTER TABLE users ADD COLUMN test_gear_reminder3_sent_at TIMESTAMP"
end
def self.down
execute "ALTER TABLE users DROP COLUMN test_gear_reminder1_sent_at"
execute "ALTER TABLE users DROP COLUMN test_gear_reminder2_sent_at"
execute "ALTER TABLE users DROP COLUMN test_gear_reminder3_sent_at"
end
end

View File

@ -1,12 +0,0 @@
class AddGroupSessionReminderColumnsToUsers < ActiveRecord::Migration
def self.up
execute "ALTER TABLE users ADD COLUMN group_session_reminder1_sent_at TIMESTAMP"
execute "ALTER TABLE users ADD COLUMN group_session_reminder2_sent_at TIMESTAMP"
execute "ALTER TABLE users ADD COLUMN group_session_reminder3_sent_at TIMESTAMP"
end
def self.down
execute "ALTER TABLE users DROP COLUMN group_session_reminder1_sent_at"
execute "ALTER TABLE users DROP COLUMN group_session_reminder2_sent_at"
execute "ALTER TABLE users DROP COLUMN group_session_reminder3_sent_at"
end
end

View File

@ -1,19 +0,0 @@
class AddTrailExpiresReminderColumnsToUsers < ActiveRecord::Migration
def self.up
execute "ALTER TABLE users ADD COLUMN trial_expires_reminder1_sent_at TIMESTAMP"
execute "ALTER TABLE users ADD COLUMN trial_expires_reminder2_sent_at TIMESTAMP"
execute "ALTER TABLE users ADD COLUMN trial_expires_reminder3_sent_at TIMESTAMP"
# slide in some more production indexes
execute "CREATE INDEX index_users_on_first_music_session_at ON users USING btree (first_music_session_at)"
# subscription_sync_code
execute "CREATE INDEX index_users_on_subscription_sync_code ON users USING btree (subscription_sync_code)"
# first_certified_gear_at
execute "CREATE INDEX index_users_on_first_certified_gear_at ON users USING btree (first_certified_gear_at)"
end
def self.down
execute "ALTER TABLE users DROP COLUMN trial_expires_reminder1_sent_at"
execute "ALTER TABLE users DROP COLUMN trial_expires_reminder2_sent_at"
execute "ALTER TABLE users DROP COLUMN trial_expires_reminder3_sent_at"
end
end

View File

@ -1,11 +0,0 @@
class AddFacebookTrackingToUsers < ActiveRecord::Migration
def up
execute "ALTER TABLE users ADD COLUMN facebook_click_id varchar(1000)"
execute "ALTER TABLE users ADD COLUMN facebook_browser_id varchar(1000)"
end
def down
execute "ALTER TABLE users DROP COLUMN facebook_click_id"
execute "ALTER TABLE users DROP COLUMN facebook_browser_id"
end
end

Some files were not shown because too many files have changed in this diff Show More