wip - react components related to musician listing and filtering

This commit is contained in:
Nuwan Chathuranga 2021-08-26 22:53:24 +05:30 committed by Nuwan
parent eb4c327eff
commit 6c88cd1215
54 changed files with 1799 additions and 459 deletions

View File

@ -0,0 +1,79 @@
{
"id": "1",
"first_name": "Test",
"last_name": "User1",
"name": "Test User1",
"city": "Denver",
"state": "CO",
"country": "US",
"location": "Denver, CO",
"online": true,
"photo_url": null,
"musician": true,
"gender": "M",
"birth_date": null,
"friend_count": 1,
"liker_count": 0,
"follower_count": 0,
"following_count": 0,
"recording_count": 0,
"session_count": 0,
"biography": "Biography of Test User1",
"favorite_count": 0,
"audio_latency": null,
"upcoming_session_count": 0,
"age": null,
"website": "www.testuser1.com",
"skill_level": 2,
"concert_count": 4,
"studio_session_count": 4,
"virtual_band": true,
"virtual_band_commitment": 2,
"traditional_band": true,
"traditional_band_commitment": 4,
"traditional_band_touring": true,
"paid_sessions": true,
"paid_sessions_hourly_rate": 10000,
"paid_sessions_daily_rate": 200000,
"free_sessions": true,
"cowriting": true,
"cowriting_purpose": 2,
"subscribe_email": true,
"is_a_teacher": false,
"is_a_student": false,
"online_presences": [
{ "id": "e1962204-f652-41b0-84d6-1afd7e9172be", "service_type": "soundcloud", "username": "testuser" },
{ "id": "005a7c78-db8b-4f72-a51f-d64d579c22b0", "service_type": "reverbnation", "username": "testuser" },
{ "id": "2dd22eef-03ba-4743-b65b-5a194591dc86", "service_type": "bandcamp", "username": "testuser" },
{ "id": "d6ae62b4-e1ce-4cf0-90b7-c64033533261", "service_type": "fandalism", "username": "testuser" },
{ "id": "c6e85453-0fa9-40d0-9754-8f372d6e0ed3", "service_type": "youtube", "username": "testuser" },
{ "id": "480ec1ad-ea1d-4990-9c68-d7f9c0174441", "service_type": "facebook", "username": "testuser" },
{ "id": "232b26d5-c75a-4d65-9013-a07b73c8a7ae", "service_type": "twitter", "username": "testuser" }
],
"performance_samples": [],
"genres": [
{ "genre_id": "asian", "player_type": "JamRuby::User", "genre_type": "profile" },
{ "genre_id": "classical", "player_type": "JamRuby::User", "genre_type": "profile" },
{ "genre_id": "african", "player_type": "JamRuby::User", "genre_type": "virtual_band" },
{ "genre_id": "classical", "player_type": "JamRuby::User", "genre_type": "virtual_band" },
{ "genre_id": "classical", "player_type": "JamRuby::User", "genre_type": "traditional_band" },
{ "genre_id": "blues", "player_type": "JamRuby::User", "genre_type": "free_sessions" },
{ "genre_id": "soft rock", "player_type": "JamRuby::User", "genre_type": "free_sessions" },
{ "genre_id": "celtic", "player_type": "JamRuby::User", "genre_type": "cowriting" },
{ "genre_id": "tv & movie soundtrack", "player_type": "JamRuby::User", "genre_type": "cowriting" }
],
"bands": [],
"instruments": [
{ "description": "Acoustic Guitar", "proficiency_level": 3, "priority": 1, "instrument_id": "acoustic guitar" },
{ "description": "Keyboard", "proficiency_level": 3, "priority": 8, "instrument_id": "keyboard" },
{ "description": "Ukulele", "proficiency_level": 3, "priority": 11, "instrument_id": "ukulele" },
{ "description": "Voice", "proficiency_level": 3, "priority": 13, "instrument_id": "voice" },
{ "description": "Piano", "proficiency_level": 2, "priority": 10, "instrument_id": "piano" }
],
"is_friend": true,
"is_following": false,
"is_liking": false,
"pending_friend_request": false,
"my_audio_latency": 5,
"internet_score": null
}

View File

@ -3,32 +3,38 @@
{
"id": "1",
"first_name": "Test",
"last_name": "User",
"name": "Test User",
"city": "City",
"state": "NC",
"last_name": "User1",
"name": "Test User1",
"city": "Denver",
"state": "CO",
"country": "US",
"online": false,
"online": true,
"musician": true,
"photo_url": null,
"biography": "",
"biography": "Biography of Test User1",
"full_score": null,
"instruments": [],
"instruments": [
{ "instrument_id": "acoustic guitar", "description": "Acoustic Guitar", "proficiency_level": 3, "priority": 1 },
{ "instrument_id": "keyboard", "description": "Keyboard", "proficiency_level": 3, "priority": 8 },
{ "instrument_id": "ukulele", "description": "Ukulele", "proficiency_level": 3, "priority": 11 },
{ "instrument_id": "voice", "description": "Voice", "proficiency_level": 3, "priority": 13 },
{ "instrument_id": "piano", "description": "Piano", "proficiency_level": 2, "priority": 10 }
],
"followings": [],
"is_friend": false,
"is_friend": true,
"is_following": false,
"pending_friend_request": false,
"friend_count": 0,
"friend_count": 1,
"follow_count": 0,
"recording_count": 0,
"session_count": 0,
"audio_latency": null
"session_count": 10,
"audio_latency": 5
},
{
"id": "a09f9a7e-afb7-489d-870d-e13a336e0b97",
"first_name": "Seth",
"last_name": "Call",
"name": "Seth Call",
"id": "2",
"first_name": "Test",
"last_name": "User2",
"name": "Test User2",
"city": "Austin",
"state": "TX",
"country": "US",
@ -51,10 +57,10 @@
"audio_latency": null
},
{
"id": "3dfca858-0e7c-4ad4-993a-c39421d93853",
"first_name": "Peter",
"last_name": "Walker",
"name": "Peter Walker",
"id": "3",
"first_name": "Test",
"last_name": "User3",
"name": "Test User3",
"city": "Austin",
"state": "TX",
"country": "US",
@ -77,10 +83,10 @@
"audio_latency": null
},
{
"id": "963d5268-66b6-463a-a3ee-c97f274fc23f",
"first_name": "Peter",
"last_name": "Walker",
"name": "Peter Walker",
"id": "4",
"first_name": "Test",
"last_name": "User4",
"name": "Test User4",
"city": "Austin",
"state": "TX",
"country": "US",
@ -103,10 +109,10 @@
"audio_latency": null
},
{
"id": "feb671a3-1821-48f0-bc14-aa26cf98bb25",
"first_name": "David",
"last_name": "Wilson",
"name": "David Wilson",
"id": "5",
"first_name": "Test",
"last_name": "User5",
"name": "Test User5",
"city": "Austin",
"state": "TX",
"country": "US",
@ -129,10 +135,379 @@
"audio_latency": null
},
{
"id": "b1ddadd0-0263-47c4-bf91-e7767f386970",
"first_name": "Oswald",
"last_name": "Becca",
"name": "Oswald Becca",
"id": "6",
"first_name": "Test",
"last_name": "User6",
"name": "Test User6",
"city": "Austin",
"state": "TX",
"country": "US",
"online": false,
"musician": true,
"photo_url": null,
"biography": "",
"full_score": null,
"instruments": [
{ "instrument_id": "acoustic guitar", "description": "Acoustic Guitar", "proficiency_level": 3, "priority": 1 }
],
"followings": [],
"is_friend": false,
"is_following": false,
"pending_friend_request": false,
"friend_count": 0,
"follow_count": 0,
"recording_count": 0,
"session_count": 0,
"audio_latency": null
},
{
"id": "7",
"first_name": "Test",
"last_name": "User7",
"name": "Test User7",
"city": "Austin",
"state": "TX",
"country": "US",
"online": false,
"musician": true,
"photo_url": null,
"biography": "",
"full_score": null,
"instruments": [
{ "instrument_id": "acoustic guitar", "description": "Acoustic Guitar", "proficiency_level": 3, "priority": 1 }
],
"followings": [],
"is_friend": false,
"is_following": false,
"pending_friend_request": false,
"friend_count": 0,
"follow_count": 0,
"recording_count": 0,
"session_count": 0,
"audio_latency": null
},
{
"id": "8",
"first_name": "Test",
"last_name": "User8",
"name": "Test User8",
"city": "Austin",
"state": "TX",
"country": "US",
"online": false,
"musician": true,
"photo_url": null,
"biography": "",
"full_score": null,
"instruments": [
{ "instrument_id": "acoustic guitar", "description": "Acoustic Guitar", "proficiency_level": 3, "priority": 1 }
],
"followings": [],
"is_friend": false,
"is_following": false,
"pending_friend_request": false,
"friend_count": 0,
"follow_count": 0,
"recording_count": 0,
"session_count": 0,
"audio_latency": null
},
{
"id": "9",
"first_name": "Test",
"last_name": "User9",
"name": "Test User9",
"city": "Austin",
"state": "TX",
"country": "US",
"online": false,
"musician": true,
"photo_url": null,
"biography": "",
"full_score": null,
"instruments": [
{ "instrument_id": "acoustic guitar", "description": "Acoustic Guitar", "proficiency_level": 3, "priority": 1 }
],
"followings": [],
"is_friend": false,
"is_following": false,
"pending_friend_request": false,
"friend_count": 0,
"follow_count": 0,
"recording_count": 0,
"session_count": 0,
"audio_latency": null
},
{
"id": "10",
"first_name": "Test",
"last_name": "User10",
"name": "Test User10",
"city": "Austin",
"state": "TX",
"country": "US",
"online": false,
"musician": true,
"photo_url": null,
"biography": "",
"full_score": null,
"instruments": [
{ "instrument_id": "acoustic guitar", "description": "Acoustic Guitar", "proficiency_level": 3, "priority": 1 }
],
"followings": [],
"is_friend": false,
"is_following": false,
"pending_friend_request": false,
"friend_count": 0,
"follow_count": 0,
"recording_count": 0,
"session_count": 0,
"audio_latency": null
},
{
"id": "11",
"first_name": "Test",
"last_name": "User11",
"name": "Test User11",
"city": "Denver",
"state": "CO",
"country": "US",
"online": true,
"musician": true,
"photo_url": null,
"biography": "Biography of Test User1",
"full_score": null,
"instruments": [
{ "instrument_id": "acoustic guitar", "description": "Acoustic Guitar", "proficiency_level": 3, "priority": 1 },
{ "instrument_id": "keyboard", "description": "Keyboard", "proficiency_level": 3, "priority": 8 },
{ "instrument_id": "ukulele", "description": "Ukulele", "proficiency_level": 3, "priority": 11 },
{ "instrument_id": "voice", "description": "Voice", "proficiency_level": 3, "priority": 13 },
{ "instrument_id": "piano", "description": "Piano", "proficiency_level": 2, "priority": 10 }
],
"followings": [],
"is_friend": true,
"is_following": false,
"pending_friend_request": false,
"friend_count": 1,
"follow_count": 0,
"recording_count": 0,
"session_count": 10,
"audio_latency": 5
},
{
"id": "12",
"first_name": "Test",
"last_name": "User12",
"name": "Test User12",
"city": "Austin",
"state": "TX",
"country": "US",
"online": false,
"musician": true,
"photo_url": null,
"biography": "",
"full_score": null,
"instruments": [
{ "instrument_id": "acoustic guitar", "description": "Acoustic Guitar", "proficiency_level": 3, "priority": 1 }
],
"followings": [],
"is_friend": false,
"is_following": false,
"pending_friend_request": false,
"friend_count": 0,
"follow_count": 0,
"recording_count": 0,
"session_count": 0,
"audio_latency": null
},
{
"id": "13",
"first_name": "Test",
"last_name": "User13",
"name": "Test User13",
"city": "Austin",
"state": "TX",
"country": "US",
"online": false,
"musician": true,
"photo_url": null,
"biography": "",
"full_score": null,
"instruments": [
{ "instrument_id": "acoustic guitar", "description": "Acoustic Guitar", "proficiency_level": 3, "priority": 1 }
],
"followings": [],
"is_friend": false,
"is_following": false,
"pending_friend_request": false,
"friend_count": 0,
"follow_count": 0,
"recording_count": 0,
"session_count": 0,
"audio_latency": null
},
{
"id": "14",
"first_name": "Test",
"last_name": "User14",
"name": "Test User14",
"city": "Austin",
"state": "TX",
"country": "US",
"online": false,
"musician": true,
"photo_url": null,
"biography": "",
"full_score": null,
"instruments": [
{ "instrument_id": "acoustic guitar", "description": "Acoustic Guitar", "proficiency_level": 3, "priority": 1 }
],
"followings": [],
"is_friend": false,
"is_following": false,
"pending_friend_request": false,
"friend_count": 0,
"follow_count": 0,
"recording_count": 0,
"session_count": 0,
"audio_latency": null
},
{
"id": "15",
"first_name": "Test",
"last_name": "User15",
"name": "Test User15",
"city": "Austin",
"state": "TX",
"country": "US",
"online": false,
"musician": true,
"photo_url": null,
"biography": "",
"full_score": null,
"instruments": [
{ "instrument_id": "acoustic guitar", "description": "Acoustic Guitar", "proficiency_level": 3, "priority": 1 }
],
"followings": [],
"is_friend": false,
"is_following": false,
"pending_friend_request": false,
"friend_count": 0,
"follow_count": 0,
"recording_count": 0,
"session_count": 0,
"audio_latency": null
},
{
"id": "16",
"first_name": "Test",
"last_name": "User16",
"name": "Test User16",
"city": "Austin",
"state": "TX",
"country": "US",
"online": false,
"musician": true,
"photo_url": null,
"biography": "",
"full_score": null,
"instruments": [
{ "instrument_id": "acoustic guitar", "description": "Acoustic Guitar", "proficiency_level": 3, "priority": 1 }
],
"followings": [],
"is_friend": false,
"is_following": false,
"pending_friend_request": false,
"friend_count": 0,
"follow_count": 0,
"recording_count": 0,
"session_count": 0,
"audio_latency": null
},
{
"id": "17",
"first_name": "Test",
"last_name": "User17",
"name": "Test User17",
"city": "Austin",
"state": "TX",
"country": "US",
"online": false,
"musician": true,
"photo_url": null,
"biography": "",
"full_score": null,
"instruments": [
{ "instrument_id": "acoustic guitar", "description": "Acoustic Guitar", "proficiency_level": 3, "priority": 1 }
],
"followings": [],
"is_friend": false,
"is_following": false,
"pending_friend_request": false,
"friend_count": 0,
"follow_count": 0,
"recording_count": 0,
"session_count": 0,
"audio_latency": null
},
{
"id": "18",
"first_name": "Test",
"last_name": "User18",
"name": "Test User18",
"city": "Austin",
"state": "TX",
"country": "US",
"online": false,
"musician": true,
"photo_url": null,
"biography": "",
"full_score": null,
"instruments": [
{ "instrument_id": "acoustic guitar", "description": "Acoustic Guitar", "proficiency_level": 3, "priority": 1 }
],
"followings": [],
"is_friend": false,
"is_following": false,
"pending_friend_request": false,
"friend_count": 0,
"follow_count": 0,
"recording_count": 0,
"session_count": 0,
"audio_latency": null
},
{
"id": "19",
"first_name": "Test",
"last_name": "User19",
"name": "Test User19",
"city": "Austin",
"state": "TX",
"country": "US",
"online": false,
"musician": true,
"photo_url": null,
"biography": "",
"full_score": null,
"instruments": [
{ "instrument_id": "acoustic guitar", "description": "Acoustic Guitar", "proficiency_level": 3, "priority": 1 }
],
"followings": [],
"is_friend": false,
"is_following": false,
"pending_friend_request": false,
"friend_count": 0,
"follow_count": 0,
"recording_count": 0,
"session_count": 0,
"audio_latency": null
},
{
"id": "20",
"first_name": "Test",
"last_name": "User20",
"name": "Test User20",
"city": "Austin",
"state": "TX",
"country": "US",
@ -155,7 +530,7 @@
"audio_latency": null
}
],
"page_count": 1,
"page_count": 2,
"my_audio_latency": 5,
"filter_json": "{\"id\":\"68dcc055-cb5d-40d6-8ed4-66772d1a1a31\",\"user_id\":\"27bd4a30-d1b8-4eea-8454-01a104d59381\",\"foreign_key1_id\":null,\"data_blob\":{\"sort_order\":\"latency\",\"instruments\":[],\"genres\":[],\"concert_gigs\":\"-1\",\"interests\":\"any\",\"studio_sessions\":\"-1\",\"ages\":[],\"skill_level\":\"-1\"}}",
"description": "Current Search: Sort = Latency to Me",

View File

@ -0,0 +1,79 @@
{
"id": "1",
"first_name": "Test",
"last_name": "User1",
"name": "Test User1",
"city": "Denver",
"state": "CO",
"country": "US",
"location": "Denver, CO",
"online": true,
"photo_url": null,
"musician": true,
"gender": "M",
"birth_date": null,
"friend_count": 1,
"liker_count": 0,
"follower_count": 0,
"following_count": 0,
"recording_count": 0,
"session_count": 0,
"biography": "Biography of Test User1",
"favorite_count": 0,
"audio_latency": null,
"upcoming_session_count": 0,
"age": null,
"website": "www.testuser1.com",
"skill_level": 2,
"concert_count": 4,
"studio_session_count": 4,
"virtual_band": true,
"virtual_band_commitment": 2,
"traditional_band": true,
"traditional_band_commitment": 4,
"traditional_band_touring": true,
"paid_sessions": true,
"paid_sessions_hourly_rate": 10000,
"paid_sessions_daily_rate": 200000,
"free_sessions": true,
"cowriting": true,
"cowriting_purpose": 2,
"subscribe_email": true,
"is_a_teacher": false,
"is_a_student": false,
"online_presences": [
{ "id": "e1962204-f652-41b0-84d6-1afd7e9172be", "service_type": "soundcloud", "username": "testuser" },
{ "id": "005a7c78-db8b-4f72-a51f-d64d579c22b0", "service_type": "reverbnation", "username": "testuser" },
{ "id": "2dd22eef-03ba-4743-b65b-5a194591dc86", "service_type": "bandcamp", "username": "testuser" },
{ "id": "d6ae62b4-e1ce-4cf0-90b7-c64033533261", "service_type": "fandalism", "username": "testuser" },
{ "id": "c6e85453-0fa9-40d0-9754-8f372d6e0ed3", "service_type": "youtube", "username": "testuser" },
{ "id": "480ec1ad-ea1d-4990-9c68-d7f9c0174441", "service_type": "facebook", "username": "testuser" },
{ "id": "232b26d5-c75a-4d65-9013-a07b73c8a7ae", "service_type": "twitter", "username": "testuser" }
],
"performance_samples": [],
"genres": [
{ "genre_id": "asian", "player_type": "JamRuby::User", "genre_type": "profile" },
{ "genre_id": "classical", "player_type": "JamRuby::User", "genre_type": "profile" },
{ "genre_id": "african", "player_type": "JamRuby::User", "genre_type": "virtual_band" },
{ "genre_id": "classical", "player_type": "JamRuby::User", "genre_type": "virtual_band" },
{ "genre_id": "classical", "player_type": "JamRuby::User", "genre_type": "traditional_band" },
{ "genre_id": "blues", "player_type": "JamRuby::User", "genre_type": "free_sessions" },
{ "genre_id": "soft rock", "player_type": "JamRuby::User", "genre_type": "free_sessions" },
{ "genre_id": "celtic", "player_type": "JamRuby::User", "genre_type": "cowriting" },
{ "genre_id": "tv & movie soundtrack", "player_type": "JamRuby::User", "genre_type": "cowriting" }
],
"bands": [],
"instruments": [
{ "description": "Acoustic Guitar", "proficiency_level": 3, "priority": 1, "instrument_id": "acoustic guitar" },
{ "description": "Keyboard", "proficiency_level": 3, "priority": 8, "instrument_id": "keyboard" },
{ "description": "Ukulele", "proficiency_level": 3, "priority": 11, "instrument_id": "ukulele" },
{ "description": "Voice", "proficiency_level": 3, "priority": 13, "instrument_id": "voice" },
{ "description": "Piano", "proficiency_level": 2, "priority": 10, "instrument_id": "piano" }
],
"is_friend": false,
"is_following": false,
"is_liking": false,
"pending_friend_request": false,
"my_audio_latency": 5,
"internet_score": null
}

View File

@ -1,35 +1,107 @@
/// <reference types="cypress" />
import { iteratee } from "lodash"
describe("Friends Index page", () => {
describe('For unauthenticated user', () => {
describe('Friends page', () => {
beforeEach(() => {
cy.stubUnauthenticate()
cy.visit('/friends')
})
cy.stubAuthenticate();
cy.intercept('POST', /\S+\/filter/, { fixture: 'people' });
});
it("should not list musicians", () => {
cy.contains("Find New Friends").should('exist')
cy.contains("Update Search").should('exist')
cy.contains("Reset Filters").should('exist')
cy.get('[data-testid=peopleListTable]').should('not.exist')
})
})
describe("For authenticated user", () => {
describe('friends list', () => {
beforeEach(() => {
cy.stubAuthenticate()
cy.intercept('GET', '/filter', { fixture: 'people' })
cy.visit('/friends')
cy.visit('/friends');
});
it('lists musicians', () => {
cy.contains('Find New Friends').should('exist');
cy.contains('Update Search').should('exist');
cy.contains('Reset Filters').should('exist');
cy.get('[data-testid=peopleListTable] > tbody tr')
.should('have.length', 20)
.first()
.contains('Test User1');
});
//TODO: paginate
});
describe('details side panel', () => {
beforeEach(() => {
cy.intercept('GET', /\S+\/profile\S+/, { fixture: 'person' });
cy.visit('/friends');
});
it('shows profile side panel', () => {
//open side panel by clicking name
cy.contains('Test User1').click();
cy.get('[data-testid=profileSidePanel]')
.should('be.visible')
.contains('Biography of Test User1');
closeMoreDetailsSidePanel()
//open side panel by clicking more button
cy.get('[data-testid=peopleListTable] > tbody tr').first().find('[data-testid=btnMore]').click()
cy.get('[data-testid=profileSidePanel]')
.should('be.visible')
.contains('Biography of Test User1');
closeMoreDetailsSidePanel()
//open side panel by clicking more link
cy.get('[data-testid=peopleListTable] > tbody tr').first().find('[data-testid=linkMore]').click()
cy.get('[data-testid=profileSidePanel]')
.should('be.visible')
.contains('Biography of Test User1');
closeMoreDetailsSidePanel()
});
});
describe('making friendship', () => {
it('add friend', () => {
cy.intercept('GET', /\S+\/profile\S+/, { fixture: 'person' });
cy.intercept('POST', /\S+\/friend_requests/, { statusCode: 201, body: { ok: true } });
cy.visit('/friends');
cy.contains('Test User1').click();
cy.get('[data-testid=profileSidePanel]')
.find('[data-testid=connect]')
.should('not.be.disabled')
.click();
cy.get('[data-testid=profileSidePanel]')
.find('[data-testid=connect]')
.should('be.disabled');
});
it('remove friend', () => {
cy.intercept('GET', /\S+\/profile\S+/, { fixture: 'friend' });
cy.intercept('DELETE', /\S+\/friends\S+/, { statusCode: 204, body: { ok: true } });
cy.visit('/friends');
cy.contains('Test User1').click();
cy.get('[data-testid=profileSidePanel]')
.find('[data-testid=disconnect]')
.should('exist')
.should('not.be.disabled')
.click();
cy.get('[data-testid=profileSidePanel]')
.find('[data-testid=disconnect]')
.should('not.exist');
cy.get('[data-testid=profileSidePanel]')
.find('[data-testid=connect]')
.should('be.exist')
.should('not.be.disabled')
});
})
it("should list musicians", () => {
cy.get('[data-testid=peopleListTable]').should('exist')
})
})
describe('send message', () => {
})
});
function closeMoreDetailsSidePanel(){
cy.get('[data-testid=profileSidePanel] .modal-header button.close').click()
}

View File

@ -32,9 +32,9 @@ describe("Top Navigation", () => {
it("shows user dropdown", () => {
showSubscribeToUpdates()
cy.get('[data-testid=navbarTopProfileDropdown]').should('exist')
cy.contains("Peter Pan")
cy.contains("My Profile")
cy.contains("Sign out")
cy.contains("Peter Pan").should('exist')
cy.contains("My Profile").should('exist')
cy.contains("Sign out").should('exist')
})
})

View File

@ -18,3 +18,6 @@ import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -5906,6 +5906,15 @@
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
},
"dom7": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/dom7/-/dom7-3.0.0.tgz",
"integrity": "sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==",
"dev": true,
"requires": {
"ssr-window": "^3.0.0-alpha.1"
}
},
"domain-browser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
@ -16813,6 +16822,12 @@
"tweetnacl": "~0.14.0"
}
},
"ssr-window": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ssr-window/-/ssr-window-3.0.0.tgz",
"integrity": "sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==",
"dev": true
},
"ssri": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-7.1.1.tgz",
@ -17293,6 +17308,16 @@
"util.promisify": "~1.0.0"
}
},
"swiper": {
"version": "6.8.2",
"resolved": "https://registry.npmjs.org/swiper/-/swiper-6.8.2.tgz",
"integrity": "sha512-VwBZ40NQ8vDzIZO9wApJTf4bDu/o3sgURMQ6fvJVSc9T63NoJV0KLC/mjkrl9GepGbmlCQNLR2tL0Kk/r8NSdw==",
"dev": true,
"requires": {
"dom7": "^3.0.0",
"ssr-window": "^3.0.0"
}
},
"symbol-observable": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",

View File

@ -95,6 +95,7 @@
"gulp-rtlcss": "^1.4.1",
"gulp-sass": "^4.1.0",
"gulp-sourcemaps": "^2.6.5",
"prettier": "1.17.1"
"prettier": "1.17.1",
"swiper": "^6.8.2"
}
}

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -11,7 +11,6 @@
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700|Poppins:100,200,300,400,500,600,700,800,900&display=swap"
/>
<title>JamKazam</title>
</head>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -3,3 +3,10 @@
// user-variables.scss
//
// Place your own variable overrides here, these will override any Bootstrap and theme variables.
$jk-navigation-link-color: #2c7be5 !default;
$jk-navigation-text-color: #313336 !default;
$dropdown-link-color: rgba($jk-navigation-link-color, 1) !default;
$link-color: rgba($jk-navigation-link-color, 1) !default;
$navbar-light-color: rgba($jk-navigation-link-color, 1) !default;

View File

@ -4,4 +4,6 @@
//
// Place your own theme CSS or SCSS rules below this line, these rules will override any Bootstrap and theme variables.
@import './custom/user.css';
@import './custom/nav';
@import './custom/user';
@import './custom/form';

View File

@ -0,0 +1,18 @@
.form-check {
display: block;
min-height: 1.5rem;
padding-left: 1.5em;
margin-bottom: 1rem !important;
}
/* -------------------------------------------------------------------------- */
/* Choices */
/* -------------------------------------------------------------------------- */
.choices {
position:relative;
margin-bottom:24px;
font-size:16px
}

View File

@ -0,0 +1,12 @@
.navbar-light{
.navbar-text {
color: $jk-navigation-text-color;
a {
color: $jk-navigation-text-color;
@include hover-focus() {
color: $navbar-light-active-color;
}
}
}
}

View File

@ -4,6 +4,7 @@
--jk-good: #198754;
--jk-fair: #e0a500;
--jk-high: #990000;
--jk-unknown: #8a8787;
}
.nav-active .nav-link-text{
@ -31,7 +32,13 @@
.latency-high {
background-color: var(--jk-high);
color: white;
color: rgb(247, 239, 239);
min-width: 50px;
}
.latency-unknown {
background-color: var(--jk-unknown);
color: rgb(253, 251, 251);
min-width: 50px;
}
@ -64,3 +71,35 @@
top: 0;
z-index: 2000;
}
.swiper-container {
width: 100%;
height: 100%;
}
.swiper-slide {
// text-align: center;
// font-size: 18px;
// background: #fff;
// /* Center slide text vertically */
// display: -webkit-box;
// display: -ms-flexbox;
// display: -webkit-flex;
// display: flex;
// -webkit-box-pack: center;
// -ms-flex-pack: center;
// -webkit-justify-content: center;
// justify-content: center;
// -webkit-box-align: center;
// -ms-flex-align: center;
// -webkit-align-items: center;
// align-items: center;
}
.swiper-slide img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}

View File

@ -4,7 +4,7 @@
margin: 0;
margin-left: auto;
margin-right: initial;
max-width: 350px;
max-width: 600px;
}
// .modal-content {
// border-radius: 0;
@ -27,7 +27,8 @@
border: none;
border-radius: 0;
padding: 0.5rem 1.25rem;
background-image: linear-gradient(-45deg, #4695ff, #1970e2);
border-bottom: solid 1px #ddd;
//background-image: linear-gradient(-45deg, #4695ff, #1970e2);
overflow: hidden;
&:before,
&:after {
@ -36,7 +37,7 @@
border-radius: 50%;
height: 12.5rem;
width: 12.5rem;
background-image: linear-gradient(45deg, #318aff, #247cef);
//background-image: linear-gradient(45deg, #318aff, #247cef);
}
&:after {
left: 5.125rem;
@ -51,7 +52,7 @@
position: absolute;
top: 0;
right: 0;
color: white;
color: #333;
opacity: 0.75;
padding-top: 0.75rem;
margin-top: 0;

View File

@ -6,9 +6,9 @@ const JKCurrentUserAvatar = () => {
const { currentUser } = useAuth();
if(currentUser && currentUser.photo_url) {
return ( <img className="avatar avatar-xl rounded-circle" src={currentUser.photo_url} /> );
return ( <img className="avatar avatar-xl rounded-circle mr-1" src={currentUser.photo_url} /> );
}else {
return ( <img className="avatar avatar-xl rounded-circle" src={avatar} /> );
return ( <img className="avatar avatar-xl rounded-circle mr-1" src={avatar} /> );
}
}

View File

@ -6,40 +6,43 @@ import { Link } from 'react-router-dom';
import JKCurrentUserAvatar from './JKCurrentUserAvatar'
const JKNavbarTopCurrentUser = () => {
const { currentUser, setCurrentUser } = useAuth();
const { currentUser } = useAuth();
const [dropdownOpen, setDropdownOpen] = useState(false);
const toggle = () => setDropdownOpen(prevState => !prevState);
const fetchCurrentUser = () => {
getCurrentUser()
.then(resp => {
if (resp.ok) {
return resp.json();
}
})
.then(data => {
console.log('CURRENT_USER', data);
setCurrentUser(data);
})
.catch(error => console.log(error));
};
// const fetchCurrentUser = () => {
// getCurrentUser()
// .then(resp => {
// if (resp.ok) {
// return resp.json();
// }
// })
// .then(data => {
// console.log('CURRENT_USER', data);
// setCurrentUser(data);
// })
// .catch(error => console.log(error));
// };
const handleLogout = () => {};
useEffect(() => {
fetchCurrentUser();
}, []);
// useEffect(() => {
// fetchCurrentUser();
// }, []);
return (
<div>
{currentUser &&
<Dropdown isOpen={dropdownOpen} toggle={toggle} data-testid="navbarTopProfileDropdown">
<DropdownToggle nav={true} caret>
<DropdownToggle nav={true}>
<JKCurrentUserAvatar />
<span className="d-none d-lg-inline navbar-text">
{currentUser.name}
</span>
</DropdownToggle>
<DropdownMenu>
<DropdownMenu right={true}>
<DropdownItem tag={Link} to="/pages/settings">
My Profile
</DropdownItem>

View File

@ -19,7 +19,7 @@ const Logo = ({ at, width, className, ...rest }) => {
className={classNames(
{
'align-items-center py-3': at === 'navbar-vertical',
'align-items-center p-2': at === 'navbar-vertical',
'align-items-center': at === 'navbar-top',
'flex-center font-weight-extra-bold fs-5 mb-4': at === 'auth'
},

View File

@ -3,11 +3,11 @@ import { Collapse, Navbar, NavItem, Nav } from 'reactstrap';
import classNames from 'classnames';
import AppContext from '../../context/Context';
import Logo from './Logo';
import SearchBox from './SearchBox';
//import SearchBox from './SearchBox';
import TopNavRightSideNavItem from './TopNavRightSideNavItem';
import NavbarTopDropDownMenus from './NavbarTopDropDownMenus';
//import NavbarTopDropDownMenus from './NavbarTopDropDownMenus';
import { navbarBreakPoint, topNavbarBreakpoint } from '../../config';
import autoCompleteInitialItem from '../../data/autocomplete/autocomplete';
//import autoCompleteInitialItem from '../../data/autocomplete/autocomplete';
const NavbarTop = () => {
const {
@ -26,7 +26,7 @@ const NavbarTop = () => {
return (
<Navbar
light
className="navbar-glass fs--1 font-weight-semi-bold row navbar-top sticky-kit"
className="navbar-glass fs--1 font-weight-semi-bold row navbar-top sticky-kit mb-3 d-flex"
expand={isTopNav && topNavbarBreakpoint}
>
<div

View File

@ -87,7 +87,7 @@ const NavbarVertical = ({ navbarStyle }) => {
}
}
>
<Nav navbar vertical>
<Nav navbar vertical className="mt-3">
<NavbarVerticalMenu routes={routes} />
</Nav>
<div className="settings px-3 px-xl-0">
@ -101,7 +101,7 @@ const NavbarVertical = ({ navbarStyle }) => {
</Nav>
</div>
)}
<div className="navbar-vertical-divider">
{/* <div className="navbar-vertical-divider">
<hr className="navbar-vertical-hr my-2" />
</div>
<Button
@ -114,7 +114,7 @@ const NavbarVertical = ({ navbarStyle }) => {
className="my-3 btn-purchase"
>
Purchase
</Button>
</Button> */}
</div>
</Collapse>
</Navbar>

View File

@ -11,7 +11,7 @@ const NavbarVerticalMenuItem = ({ route }) => (
<FontAwesomeIcon icon={route.icon} />
</span>
)}
<span className="nav-link-text">{route.name}</span>
<span className="nav-link-text pl-1">{route.name}</span>
{!!route.badge && (
<Badge color={route.badge.color || 'soft-success'} pill className="ml-2">
{route.badge.text}

View File

@ -1,9 +1,7 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
//import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { useState, useEffect } from 'react';
import { Link, useHistory } from 'react-router-dom';
import { DropdownItem, DropdownMenu, DropdownToggle, Dropdown } from 'reactstrap';
import team3 from '../../assets/img/team/3.jpg';
import Avatar from '../common/Avatar';
import { useAuth } from '../../context/AuthContext';
const ProfileDropdown = () => {
@ -19,7 +17,7 @@ const ProfileDropdown = () => {
// removeCookie("remember_token", {
// domain: ".jamkazam.local"
// });
history.push('/authentication/basic/logout');
// history.push('/authentication/basic/logout');
console.log("signout...");
}

View File

@ -1,5 +1,5 @@
import React, { useContext } from 'react';
import { Nav, NavItem, NavLink, UncontrolledTooltip } from 'reactstrap';
import { Navbar, Nav, NavItem, NavLink, NavbarText } from 'reactstrap';
// import ProfileDropdown from './ProfileDropdown';
// import NotificationDropdown from './NotificationDropdown';
// import SettingsAnimatedIcon from './SettingsAnimatedIcon';
@ -14,32 +14,17 @@ import JKNavbarTopProfile from './JKNavbarTopProfile';
const TopNavRightSideNavItem = () => {
const { isTopNav, isCombo } = useContext(AppContext);
return (
<Nav navbar className="navbar-nav-icons ml-auto flex-row align-items-center">
{/* <NavItem>
<SettingsAnimatedIcon />
</NavItem> */}
{/* {(isCombo || isTopNav) && (
<NavItem className={classNames(`p-2 px-lg-0 cursor-pointer`, { [`d-${navbarBreakPoint}-none`]: isCombo })}>
<NavLink tag={Link} to="/changelog" id="changelog">
<FontAwesomeIcon icon="code-branch" transform="right-6 grow-4" />
</NavLink>
<UncontrolledTooltip autohide={false} placement="left" target="changelog">
Changelog
</UncontrolledTooltip>
<Navbar expand="md" className="ml-auto">
<Nav className="align-items-center" navbar>
<NavbarText className="d-none d-md-inline">Keep JamKazam Improving:</NavbarText>
<NavItem className="d-none d-md-inline mr-5">
<NavLink>Subscribe</NavLink>
</NavItem>
)} */}
{/* <CartNotification /> */}
{/* <NotificationDropdown /> */}
<NavItem className="me-4 d-none d-md-inline">
<div className="navbar-text d-inline">Keep JamKazam Improving:</div>
<NavLink className="text-green d-inline navbar-text nav-link">Subscribe</NavLink>
</NavItem>
<NavItem className="nav-item me-2">
<NavItem>
<JKNavbarTopProfile />
</NavItem>
</Nav>
</Navbar>
);
};

View File

@ -1,6 +1,6 @@
import React, {useState, useEffect} from 'react';
import React, { useState, useEffect, Fragment } from 'react';
import PropTypes from 'prop-types';
import { Alert, Card, CardBody, Col, Row, Button } from 'reactstrap';
import { Alert, Card, CardBody, Col, Row, Button, Pagination, PaginationItem, PaginationLink, Form } from 'reactstrap';
import Loader from '../common/Loader';
import FalconCardHeader from '../common/FalconCardHeader';
import { isIterableArray } from '../../helpers/utils';
@ -8,9 +8,10 @@ import { isIterableArray } from '../../helpers/utils';
// import rawPeople from '../../data/people/people';
// import peopleCategories from '../../data/people/peopleCategories';
// import apiFetch from '../../helpers/apiFetch';
import JKPeopleSearch from "./JKPeopleSearch";
import JKPeopleSearch from './JKPeopleSearch';
import JKPeopleList from './JKPeopleList';
import { getPeople } from "../../helpers/rest";
import { getMusicians, getPeople } from '../../helpers/rest';
import JKPeopleSwiper from './JKPeopleSwiper';
const JKPeople = ({ className }) => {
//const { loading, data: people, setData: setPeople } = useFakeFetch(rawPeople);
@ -18,63 +19,95 @@ const JKPeople = ({ className }) => {
const [people, setPeople] = useState([]);
const [loading, setLoading] = useState(true);
const [showSearch, setShowSearch] = useState(false);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(0);
const fetchPeople = React.useCallback( () => {
getPeople()
const fetchPeople = React.useCallback(page => {
//getMusicians(page)
console.log("PAGE", page);
getPeople({ page: page })
.then(response => {
if (!response.ok) {
//TODO: handle failure
console.log(response);
//console.log(response);
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log('people received', data);
setPeople(data.musicians);
console.log('PEOPLE', data.musicians);
//const users = new Set([...people, ...data.musicians]);
//console.log("new users", users);
//setPeople(Array.from(users));
setPeople(prev => Array.from(new Set([...prev, ...data.musicians])))
setTotalPages(data.page_count);
})
.catch(error => {
//TODO: handle error
console.log(error);
}).finally(() => {
setLoading(false)
})
.finally(() => {
setLoading(false);
});
}, [])
}, []);
useEffect(() => {
fetchPeople();
}, [fetchPeople])
fetchPeople(page);
}, [page]);
const searchPeople = ({ target }) => {
const keyword = target.value.toLowerCase();
const filteredResult = people.filter(
person => person.name.toLowerCase().includes(keyword) || person.institution.toLowerCase().includes(keyword)
);
setPeople(keyword.length ? filteredResult : people);
const goNextPage = () => {
if (page < totalPages) {
setPage(val => ++val);
}
};
const goPrevPage = () => {
if (page > 1) {
setPage(prev => --prev);
}
};
// const searchPeople = ({ target }) => {
// const keyword = target.value.toLowerCase();
// const filteredResult = people.filter(
// person => person.name.toLowerCase().includes(keyword) || person.institution.toLowerCase().includes(keyword)
// );
// setPeople(keyword.length ? filteredResult : people);
// };
return (
<Card>
<FalconCardHeader title="Find New Friends">
<div className="col-12 col-sm-auto">
<Button color="primary" className="me-2 fs--1" onClick={() => setShowSearch(!showSearch)}>Update Search</Button>
<Button outline disabled color="secondary" className="fs--1">Reset Filters</Button>
</div>
</FalconCardHeader>
<JKPeopleSearch show={showSearch} setShow={setShowSearch} setPeople={setPeople} />
<FalconCardHeader title="Find New Friends" titleClass="font-weight-bold">
<Form inline className="mt-md-0 mt-3">
<Button color="primary" className="me-2 mr-2 fs--1" onClick={() => setShowSearch(!showSearch)}>
Update Search
</Button>
<Button outline disabled color="secondary" className="fs--1">
Reset Filters
</Button>
</Form>
</FalconCardHeader>
<CardBody className="pt-0">
{loading ? (
<Loader />
) : isIterableArray(people) ? (
//Start Find Friends table hidden on small screens
<Fragment>
<Row className="mb-3 justify-content-between d-none d-md-block">
<div className="table-responsive-xl px-2">
<JKPeopleList people={people} goNextPage={goNextPage} page={page} totalPages={totalPages} />
</div>
</Row>
<JKPeopleList people={people} />
<Row className="swiper-container d-block d-md-none">
<JKPeopleSwiper people={people} goNextPage={goNextPage} />
</Row>
</Fragment>
) : (
<Row className="p-card">
<Col>
@ -85,8 +118,9 @@ const JKPeople = ({ className }) => {
</Row>
)}
</CardBody>
</Card>);
}
</Card>
);
};
JKPeople.propTypes = {
className: PropTypes.string

View File

@ -1,35 +1,47 @@
import React from "react";
import { Table } from 'reactstrap';
import React from 'react';
import { Table, Row, Col, Button } from 'reactstrap';
import JKPerson from './JKPerson';
import PropTypes from "prop-types";
import PropTypes from 'prop-types';
const JKPeopleList = ({people}) => {
const JKPeopleList = ({ people, goNextPage, page, totalPages }) => {
return (
<Table className="table-bordered table-striped fs--1" data-testid="peopleListTable">
<>
<Table striped bordered className="fs--1" data-testid="peopleListTable">
<thead className="bg-200 text-900">
<tr>
<th scope="col">Name</th>
<th scope="col" style={{ minWidth: 250 }}>About</th>
<th scope="col" style={{ minWidth: 250 }}>
About
</th>
<th scope="col">Instruments</th>
<th scope="col">Genres</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody className="list">
{people.map((person, index) => (
<tr className="align-middle" key={person.id}>
<JKPerson {...person} />
<JKPerson person={person} viewMode="list" />
</tr>
))}
</tbody>
</Table>
<Row>
<Col>
{page < totalPages && (
<a className="ml-2 fw-semi-bold" href="#!" onClick={goNextPage}>
More...
</a>
)}
</Col>
</Row>
</>
);
}
};
JKPeopleList.propTypes = {
people: PropTypes.arrayOf(
PropTypes.instanceOf(Object)
)
}
people: PropTypes.arrayOf(PropTypes.instanceOf(Object))
};
export default JKPeopleList;

View File

@ -1,9 +1,9 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect, useRef, Fragment } from 'react';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
import Select from 'react-select';
import JKTooltip from '../common/JKTooltip';
import PropTypes from 'prop-types';
import { getGenres, getInstruments, postPeopleSearch } from '../../helpers/rest';
import { getGenres, getInstruments, getPeople } from '../../helpers/rest';
import { useForm, Controller } from 'react-hook-form';
const JKPeopleSearch = props => {
@ -54,7 +54,7 @@ const JKPeopleSearch = props => {
}
})
.then(data => {
console.log(data);
//console.log(data);
setGenres(
data.map(genre => {
return {
@ -88,7 +88,7 @@ const JKPeopleSearch = props => {
}
const updatedData = {...data, genres}
console.log('submitting...', updatedData);
await postPeopleSearch(updatedData)
await getPeople(updatedData)
.then(response => {
if(!response.ok){
//TODO: handle failure
@ -121,8 +121,8 @@ const JKPeopleSearch = props => {
];
return (
<div>
<Modal isOpen={show} toggle={toggle} size="xl">
<Fragment>
<Modal isOpen={show} toggle={toggle} className="mw-100 mx-1 mr-1 ml-1 mx-md-5 mr-md-5 ml-md-5 mx-xl-10 mr-xl-10 ml-xl-10">
<ModalHeader toggle={toggle}>Update Search</ModalHeader>
<ModalBody>
<div className="px-4 pb-4">
@ -212,7 +212,7 @@ const JKPeopleSearch = props => {
Instruments{' '}
<JKTooltip title="Select one or more instruments to filter for. If this field is blank, all instruments will be searched for." />
</label>
<div>
<div className="choices">
<Controller
name="instruments"
control={control}
@ -225,7 +225,7 @@ const JKPeopleSearch = props => {
Genres{' '}
<JKTooltip title="Select one or more genres to filter for. If this field is blank, all genres will be included." />
</label>
<div>
<div className="choices">
<Controller
name="genres"
control={control}
@ -237,7 +237,7 @@ const JKPeopleSearch = props => {
<label className="form-label" htmlFor="lastActive">
Last Active <JKTooltip title="Select onefor when the user was last active on JamKazam." />
</label>
<div>
<div className="choices">
<Controller
name="last_active"
control={control}
@ -249,7 +249,7 @@ const JKPeopleSearch = props => {
<label className="form-label" htmlFor="joined">
Joined JamKazam <JKTooltip title="Select onefor when the user joined JamKazam." />
</label>
<div>
<div className="choices">
<Controller
name="joined"
control={control}
@ -264,7 +264,7 @@ const JKPeopleSearch = props => {
</div>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={toggle}>
<Button color="outline-primary" onClick={toggle}>
Cancel
</Button>{' '}
<Button color="primary" onClick={submitForm}>
@ -272,7 +272,7 @@ const JKPeopleSearch = props => {
</Button>
</ModalFooter>
</Modal>
</div>
</Fragment>
);
};

View File

@ -0,0 +1,74 @@
import React from 'react';
import PropTypes from 'prop-types';
// import Swiper core and required modules
import SwiperCore, { Navigation, Pagination, Scrollbar, A11y } from 'swiper';
// Import Swiper React components
import { Swiper, SwiperSlide } from 'swiper/react';
// Import Swiper styles
import 'swiper/swiper.scss';
import 'swiper/components/navigation/navigation.scss';
import 'swiper/components/pagination/pagination.scss';
import 'swiper/components/scrollbar/scrollbar.scss';
import { Card, CardBody, CardHeader } from 'reactstrap';
import JKPerson from './JKPerson';
import JKProfileAvatar from '../profile/JKProfileAvatar';
SwiperCore.use([Navigation, Pagination, Scrollbar, A11y]);
const JKPeopleSwiper = ({ people, goNextPage }) => {
return (
<>
<Swiper
spaceBetween={0}
slidesPerView={1}
onSlideChange={() => console.log('slide change')}
onSlideNextTransitionEnd={swiper => {
if(swiper.isEnd){
goNextPage()
}
}}
pagination={{
clickable: true,
type: 'custom'
}}
navigation={{
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev'
}}
>
{people.map((person, index) => (
<SwiperSlide key={person.id}>
<Card>
<CardHeader className="bg-200">
<div className="avatar avatar-xl d-inline-block me-2 mr-2">
<JKProfileAvatar url={person.photo_url} size="xl"/>
</div>
<h5 className="d-inline-block align-top mt-1">{person.name}</h5>
</CardHeader>
<CardBody>
<JKPerson person={person} viewMode="swipe" />
</CardBody>
</Card>
</SwiperSlide>
))}
</Swiper>
<div className="py-4 px-6 bg-white border-top w-100 fixed-bottom">
<div className="swiper-pagination" />
<div className="swiper-button-prev" />
<div className="swiper-button-next" />
</div>
</>
);
};
JKPeopleSwiper.propTypes = {
people: PropTypes.arrayOf(PropTypes.instanceOf(Object))
};
export default JKPeopleSwiper;

View File

@ -1,56 +1,60 @@
import React, { Fragment, useState } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { Row, Col } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import avatar from '../../assets/img/team/avatar.png';
import JKProfileSidePanel from '../profile/JKProfileSidePanel';
import JKProfileAvatar from '../profile/JKProfileAvatar';
import JKProfileInstrumentsList from '../profile/JKProfileInstrumentsList';
import {getUserProfile} from '../../helpers/rest';
import { getPersonById } from '../../helpers/rest';
import JKConnectButton from '../profile/JKConnectButton';
import JKMessageButton from '../profile/JKMessageButton';
import JKLatencyBadge from '../profile/JKLatencyBadge';
import { useAuth } from '../../context/AuthContext';
const JKPerson = ({ id, name, biography, photo_url, instruments }) => {
const JKPerson = props => {
const { id, name, biography, photo_url, instruments, latency_data } = props.person;
const viewMode = props.viewMode;
const { currentUser } = useAuth();
const [showSidePanel, setShowSidePanel] = useState(false)
const [showSidePanel, setShowSidePanel] = useState(false);
const [user, setUser] = useState(null);
const fetchPerson = () => {
console.log("fetchPerson called");
getUserProfile(id)
const fetchPerson = async () => {
//console.log("fetchPerson called");
await getPersonById(id)
.then(response => {
if (response.ok) {
return response.json()
return response.json();
} else {
}
})
.then(json => {
console.log("USER", json);
setUser(json)
console.log('USER', json);
setUser(json);
})
.catch(error => console.log(error))
}
.catch(error => console.log(error));
};
const toggleMoreDetails = () => {
setShowSidePanel(prev => !prev)
if(!user){
fetchPerson()
}
}
fetchPerson();
setShowSidePanel(prev => !prev);
};
return (
<Fragment>
<>
{viewMode === 'list' ? (
<>
<td className="text-nowrap">
<a onClick={toggleMoreDetails} className="d-flex align-items-center mb-1 fs-0">
<div className="avatar avatar-xl">
<JKProfileAvatar url={photo_url} />
</div>
<div className="ms-2">
<div className="ml-2 ms-2">
<strong>{name}</strong>
</div>
</a>
<div>
<strong>Latency To Me:</strong> 24ms <span className="badge latency-good">GOOD</span>
<strong>Latency To Me:</strong>
<JKLatencyBadge latencyData={latency_data} />
</div>
<div>
<strong>Last Active:</strong> 1 hour
@ -59,53 +63,72 @@ const JKPerson = ({ id, name, biography, photo_url, instruments }) => {
<td>
{biography}
{biography.length > 0 && (
<a onClick={toggleMoreDetails}>
<a data-testid="linkMore" onClick={toggleMoreDetails}>
{' '} more »
</a>
)}
</td>
<td>
</td>
<td>
<JKProfileInstrumentsList instruments={instruments} />
</td>
<td><JKProfileInstrumentsList instruments={instruments} /></td>
<td className="text-nowrap">
<a
href="#"
className="btn fs--1 btn-primary px-2 py-1 mr-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Connect with This Friend"
>
<FontAwesomeIcon icon="plus" transform="shrink-4 down-1" className="mr-1" />
</a>
<JKConnectButton
currentUser={currentUser}
user={props.person}
addContent={<FontAwesomeIcon icon="plus" transform="shrink-4 down-1" className="mr-1" />}
removeContent={<FontAwesomeIcon icon="minus" transform="shrink-4 down-1" className="mr-1" />}
cssClasses="fs--1 px-2 py-1 mr-1"
/>
<a
href="#"
className="btn btn-primary fs--1 px-2 py-1 mr-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Send a Message"
>
<JKMessageButton currentUser={currentUser} user={props.person} cssClasses="fs--1 px-2 py-1 mr-1">
<FontAwesomeIcon icon="comments" transform="shrink-4 down-1" className="mr-1" />
</a>
</JKMessageButton>
<a onClick={toggleMoreDetails}>
<a onClick={toggleMoreDetails} data-testid="btnMore">
<span className="btn btn-primary fs--1 px-2 py-1" data-bs-toggle="tooltip" title="View Profile">
<FontAwesomeIcon icon="user" transform="shrink-4 down-1" className="mr-1" />
</span>
</a>
</td>
</>
) : (
<>
<div>
<strong>Latency To Me:</strong> <JKLatencyBadge latencyData={latency_data} />
</div>
<div>
<strong>Last Active:</strong> 1 hour
</div>
<h5>Instruments</h5>
<JKProfileInstrumentsList instruments={instruments} />
<JKConnectButton
currentUser={currentUser}
user={props.person}
addContent={<FontAwesomeIcon icon="plus" transform="shrink-4 down-1" className="mr-1" />}
removeContent={<FontAwesomeIcon icon="minus" transform="shrink-4 down-1" className="mr-1" />}
cssClasses="fs--1 px-2 py-1 mr-1"
/>
<JKMessageButton currentUser={currentUser} user={props.person} cssClasses="fs--1 px-2 py-1 mr-1">
<FontAwesomeIcon icon="comments" transform="shrink-4 down-1" className="mr-1" />
</JKMessageButton>
<a onClick={toggleMoreDetails} data-testid="btnMore">
<span className="btn btn-primary fs--1 px-2 py-1" data-bs-toggle="tooltip" title="View Profile">
<FontAwesomeIcon icon="user" transform="shrink-4 down-1" className="mr-1" />
</span>
</a>
</>
)}
<JKProfileSidePanel user={user} show={showSidePanel} setShow={setShowSidePanel} />
</Fragment>
)
}
</>
);
};
JKPerson.propTypes = {
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
biography: PropTypes.string.isRequired,
photo_url: PropTypes.string,
person: PropTypes.object.isRequired,
viewMode: PropTypes.string
//instruments: PropTypes.arrayOf(PropTypes.string)
};

View File

@ -0,0 +1,62 @@
import React, {useEffect, useState} from 'react';
import {addFriend as connect, removeFriend as disconnect} from '../../helpers/rest';
const JKConnectButton = (props) => {
const { user, currentUser, addContent, removeContent, cssClasses } = props
const [isFriend, setIsFriend] = useState(false)
const [pendingFriendRequest, setPendingFriendRequest] = useState(false)
useEffect(() => {
setIsFriend(user.is_friend);
setPendingFriendRequest(user.pending_friend_request)
}, [user])
const addFriend = () => {
connect(currentUser.id, user.id)
.then(resp => {
if(resp.ok && resp.status === 201){
setPendingFriendRequest(true)
}
})
.catch(err => console.log(err))
}
const removeFriend = () => {
disconnect(currentUser.id, user.id)
.then(resp => {
if(resp.ok){
setIsFriend(false)
}
})
.catch(err => console.log(err))
}
const buttonTitle = () => {
let title;
if (pendingFriendRequest) {
title = 'You have sent a friend request to this user';
} else if (!isFriend) {
title = 'Send friend request';
} else if (isFriend) {
title = 'Unfriend this person';
}
return title;
};
return (
<>
{ !isFriend ? (
<button className={`btn btn-primary ${cssClasses}`} data-testid="connect" disabled={pendingFriendRequest} onClick={addFriend} title={buttonTitle()}>
{addContent}
</button>)
: (
<button className={`btn btn-primary ${cssClasses}`} data-testid="disconnect" onClick={removeFriend} title={buttonTitle()}>
{removeContent}
</button>
)
}
</>
);
};
export default JKConnectButton;

View File

@ -0,0 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
const JKLatencyBadge = ({ latencyData, showAll }) => {
let label = 'UNKNOWN';
let latency = '';
if (latencyData) {
label = latencyData.label;
if (showAll) {
latency = `${latencyData.ars_internet_latency}ms + ${latencyData.audio_latency}ms`;
} else {
latency = `${latencyData.ars_total_latency}ms`;
}
}
return (
<>
{latency} <span className={`badge latency-${label.toLowerCase()}`}>{label}</span>
</>
);
};
JKLatencyBadge.propTypes = {
latencyData: PropTypes.object,
showAll: PropTypes.bool
};
JKLatencyBadge.defaultProps = {
showAll: false
};
export default JKLatencyBadge;

View File

@ -0,0 +1,35 @@
import React, { useState, useEffect } from 'react';
import JKMessageModal from './JKMessageModal';
const JKMessageButton = props => {
const { currentUser, user, cssClasses, children } = props;
const [showModal, setShowModal] = useState(false);
const [isFriend, setIsFriend] = useState(false);
const [pendingFriendRequest, setPendingFriendRequest] = useState(false);
useEffect(() => {
setIsFriend(user.is_friend);
setPendingFriendRequest(user.pending_friend_request);
}, [user]);
const buttonTitle = () => {
return isFriend ? 'Send friend request' : 'You can message this user once you are friends.'
};
return (
<>
<JKMessageModal show={showModal} setShow={setShowModal} user={user} currentUser={currentUser} />
<button
onClick={() => setShowModal(!showModal)}
className={`btn btn-primary ${cssClasses}`}
title={buttonTitle()}
data-testid="message"
disabled={!isFriend}
>
{children}
</button>
</>
);
};
export default JKMessageButton;

View File

@ -0,0 +1,84 @@
import React, { useEffect, useState } from 'react';
import { Modal, ModalHeader, ModalBody, Row, Col, Button, ModalFooter } from 'reactstrap';
import { Scrollbar } from 'react-scrollbars-custom';
import JKProfileAvatar from './JKProfileAvatar';
import { getTextMessages, createTextMessage } from '../../helpers/rest';
const JKMessageModal = props => {
const { show, setShow, user } = props;
const [offset, setOffset] = useState(0);
const [messages, setMessages] = useState([]);
const [newMessage, setNewMessage] = useState("");
const toggle = () => setShow(!show);
const LIMIT = 20;
const fetchMessages = async () => {
await getTextMessages({
target_user_id: user.id,
offset: offset,
limit: LIMIT
})
.then(resp => {
if (resp.ok) {
return resp.json();
} else {
}
})
.then(json => {
console.log(json);
setMessages(json);
})
.catch(error => console.log(error));
}
const sendMessage = () => {
const params = { message: newMessage, target_user_id: user.id }
console.log("Sending new message", params);
createTextMessage(params)
.then(resp => console.log(resp))
.catch(error => console.log(error))
}
useEffect(() => {
if (show) {
console.log('JKMessageModal User', user.id);
fetchMessages();
}
}, [show]);
return (
<>
<Modal isOpen={show} toggle={toggle}>
<ModalHeader toggle={toggle}>Conversation with {user.name}</ModalHeader>
<ModalBody>
<Scrollbar style={{ width: '100%', height: 400 }}>
{messages.map((message, index) => (
<div className="d-flex mb-2 mr-1" key={message.id}>
<div className="avatar avatar-2xl d-inline-block me-2 mr-2">
<JKProfileAvatar url={user.photo_url} />
</div>
<div className="d-inline-block ml-2 ms-2 mb-0">
<p className="mb-0">{message.message}</p>
<time className="notification-time">{message.created_at}</time>
</div>
</div>
))}
</Scrollbar>
<Row>
<Col>
<textarea style={{ width: '100%' }} value={newMessage} onChange={(e) => setNewMessage(e.target.value) } />
</Col>
</Row>
</ModalBody>
<ModalFooter>
<Button onClick={toggle}>Close</Button>
<Button color="primary" onClick={sendMessage}>Send</Button>
</ModalFooter>
</Modal>
</>
);
};
export default JKMessageModal;

View File

@ -1,15 +1,27 @@
import React from "react";
import avatar from "../../assets/img/team/avatar.png";
const JKProfileAvatar = ({url}) => {
import React from 'react';
import PropTypes from 'prop-types';
import defaultAvatarUrl from '../../assets/img/team/avatar.png';
import Avatar from '../common/Avatar';
const JKProfileAvatar = ({ url, size }) => {
const avatarUrl = () => {
if (url) {
return ( <img className="avatar avatar-xl rounded-circle" src={url} /> );
return url;
} else {
return ( <img className="avatar avatar-xl rounded-circle" src={avatar} /> );
return defaultAvatarUrl;
}
};
}
return <Avatar src={avatarUrl()} size={size} />;
};
JKProfileAvatar.propTypes = {
url: PropTypes.string,
size: PropTypes.string
};
JKProfileAvatar.defaultProps = {
size: 'l'
};
export default JKProfileAvatar;

View File

@ -1,6 +1,6 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import PropTypes from 'prop-types';
import React, { useContext, useEffect } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import classNames from 'classnames';
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
import ScrollBarCustom from '../common/ScrollBarCustom';
@ -11,27 +11,31 @@ import JKProfileOnlinePresence from './JKProfileOnlinePresence';
import JKProfileInterests from './JKProfileInterests';
import JKProfileGenres from './JKProfileGenres';
import JKProfilePerformanceSamples from './JKProfilePerformanceSamples';
import { useAuth } from '../../context/AuthContext';
import JKConnectButton from './JKConnectButton';
import JKLatencyBadge from './JKLatencyBadge';
const JKProfileSidePanel = props => {
const { show, setShow, user } = props;
const {currentUser} = useAuth()
const toggle = () => setShow(!show);
return (
<Modal
isOpen={show}
toggle={toggle}
modalClassName="overflow-hidden modal-fixed-right modal-theme"
modalClassName="overflow-hidden modal-profile modal-fixed-right w-100 modal-theme"
className="modal-dialog-vertical"
contentClassName="vh-100 border-0"
data-testid="profileSidePanel"
>
<ModalHeader tag="div" toggle={toggle} className="modal-header-settings">
{user && (
<Fragment>
<div className="avatar avatar-2xl d-inline-block me-2">
<JKProfileAvatar url={user.photo_url} />
<div className="avatar avatar-2xl d-inline-block me-2 mr-2">
<JKProfileAvatar url={user.photo_url} size="2xl" />
</div>
<h4 className="d-inline-block align-middle mt-1">{user.name}</h4>
<h4 className="d-inline-block align-middle mt-n3 pt-0">{user.name}</h4>
</Fragment>
)}
</ModalHeader>
@ -43,12 +47,11 @@ const JKProfileSidePanel = props => {
)
}}
>
<ModalBody>
<ModalBody className="pb-5">
{user && (
<div>
<p>
<strong>Latency to Me:</strong> 18ms Internet + 8ms Audio{' '}
<span className="badge latency-good">GOOD</span>
<strong>Latency to Me:</strong> <JKLatencyBadge latencyData={user.latencyData} showAll={true} />
<br />
<strong>Location:</strong> {`${user.city}, ${user.country}`}
<br />
@ -115,14 +118,16 @@ const JKProfileSidePanel = props => {
<h5>Interests</h5>
<JKProfileInterests user={user} />
{ currentUser &&
<div className="p-3 bg-white border-top fixed-bottom">
<button className="btn btn-primary">
<span className="fas fa-plus" /> Add Friend
</button>{' '}
<button className="btn btn-outline-primary">
<JKConnectButton currentUser={currentUser} user={user} addContent={<><FontAwesomeIcon icon="plus" transform="shrink-4 down-1" className="mr-1" /> Add Friend </>} removeContent={<><FontAwesomeIcon icon="minus" transform="shrink-4 down-1" className="mr-1" /> Disconnect</>} />
{' '}
<button className="btn btn-outline-primary" data-testid="message">
<span className="fas fa-comment" /> Send Message
</button>
</div>
}
</div>
)}
</ModalBody>

View File

@ -1,15 +1,22 @@
import { reject } from "lodash";
import apiFetch from "./apiFetch";
export const getPeople = () => {
export const getMusicians = (page) => {
return new Promise((resolve, reject) => {
apiFetch("/search/musicians?results=true")
apiFetch(`/search/musicians?results=true`)
.then(response => resolve(response))
.catch(error => reject(error))
})
}
export const getUserProfile = (id) => {
// export const getPeople = (page) => {
// return new Promise((resolve, reject) => {
// apiFetch(`/filter?page=${page}`)
// .then(response => resolve(response))
// .catch(error => reject(error))
// })
// }
export const getPersonById = (id) => {
return new Promise((resolve, reject) => (
apiFetch(`/users/${id}/profile?show_teacher=true`)
.then(response => resolve(response))
@ -17,9 +24,9 @@ export const getUserProfile = (id) => {
))
}
export const postPeopleSearch = (data) => {
export const getPeople = ({ data, page } = {}) => {
return new Promise((resolve, reject) => {
apiFetch("/filter", {
apiFetch(`/filter?page=${page}`, {
method: 'POST',
body: JSON.stringify(data)
})
@ -51,3 +58,43 @@ export const getCurrentUser = () => {
.catch(error => reject(error))
})
}
export const addFriend = (userId, friendId) => {
return new Promise((resolve, reject) => {
apiFetch(`/users/${userId}/friend_requests`, {
method: 'POST',
body: JSON.stringify({ friend_id: friendId })
})
.then(response => resolve(response))
.catch(error => reject(error))
})
}
export const removeFriend = (userId, friendId) => {
return new Promise((resolve, reject) => {
apiFetch(`/users/${userId}/friends/${friendId}`, {
method: 'DELETE'
})
.then(response => resolve(response))
.catch(error => reject(error))
})
}
export const getTextMessages = (options = {}) => {
return new Promise((resolve, reject) => {
apiFetch(`/text_messages?${new URLSearchParams(options)}`)
.then(response => resolve(response))
.catch(error => reject(error))
})
}
export const createTextMessage = (options) => {
return new Promise((resolve, reject) => {
apiFetch(`/text_messages`, {
method: "POST",
body: JSON.stringify(options)
})
.then(response => resolve(response))
.catch(error => reject(error))
})
}

View File

@ -1,8 +1,10 @@
import React, { useContext, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Route, Switch, Redirect } from 'react-router-dom';
import Dashboard from '../components/dashboard/Dashboard';
import { Route, Switch, Redirect, NavLink } from 'react-router-dom';
import { Card, CardBody, Row, Col, Button } from "reactstrap";
import Logo from '../components/navbar/Logo';
import Section from '../components/common/Section';
//import Dashboard from '../components/dashboard/Dashboard';
import JKDashboard from '../components/dashboard/JkDashboard';
//import DashboardAlt from '../components/dashboard-alt/DashboardAlt';
@ -17,6 +19,7 @@ import ProductProvider from '../components/e-commerce/ProductProvider';
import { getPageName } from '../helpers/utils';
import { useAuth } from '../context/AuthContext';
import { getCurrentUser } from '../helpers/rest';
const DashboardRoutes = loadable(() => import('./DashboardRoutes'));
@ -25,6 +28,26 @@ const DashboardLayout = ({ location }) => {
const isKanban = getPageName('kanban');
const {currentUser, setCurrentUser} = useAuth()
const fetchCurrentUser = () => {
getCurrentUser()
.then(resp => {
if (resp.ok) {
return resp.json();
}
})
.then(data => {
console.log('layout CURRENT_USER', data);
setCurrentUser(data);
})
.catch(error => console.log(error));
};
useEffect(() => {
fetchCurrentUser()
}, [])
useEffect(() => {
DashboardRoutes.preload();
}, []);
@ -34,6 +57,26 @@ const DashboardLayout = ({ location }) => {
}, [location.pathname]);
return (
<>
{ currentUser == null ? (
<Section className="py-0">
<Row className="flex-center min-vh-100 py-6">
<Col sm={10} md={8} lg={6} xl={5} className="col-xxl-4">
<Logo/>
<Card>
<CardBody className="fs--1 font-weight-normal p-5">
<Row className="justify-content-center">
<h3 className="mt-3 mt-md-4 font-weight-normal fs-2">Signin to begin</h3>
<p>Please login to your jamkazam account before accessing this interface.</p>
<a className="btn btn-primary" href="https://www.jamkazam.com/signin">Signin</a>
</Row>
</CardBody>
</Card>
</Col>
</Row>
</Section>
) : (
<div className={isFluid || isKanban ? 'container-fluid' : 'container'}>
{isVertical && <NavbarVertical isKanban={isKanban} navbarStyle={navbarStyle} />}
<ProductProvider>
@ -51,6 +94,9 @@ const DashboardLayout = ({ location }) => {
{/* <SidePanelModal path={location.pathname} /> */}
</ProductProvider>
</div>
)}
</>
);
};

View File

@ -1,15 +1,16 @@
import React, { useEffect, useState } from 'react';
import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-dom';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { toast, ToastContainer } from 'react-toastify';
import { CloseButton, Fade } from '../components/common/Toast';
import DashboardLayout from './DashboardLayout';
import ErrorLayout from './ErrorLayout';
import loadable from '@loadable/component';
const AuthBasicLayout = loadable(() => import('./AuthBasicLayout'));
// import loadable from '@loadable/component';
//const AuthBasicLayout = loadable(() => import('./AuthBasicLayout'));
//const Landing = loadable(() => import('../components/landing/Landing'));
//const WizardLayout = loadable(() => import('../components/auth/wizard/WizardLayout'));
//const AuthCardRoutes = loadable(() => import('../components/auth/card/AuthCardRoutes'));
@ -17,31 +18,16 @@ const AuthBasicLayout = loadable(() => import('./AuthBasicLayout'));
const Layout = () => {
useEffect(() => {
AuthBasicLayout.preload();
//AuthBasicLayout.preload();
//Landing.preload();
//WizardLayout.preload();
//AuthCardRoutes.preload();
//AuthSplitRoutes.preload();
}, []);
// async function fetchUserData(){
// await apiFetch('/users/current_user_data')
// .then(resp => {
// if(!resp.ok){
// //handle error
// console.log("fetchUserData failed", resp);
// }else{
// return resp.json()
// }
// })
// .then(json => {
// setCurrentUser(json)
// console.log("USER>>>>>>>", json);
// })
// .catch(error => console.log(error))
// }
return (
<Router fallback={<span />}>
<Switch>
@ -51,10 +37,9 @@ const Layout = () => {
<Route path="/authentication/split" component={AuthSplitRoutes} />
<Route path="/authentication/wizard" component={WizardLayout} /> */}
<Route path="/errors" component={ErrorLayout} />
<Route path="/authentication/basic" component={AuthBasicLayout} />
{/* <Route path="/authentication/basic" component={AuthBasicLayout} /> */}
<Route component={DashboardLayout} />
</Switch>
<ToastContainer transition={Fade} closeButton={<CloseButton />} position={toast.POSITION.BOTTOM_LEFT} />
@ -62,6 +47,4 @@ const Layout = () => {
);
};
export default Layout;

1
web/.gitignore vendored
View File

@ -43,3 +43,4 @@ public/uploads
/log/*.out
BUILD_NUMBER
.byebug_history
.ruby-version

View File

@ -5,6 +5,8 @@ class ApiSearchController < ApiController
respond_to :json
include LatencyHelper
def index
if 1 == params[Search::PARAM_MUSICIAN].to_i || 1 == params[Search::PARAM_BAND].to_i
query = params.clone
@ -93,8 +95,8 @@ class ApiSearchController < ApiController
end
end
#filter users by first fetching users from latency graph database
#for the latency filter options and then quering the relational
#Filter users by first fetching users from latency graph database
#for latency specific filter options and then query the postgresql relational
#database for other filter options
def filter
latency_good = ActiveRecord::Type::Boolean.new.type_cast_from_user(params[:latency_good])
@ -102,7 +104,9 @@ class ApiSearchController < ApiController
latency_high = ActiveRecord::Type::Boolean.new.type_cast_from_user(params[:latency_high])
begin
user_ids = user_ids_by_latency(latency_good, latency_fair, latency_high)
@latency_data = users_latency_data(latency_good, latency_fair, latency_high)
#debugger
user_ids = @latency_data.map{ |l_data| l_data[:user_id] }
filter_params = {
"sort_order"=>"latency",
@ -168,58 +172,73 @@ private
"#{Rails.application.config.latency_data_host}/search_users"
end
def user_ids_by_latency(latency_good, latency_fair, latency_high)
user_ids = []
if latency_good || latency_fair || latency_high
uri = URI(filter_latency_url)
begin
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if Rails.application.config.latency_data_host.start_with?("https://")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Basic #{Rails.application.config.latency_data_host_auth_code}"
req["Content-Type"] = "application/json"
req.body = {
my_user_id: current_user.id,
my_public_ip: request.remote_ip,
my_device_id: nil,
my_client_id: nil
}.to_json
# def users_latency_data(latency_good, latency_fair, latency_high)
# latency_data = []
# if latency_good || latency_fair || latency_high
# uri = URI(filter_latency_url)
# begin
# http = Net::HTTP.new(uri.host, uri.port)
# http.use_ssl = true if Rails.application.config.latency_data_host.start_with?("https://")
# req = Net::HTTP::Post.new(uri)
# req["Authorization"] = "Basic #{Rails.application.config.latency_data_host_auth_code}"
# req["Content-Type"] = "application/json"
# req.body = {
# my_user_id: current_user.id,
# my_public_ip: request.remote_ip,
# my_device_id: nil,
# my_client_id: nil
# }.to_json
response = http.request(req)
# response = http.request(req)
if response.is_a?(Net::HTTPOK) || response.is_a?(Net::HTTPSuccess)
graph_db_users = JSON.parse(response.body)["users"]
if latency_good || latency_fair || latency_high
graph_db_users.select! do |user|
total_latency = user["ars"]["total_latency"].to_f
(total_latency <= 40 && latency_good) ||
(total_latency > 40 && total_latency <= 80 && latency_fair) ||
(total_latency > 80 && latency_high)
end
end
# if response.is_a?(Net::HTTPOK) || response.is_a?(Net::HTTPSuccess)
# graph_db_users = JSON.parse(response.body)["users"]
# if latency_good || latency_fair || latency_high
# graph_db_users.select! do |user|
# total_latency = user["ars"]["total_latency"].to_f
# (total_latency >= 0 && total_latency <= 40 && latency_good) ||
# (total_latency > 40 && total_latency <= 80 && latency_fair) ||
# (total_latency > 80 && latency_high)
# end
# end
user_ids = graph_db_users.map { | user | user["user_id"] }.uniq
return user_ids
else
logger.debug("Latency response failed: #{response}")
Bugsnag.notify("LatencyResponseFailed") do |report|
report.severity = "faliure"
report.add_tab(:latency, {
user_id: current_user.id,
name: current_user.name,
params: params,
url: filter_latency_url,
code: response.code,
body: response.body,
})
end
end
rescue => exception
raise exception
end
end
user_ids
end
# latency_data = graph_db_users.map { | user |
# total = user["ars"]["total_latency"].to_f
# label = if total >= 0 && total <= 40
# 'good'
# elsif total > 40 && total <= 80
# 'fair'
# else
# 'high'
# end
# {
# user_id: user["user_id"],
# audio_latency: user["audio_latency"].to_f,
# ars_total_latency: user["ars"]["total_latency"].to_f,
# ars_internet_latency: user["ars"]["internet_latency"].to_f
# }
# }.uniq
# #debugger
# return latency_data
# else
# logger.debug("Latency response failed: #{response}")
# Bugsnag.notify("LatencyResponseFailed") do |report|
# report.severity = "faliure"
# report.add_tab(:latency, {
# user_id: current_user.id,
# name: current_user.name,
# params: params,
# url: filter_latency_url,
# code: response.code,
# body: response.body,
# })
# end
# end
# rescue => exception
# raise exception
# end
# end
# latency_data
# end
end

View File

@ -34,7 +34,7 @@ class ApiUsersController < ApiController
end
def me
render json: { first_name: current_user.first_name, last_name: current_user.last_name, name: current_user.name, photo_url: current_user.photo_url }, status: 200
render json: { id: current_user.id, first_name: current_user.first_name, last_name: current_user.last_name, name: current_user.name, photo_url: current_user.photo_url }, status: 200
end
def show

View File

@ -0,0 +1,153 @@
module LatencyHelper
LATENCY_SCORES = {
good: { label: 'GOOD', min: 0, max: 40 },
fair: { label: 'FAIR', min: 40, max: 80 },
high: { label: 'HIGH', min: 80, max: 10000000 },
me: { label: 'ME', min: -1, max: -1 },
unknown: { label: 'UNKNOWN', min: -2, max: -2 }
}
# def users_latency_data(latency_good, latency_fair, latency_high)
# latency_data = []
# if latency_good || latency_fair || latency_high
# uri = URI(filter_latency_url)
# begin
# http = Net::HTTP.new(uri.host, uri.port)
# http.use_ssl = true if Rails.application.config.latency_data_host.start_with?("https://")
# req = Net::HTTP::Post.new(uri)
# req["Authorization"] = "Basic #{Rails.application.config.latency_data_host_auth_code}"
# req["Content-Type"] = "application/json"
# req.body = {
# my_user_id: current_user.id,
# my_public_ip: request.remote_ip,
# my_device_id: nil,
# my_client_id: nil
# }.to_json
# response = http.request(req)
# if response.is_a?(Net::HTTPOK) || response.is_a?(Net::HTTPSuccess)
# graph_db_users = JSON.parse(response.body)["users"]
# if latency_good || latency_fair || latency_high
# graph_db_users.select! do |user|
# total_latency = user["ars"]["total_latency"].to_f
# (total_latency >= LATENCY_SCORES[:good][:min] && total_latency <= LATENCY_SCORES[:good][:max] && latency_good) ||
# (total_latency > LATENCY_SCORES[:fair][:min] && total_latency <= LATENCY_SCORES[:fair][:max] && latency_fair) ||
# (total_latency > LATENCY_SCORES[:high][:min] && latency_high)
# end
# end
# latency_data = graph_db_users.map { | user |
# total = user["ars"]["total_latency"].to_f
# label = if total >= LATENCY_SCORES[:good][:min] && total <= LATENCY_SCORES[:good][:max]
# LATENCY_SCORES[:good][:label]
# elsif total > LATENCY_SCORES[:fair][:min] && total <= LATENCY_SCORES[:fair][:max]
# LATENCY_SCORES[:fair][:label]
# elsif total > LATENCY_SCORES[:high][:min]
# LATENCY_SCORES[:high][:label]
# else
# LATENCY_SCORES[:unknown][:label]
# end
# {
# user_id: user["user_id"],
# audio_latency: user["audio_latency"].to_f,
# ars_total_latency: user["ars"]["total_latency"].to_f,
# ars_internet_latency: user["ars"]["internet_latency"].to_f,
# label: label
# }
# }.uniq
# #debugger
# return latency_data
# else
# logger.debug("Latency response failed: #{response}")
# Bugsnag.notify("LatencyResponseFailed") do |report|
# report.severity = "faliure"
# report.add_tab(:latency, {
# user_id: current_user.id,
# name: current_user.name,
# params: params,
# url: filter_latency_url,
# code: response.code,
# body: response.body,
# })
# end
# end
# rescue => exception
# raise exception
# end
# end
# latency_data
# end
def users_latency_data(latency_good, latency_fair, latency_high)
latency_data = []
if latency_good || latency_fair || latency_high
uri = URI(filter_latency_url)
begin
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if Rails.application.config.latency_data_host.start_with?("https://")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Basic #{Rails.application.config.latency_data_host_auth_code}"
req["Content-Type"] = "application/json"
req.body = {
my_user_id: current_user.id,
my_public_ip: request.remote_ip,
my_device_id: nil,
my_client_id: nil
}.to_json
response = http.request(req)
if response.is_a?(Net::HTTPOK) || response.is_a?(Net::HTTPSuccess)
graph_db_users = JSON.parse(response.body)["users"]
if latency_good || latency_fair || latency_high
graph_db_users.select! do |user|
total_latency = user["ars"]["total_latency"].to_f
(total_latency >= LATENCY_SCORES[:good][:min] && total_latency <= LATENCY_SCORES[:good][:max] && latency_good) ||
(total_latency > LATENCY_SCORES[:fair][:min] && total_latency <= LATENCY_SCORES[:fair][:max] && latency_fair) ||
(total_latency > LATENCY_SCORES[:high][:min] && latency_high)
end
end
latency_data = graph_db_users.map { | user |
total = user["ars"]["total_latency"].to_f
label = if total >= LATENCY_SCORES[:good][:min] && total <= LATENCY_SCORES[:good][:max]
LATENCY_SCORES[:good][:label]
elsif total > LATENCY_SCORES[:fair][:min] && total <= LATENCY_SCORES[:fair][:max]
LATENCY_SCORES[:fair][:label]
elsif total > LATENCY_SCORES[:high][:min]
LATENCY_SCORES[:high][:label]
else
LATENCY_SCORES[:unknown][:label]
end
{
user_id: user["user_id"],
audio_latency: user["audio_latency"].to_f,
ars_total_latency: user["ars"]["total_latency"].to_f,
ars_internet_latency: user["ars"]["internet_latency"].to_f,
label: label
}
}.uniq
return latency_data
else
logger.debug("Latency response failed: #{response}")
Bugsnag.notify("LatencyResponseFailed") do |report|
report.severity = "faliure"
report.add_tab(:latency, {
user_id: current_user.id,
name: current_user.name,
params: params,
url: filter_latency_url,
code: response.code,
body: response.body,
})
end
end
rescue => exception
raise exception
end
end
latency_data
end
end

View File

@ -60,6 +60,17 @@ if @search.is_a?(BaseSearch)
node :audio_latency do |musician|
last_jam_audio_latency(musician)
end
node :latency_data do |musician|
if latency = @latency_data.detect{|l_data| l_data[:user_id] == musician.id }
{
audio_latency: latency[:audio_latency],
ars_internet_latency: latency[:ars_internet_latency],
ars_total_latency: latency[:ars_total_latency]
}
end if @latency_data
end
}
elsif @search.is_a?(BandSearch)

View File

@ -4,7 +4,7 @@ Rails.application.config.middleware.insert_before 0, Rack::Cors do
resource '*',
headers: :any,
methods: [:get, :post, :options],
methods: [:get, :post, :delete, :options],
credentials: true
end
end

View File

@ -10,16 +10,18 @@ describe "Musician Filter API", type: :request do
let(:user4) { FactoryGirl.create(:user) }
let(:user5) { FactoryGirl.create(:user) }
let(:user6) { FactoryGirl.create(:user) }
let(:user7) { FactoryGirl.create(:user) }
let(:latency_data_uri) { /\S+\/search_users/ }
let(:response_body) { mock_latency_response([
{ user: user1, ars_total_latency: 1.0, ars_internet_latency: 0.4, audio_latency: 0.6 }, #GOOD
{ user: user2, ars_total_latency: 40.0, ars_internet_latency: 25.0, audio_latency: 15.0 }, #GOOD
{ user: user3, ars_total_latency: 41.0, ars_internet_latency: 25, audio_latency: 16 }, #FAIR
{ user: user3, ars_total_latency: 40.1, ars_internet_latency: 25, audio_latency: 15.1 }, #FAIR
{ user: user4, ars_total_latency: 80.0, ars_internet_latency: 40, audio_latency: 40.0 }, #FAIR
{ user: user5, ars_total_latency: 81.0, ars_internet_latency: 41, audio_latency: 40 }, #HIGH
{ user: user6, ars_total_latency: 100.0, ars_internet_latency: 50.0, audio_latency: 50.0 } #HIGH
{ user: user5, ars_total_latency: 80.1, ars_internet_latency: 40.1, audio_latency: 40 }, #HIGH
{ user: user6, ars_total_latency: 100.0, ars_internet_latency: 50.0, audio_latency: 50.0 }, #HIGH
{ user: user7, ars_total_latency: -2, ars_internet_latency: -1, audio_latency: -1 } #UNKNOWN
])
}
@ -28,7 +30,6 @@ describe "Musician Filter API", type: :request do
let(:rock) { Genre.find_by_id('rock') }
before(:each) do
#ActiveMusicSession.delete_all
User.delete_all
stub_request(:post, latency_data_uri)
.with(:headers => {'Accept'=>'*/*', 'Content-Type'=>'application/json', 'User-Agent'=>'Ruby'})
@ -45,20 +46,27 @@ describe "Musician Filter API", type: :request do
it "get all musicians" do
get '/api/search/musicians.json?results=true'
expect(JSON.parse(response.body)["musicians"].size).to eq(6)
expect(JSON.parse(response.body)["musicians"].size).to eq(7)
end
it "filter all musicians for all latency types" do
post '/api/filter.json', { latency_good: false, latency_fair: false, latency_high: false }
expect(JSON.parse(response.body)["musicians"].size).to eq(6)
expect(JSON.parse(response.body)["musicians"].size).to eq(7)
expect(JSON.parse(response.body)["musicians"][0]["latency_data"]).to eq(nil)
end
it "filter GOOD latency users" do
it "filter GOOD latency users", focus: true do
post '/api/filter.json', { latency_good: true, latency_fair: false, latency_high: false }
expect(response.content_type).to eq("application/json")
expect(response).to render_template(:index)
expect(response).to have_http_status(:created)
expect(JSON.parse(response.body)["musicians"].size).to eq(2)
#test latency data
expect(JSON.parse(response.body)["musicians"][0]["latency_data"]).not_to eq(nil)
expect(JSON.parse(response.body)["musicians"][0]["latency_data"]["audio_latency"]).not_to eq(nil)
expect(JSON.parse(response.body)["musicians"][0]["latency_data"]["ars_internet_latency"]).not_to eq(nil)
expect(JSON.parse(response.body)["musicians"][0]["latency_data"]["ars_total_latency"]).not_to eq(nil)
end
it "filter FAIR latency musicians" do
@ -120,7 +128,7 @@ describe "Musician Filter API", type: :request do
end
it "filter musicians by instruments they play", focus: true do
it "filter musicians by instruments they play" do
user1.musician_instruments << FactoryGirl.create(:musician_instrument, player: user1, instrument: JamRuby::Instrument.find('drums'), proficiency_level: 1 )
user1.musician_instruments << FactoryGirl.create(:musician_instrument, player: user1, instrument: JamRuby::Instrument.find('violin'), proficiency_level: 2 )
user1.save!
@ -144,7 +152,7 @@ describe "Musician Filter API", type: :request do
expect(JSON.parse(response.body)["musicians"].size).to eq(0)
end
it "filter musicians by joined within day" do
it "filter musicians by days ago that they joined" do
user1.created_at = 1.day.ago
user1.save!

View File

@ -26,3 +26,4 @@ target
vendor
BUILD_NUMBER
.byebug_history
.ruby-version