beta site current session listing with e2e tests
This commit is contained in:
parent
b3922ec025
commit
6a57530a8b
|
|
@ -1,33 +1,265 @@
|
|||
/// <reference types="cypress" />
|
||||
|
||||
describe('Browse sessions', () => {
|
||||
import makeFakeSession from '../../factories/session';
|
||||
import makeFakeUser from '../../factories/user';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
describe('Browse sessions', () => {
|
||||
const currentUser = makeFakeUser();
|
||||
beforeEach(() => {
|
||||
cy.stubAuthenticate({ id: '6'})
|
||||
cy.stubAuthenticate({ id: currentUser.id });
|
||||
});
|
||||
|
||||
describe('when there are no active sessions', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('GET', /\S+\/api\/sessions/, {
|
||||
body: []
|
||||
})
|
||||
cy.visit('/sessions')
|
||||
});
|
||||
cy.visit('/sessions');
|
||||
});
|
||||
|
||||
it("alerts when there is no records", () => {
|
||||
cy.contains("No Records!")
|
||||
})
|
||||
})
|
||||
it('alerts when there is no records', () => {
|
||||
cy.contains('No Records!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are active sessions', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('GET', /\S+\/api\/sessions/, { fixture: 'sessions' })
|
||||
cy.visit('/sessions')
|
||||
describe('when there are active sessions', () => {
|
||||
describe('in desktop', () => {
|
||||
beforeEach(() => {
|
||||
cy.viewport('macbook-15');
|
||||
});
|
||||
|
||||
it('lists the sessions', () => {
|
||||
const session = makeFakeSession();
|
||||
cy.intercept('GET', /\S+\/api\/sessions/, [session]);
|
||||
cy.visit('/sessions');
|
||||
cy.get('[data-testid=sessionsListTable] tbody tr').should('have.length', 1);
|
||||
});
|
||||
|
||||
describe('session description', () => {
|
||||
it('when user has provided a description', () => {
|
||||
const session = makeFakeSession({
|
||||
description: 'custom description'
|
||||
});
|
||||
cy.intercept('GET', /\S+\/api\/sessions/, [session]);
|
||||
cy.visit('/sessions');
|
||||
cy.get('[data-testid=sessionsListTable] tbody tr').contains('custom description');
|
||||
});
|
||||
|
||||
it('when user has not provided a description and session is public', () => {
|
||||
const session = makeFakeSession({
|
||||
description: null,
|
||||
musician_access: true,
|
||||
approval_required: false
|
||||
});
|
||||
cy.intercept('GET', /\S+\/api\/sessions/, [session]);
|
||||
cy.visit('/sessions');
|
||||
cy.get('[data-testid=sessionsListTable] tbody tr').contains('Public, open session. Feel free to join!');
|
||||
});
|
||||
|
||||
it('when user has not provided a description and session is private and requires approval to join', () => {
|
||||
const session = makeFakeSession({
|
||||
description: null,
|
||||
musician_access: true,
|
||||
approval_required: true
|
||||
});
|
||||
cy.intercept('GET', /\S+\/api\/sessions/, [session]);
|
||||
cy.visit('/sessions');
|
||||
cy.get('[data-testid=sessionsListTable] tbody tr').contains(
|
||||
'Private session. Click the enter button in the right column to request to join'
|
||||
);
|
||||
});
|
||||
|
||||
it('when user has not provided a description and session is RSVP', () => {
|
||||
const session = makeFakeSession({
|
||||
description: null,
|
||||
musician_access: false,
|
||||
approval_required: false
|
||||
});
|
||||
cy.intercept('GET', /\S+\/api\/sessions/, [session]);
|
||||
cy.visit('/sessions');
|
||||
cy.get('[data-testid=sessionsListTable] tbody tr').contains('Only RSVP musicians may join');
|
||||
});
|
||||
});
|
||||
|
||||
describe('invitation', () => {
|
||||
it('shows invite detail if the user has been invited', () => {
|
||||
const session = makeFakeSession({
|
||||
invitations: [
|
||||
{
|
||||
sender_id: '1',
|
||||
receiver_id: currentUser.id
|
||||
}
|
||||
]
|
||||
});
|
||||
cy.intercept('GET', /\S+\/api\/sessions/, [session]);
|
||||
cy.visit('/sessions');
|
||||
cy.get('[data-testid=sessionsListTable] tbody tr').contains('YOU WERE INVITED TO THIS SESSION');
|
||||
});
|
||||
});
|
||||
|
||||
describe('friend info', () => {
|
||||
it('shows if there is a friend in session', () => {
|
||||
const session = makeFakeSession({
|
||||
participants: [
|
||||
{
|
||||
user: {
|
||||
is_friend: true
|
||||
},
|
||||
tracks: [
|
||||
{
|
||||
id: '1',
|
||||
instrument: 'Guitar'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
console.log('_DEBUG_ session', session);
|
||||
cy.intercept('GET', /\S+\/api\/sessions/, [session]);
|
||||
cy.visit('/sessions');
|
||||
cy.get('[data-testid=sessionsListTable] tbody tr').contains('YOU HAVE A FRIEND IN THIS SESSION');
|
||||
});
|
||||
});
|
||||
|
||||
describe('participants', () => {
|
||||
it('shows the participants', () => {
|
||||
const session = makeFakeSession({
|
||||
participants: [
|
||||
{
|
||||
user: {
|
||||
name: 'John Doe'
|
||||
},
|
||||
tracks: [
|
||||
{
|
||||
id: '1',
|
||||
instrument: 'Guitar'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
user: {
|
||||
name: 'Ray Charles'
|
||||
},
|
||||
tracks: [
|
||||
{
|
||||
id: '2',
|
||||
instrument: 'Bass'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
cy.intercept('GET', /\S+\/api\/sessions/, [session]);
|
||||
cy.visit('/sessions');
|
||||
cy.get('[data-testid=sessionsListTable] tbody tr').contains('John Doe');
|
||||
cy.get('[data-testid=sessionsListTable] tbody tr').contains('Ray Charles');
|
||||
});
|
||||
});
|
||||
|
||||
describe('instruments', () => {
|
||||
it('shows the instruments', () => {
|
||||
const session = makeFakeSession({
|
||||
participants: [
|
||||
{
|
||||
id: 'p1',
|
||||
user: {
|
||||
name: 'John Doe'
|
||||
},
|
||||
tracks: [
|
||||
{
|
||||
id: '1',
|
||||
instrument: 'Guitar'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
instrument: 'Drums'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
user: {
|
||||
name: 'Ray Charles'
|
||||
},
|
||||
tracks: [
|
||||
{
|
||||
id: '2',
|
||||
instrument: 'Bass'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
cy.intercept('GET', /\S+\/api\/sessions/, [session]);
|
||||
cy.visit('/sessions');
|
||||
cy.get('[data-testid=sessionsListTable] tbody tr')
|
||||
.find(`[data-testid=Participantp1Tracks]`)
|
||||
.contains('Guitar');
|
||||
cy.get('[data-testid=sessionsListTable] tbody tr')
|
||||
.find(`[data-testid=Participantp1Tracks]`)
|
||||
.contains('Drums');
|
||||
cy.get('[data-testid=sessionsListTable] tbody tr')
|
||||
.find(`[data-testid=Participantp2Tracks]`)
|
||||
.contains('Bass');
|
||||
});
|
||||
});
|
||||
|
||||
describe('join button', () => {
|
||||
it('shows toast message if private & not invited', () => {
|
||||
const session = makeFakeSession({
|
||||
musician_access: true,
|
||||
approval_required: true
|
||||
});
|
||||
cy.intercept('GET', /\S+\/api\/sessions/, [session]);
|
||||
cy.visit('/sessions');
|
||||
cy.get('[data-testid=sessionsListTable] tbody tr')
|
||||
.find('[data-testid=joinBtn]')
|
||||
.click();
|
||||
cy.contains('You have requested to join this private session.');
|
||||
});
|
||||
|
||||
it('opens native client and place in the session if public', () => {
|
||||
const session = makeFakeSession({
|
||||
musician_access: true,
|
||||
approval_required: false
|
||||
});
|
||||
const newUrl = `jamkazam://url=http://www.jamkazam.local:3000/client#/findSession/custom~yes%7CjoinSessionId~${
|
||||
session.id
|
||||
}`;
|
||||
cy.intercept('GET', /\S+\/api\/sessions/, [session]);
|
||||
cy.visit('/sessions').then(win => {
|
||||
cy.stub(win, 'open').as('windowOpen');
|
||||
});
|
||||
cy.get('[data-testid=sessionsListTable] tbody tr')
|
||||
.find('[data-testid=joinBtn]')
|
||||
.click();
|
||||
cy.get('@windowOpen').should('have.been.calledWith', newUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("lists the sessions", () => {
|
||||
cy.get('[data-testid=sessionsListTable] tbody tr').should('have.length', 1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('in mobile', () => {
|
||||
beforeEach(() => {
|
||||
cy.viewport('iphone-6');
|
||||
});
|
||||
|
||||
})
|
||||
describe('pagination', () => {
|
||||
it('shows the next page of sessions', () => {
|
||||
const sessions = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
sessions.push(makeFakeSession({ id: faker.string.uuid() }));
|
||||
}
|
||||
cy.intercept('GET', /\S+\/api\/sessions/, sessions);
|
||||
cy.visit('/sessions');
|
||||
cy.get('[data-testid=sessionsSwiper] .swiper-button-prev').should('have.class', 'swiper-button-disabled');
|
||||
for (let i = 0; i < 4; i++) { // 4 because the first one is already visible
|
||||
cy.get('[data-testid=sessionsSwiper] .swiper-button-next').click();
|
||||
cy.wait(500);
|
||||
}
|
||||
cy.get('[data-testid=sessionsSwiper] .swiper-button-next').should('have.class', 'swiper-button-disabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ describe('Create new session', () => {
|
|||
beforeEach(() => {
|
||||
cy.stubAuthenticate({ id: '6' });
|
||||
cy.intercept('GET', /\S+\/users\/\d+\/friends/, { fixture: 'friends' }).as('friends');
|
||||
cy.visit('/sessions/new');
|
||||
|
||||
});
|
||||
|
||||
it("adds invitees - using autocomplete list click", () => {
|
||||
cy.visit('/sessions/new');
|
||||
cy.get('[data-testid=autocomplete-text]').type('Dav')
|
||||
cy.get('[data-testid=autocomplete-list] li').should('have.length', 2)
|
||||
cy.get('[data-testid=autocomplete-list] li:first').click()
|
||||
|
|
@ -21,6 +22,7 @@ describe('Create new session', () => {
|
|||
//the behaviour is to submit the form on hitting the enter key. need to figureout a way to prevent this.
|
||||
//https://www.w3.org/MarkUp/html-spec/html-spec_8.html#SEC8.2
|
||||
it.skip("adds invitees using autocomplete enter", () => {
|
||||
cy.visit('/sessions/new');
|
||||
cy.get('[data-testid=autocomplete-text]').type('Dav')
|
||||
cy.get('[data-testid=autocomplete-list] li').should('have.length', 2)
|
||||
cy.get('[data-testid=autocomplete-text]').type('{enter}')
|
||||
|
|
@ -31,6 +33,7 @@ describe('Create new session', () => {
|
|||
});
|
||||
|
||||
it("removes invitees", () => {
|
||||
cy.visit('/sessions/new');
|
||||
cy.get('[data-testid=autocomplete-text]').type('Seth')
|
||||
cy.get('[data-testid=autocomplete-list] li').should('have.length', 1)
|
||||
cy.get('[data-testid=autocomplete-list] li:first').click()
|
||||
|
|
@ -45,6 +48,7 @@ describe('Create new session', () => {
|
|||
});
|
||||
|
||||
it("choose friends as invitees", ()=> {
|
||||
cy.visit('/sessions/new');
|
||||
cy.get('[data-testid=btn-choose-friends]').click();
|
||||
cy.get('[data-testid=modal-choose-friends]').should('be.visible').contains('Invite Friends to Session')
|
||||
cy.get('[data-testid=modal-choose-friends]').find('[type="checkbox"]').first().click()
|
||||
|
|
@ -55,6 +59,7 @@ describe('Create new session', () => {
|
|||
})
|
||||
|
||||
it("prefill form using saved data in localStorage", () => {
|
||||
cy.visit('/sessions/new');
|
||||
cy.get('[data-testid=session-privacy]').select("2")
|
||||
cy.get('[data-testid=autocomplete-text]').type('Dav')
|
||||
cy.get('[data-testid=autocomplete-list] li:first').click()
|
||||
|
|
@ -67,8 +72,8 @@ describe('Create new session', () => {
|
|||
})
|
||||
|
||||
it.only("submits form", () => {
|
||||
const newUrl = `jamkazam://url=http://www.jamkazam.local:3000/client#/createSession/privacy~2%7Cdescription~test%7CinviteeIds~1`;
|
||||
cy.window().then(win => {
|
||||
const newUrl = `jamkazam://url=http://www.jamkazam.local:3000/client#/createSession/custom~yes%7Cprivacy~2%7Cdescription~test%7CinviteeIds~1`;
|
||||
cy.visit('/sessions/new').then((win) => {
|
||||
cy.stub(win, 'open').as('windowOpen');
|
||||
});
|
||||
cy.get('[data-testid=session-privacy]').select("2")
|
||||
|
|
@ -76,8 +81,8 @@ describe('Create new session', () => {
|
|||
cy.get('[data-testid=autocomplete-list] li:first').click()
|
||||
cy.get('[data-testid=session-description]').type("test")
|
||||
cy.get('[data-testid=btn-create-session]').click();
|
||||
cy.get('@windowOpen').should('have.been.calledWith', newUrl);
|
||||
cy.get('[data-testid=btn-create-session]').should('be.disabled')
|
||||
cy.get('@windowOpen').should('be.calledWith', newUrl);
|
||||
})
|
||||
|
||||
})
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import { mergePartially, NestedPartial } from 'merge-partially';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface Track {
|
||||
id: string;
|
||||
instrument: string;
|
||||
}
|
||||
|
||||
interface Participant {
|
||||
id: string;
|
||||
client_id: string;
|
||||
user: User;
|
||||
tracks: Track[];
|
||||
}
|
||||
|
||||
interface Invitations {
|
||||
id: string;
|
||||
sender_id?: string;
|
||||
receiver_id?: string;
|
||||
}
|
||||
|
||||
interface Session {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
participants?: Participant[];
|
||||
invitations?: Invitations[];
|
||||
}
|
||||
|
||||
export default function makeFakeSession(overrides?: NestedPartial<Session>): Session {
|
||||
return mergePartially.deep(
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
name: faker.lorem.sentence({ min: 3, max: 5 }),
|
||||
description: faker.lorem.paragraph(),
|
||||
participants: [
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
client_id: faker.string.uuid(),
|
||||
user: {
|
||||
id: faker.string.uuid(),
|
||||
name: faker.person.firstName(),
|
||||
},
|
||||
tracks: [{
|
||||
id: faker.string.uuid(),
|
||||
instrument: "Piano"
|
||||
}]
|
||||
}],
|
||||
invitations: [
|
||||
{
|
||||
id: faker.string.uuid()
|
||||
}
|
||||
]
|
||||
},
|
||||
overrides
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { mergePartially, NestedPartial } from 'merge-partially';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
interface IUser {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export default function makeFakeUser(overrides?: NestedPartial<IUser>): IUser {
|
||||
return mergePartially.deep(
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
email: faker.internet.email(),
|
||||
firstName: faker.person.firstName(),
|
||||
lastName: faker.person.lastName(),
|
||||
},
|
||||
overrides
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["es5", "dom"],
|
||||
"types": ["cypress", "node"]
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -36,6 +36,7 @@
|
|||
"leaflet.markercluster": "^1.4.1",
|
||||
"leaflet.tilelayer.colorfilter": "^1.2.5",
|
||||
"lodash": "^4.17.20",
|
||||
"merge-partially": "^2.0.2",
|
||||
"moment": "^2.28.0",
|
||||
"plyr": "3.6.2",
|
||||
"prism-react-renderer": "^0.1.7",
|
||||
|
|
@ -77,7 +78,8 @@
|
|||
"react-typed": "^1.2.0",
|
||||
"reactstrap": "^8.6.0",
|
||||
"slick-carousel": "^1.8.1",
|
||||
"uuid": "^3.4.0"
|
||||
"uuid": "^3.4.0",
|
||||
"faker": "^5.5.3"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
|
|
@ -91,6 +93,7 @@
|
|||
"extends": "react-app"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.2.0",
|
||||
"@playwright/test": "^1.15.2",
|
||||
"browser-sync": "^2.26.12",
|
||||
"eslint-config-prettier": "^4.2.0",
|
||||
|
|
@ -106,6 +109,7 @@
|
|||
"gulp-sourcemaps": "^2.6.5",
|
||||
"prettier": "1.17.1",
|
||||
"swiper": "^6.8.2",
|
||||
"typescript": "^3.9.10",
|
||||
"webpack-cli": "^4.10.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +1,22 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Col,
|
||||
Row,
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
Table,
|
||||
Form,
|
||||
Modal,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter
|
||||
} from 'reactstrap';
|
||||
import { Alert, Col, Row, Card, CardBody, Table } from 'reactstrap';
|
||||
import FalconCardHeader from '../common/FalconCardHeader';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { fetchSessions } from '../../store/features/sessionsSlice';
|
||||
import SessionRow from '../sessions/SessionRow';
|
||||
import Loader from '../common/Loader';
|
||||
import { isIterableArray } from '../../helpers/utils';
|
||||
import { useResponsive } from '@farfetch/react-context-responsive';
|
||||
|
||||
import JKSessionSwiper from '../sessions/JKSessionSwiper';
|
||||
import JKSessionList from '../sessions/JKSessionList';
|
||||
import Loader from '../common/Loader';
|
||||
|
||||
function JKMusicSessions() {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const sessions = useSelector(state => state.session.sessions);
|
||||
const loadingStatus = useSelector(state => state.session.status);
|
||||
const { greaterThan } = useResponsive();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchSessions());
|
||||
|
|
@ -36,34 +26,31 @@ function JKMusicSessions() {
|
|||
<Card>
|
||||
<FalconCardHeader title={t('list.page_title', { ns: 'sessions' })} titleClass="font-weight-bold" />
|
||||
<CardBody className="pt-0">
|
||||
<Table striped bordered className="fs--1" data-testid="sessionsListTable">
|
||||
<thead className="bg-200 text-900">
|
||||
<tr>
|
||||
<th scope="col">{t('list.header.session', { ns: 'sessions' })}</th>
|
||||
<th scope="col" style={{ minWidth: 250 }}>
|
||||
{t('list.header.musicians', { ns: 'sessions' })}
|
||||
</th>
|
||||
<th scope="col">{t('list.header.latency', { ns: 'sessions' })}</th>
|
||||
<th scope="col">{t('list.header.instruments', { ns: 'sessions' })}</th>
|
||||
<th scope="col">{t('actions', { ns: 'common' })}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="list">
|
||||
{loadingStatus === 'loading' && sessions.length === 0 ? (
|
||||
<Loader />
|
||||
) : isIterableArray(sessions) ? (
|
||||
sessions.map(session => <SessionRow key={session.id} session={session} />)
|
||||
{loadingStatus === 'loading' && sessions.length === 0 ? (
|
||||
<Loader />
|
||||
) : isIterableArray(sessions) ? (
|
||||
<>
|
||||
{greaterThan.sm ? (
|
||||
<Row className="mb-3 justify-content-between d-none d-md-block">
|
||||
<div className="table-responsive-xl px-2">
|
||||
<JKSessionList sessions={sessions} />
|
||||
</div>
|
||||
</Row>
|
||||
) : (
|
||||
<Row className="p-card">
|
||||
<Col>
|
||||
<Alert color="info" className="mb-0">
|
||||
No Records!
|
||||
</Alert>
|
||||
</Col>
|
||||
<Row className="swiper-container d-block d-md-none" data-testid="sessionsSwiper">
|
||||
<JKSessionSwiper sessions={sessions} />
|
||||
</Row>
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
</>
|
||||
) : (
|
||||
<Row className="p-card">
|
||||
<Col>
|
||||
<Alert color="info" className="mb-0">
|
||||
{t('no_records', { ns: 'common' })}
|
||||
</Alert>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -426,7 +426,6 @@ function JKPeopleFilter() {
|
|||
{loadingStatus === 'loading' && people.length === 0 ? (
|
||||
<Loader />
|
||||
) : isIterableArray(people) ? (
|
||||
//Start Find Friends table hidden on small screens
|
||||
<>
|
||||
{greaterThan.sm ? (
|
||||
<Row className="mb-3 justify-content-between d-none d-md-block">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import JKLatencyBadge from './JKLatencyBadge';
|
||||
|
||||
const JKUserLatency = ({user}) => {
|
||||
const latencyData = useSelector(state => state.latency.latencies.find(l => l.user_id === user.id));
|
||||
return (
|
||||
<JKLatencyBadge latencyData={latencyData} />
|
||||
);
|
||||
};
|
||||
|
||||
JKUserLatency.propTypes = { user: PropTypes.object.isRequired };
|
||||
|
||||
export default JKUserLatency;
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { useAuth } from '../../context/UserAuth';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { fetchUserLatencies } from '../../store/features/latencySlice';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useResponsive } from '@farfetch/react-context-responsive';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Row, Col, Button } from 'reactstrap';
|
||||
|
||||
import jkCustomUrlScheme from '../../helpers/jkCustomUrlScheme';
|
||||
|
||||
import JKUserLatencyBadge from '../profile/JKUserLatencyBadge';
|
||||
import JKSessionUser from './JKSessionUser';
|
||||
|
||||
function JKSession({ session }) {
|
||||
const { currentUser } = useAuth();
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const { greaterThan } = useResponsive();
|
||||
|
||||
useEffect(() => {
|
||||
const participantIds = session.participants.map(p => p.user.id);
|
||||
const options = { currentUserId: currentUser.id, participantIds };
|
||||
dispatch(fetchUserLatencies(options));
|
||||
}, []);
|
||||
|
||||
function joinSession() {
|
||||
if (session.musician_access && session.approval_required) {
|
||||
toast.info(t('list.alerts.join_request_sent', { ns: 'sessions' }));
|
||||
} else {
|
||||
const q = `joinSessionId~${session.id}`;
|
||||
const urlScheme = jkCustomUrlScheme('findSession', q);
|
||||
window.open(urlScheme);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const sessionDescription = session => {
|
||||
if (session.description) {
|
||||
return session.description;
|
||||
} else if (session.musician_access && !session.approval_required) {
|
||||
return t('list.descriptions.public_open_session', { ns: 'sessions' });
|
||||
} else if (session.musician_access && session.approval_required) {
|
||||
return t('list.descriptions.private_session', { ns: 'sessions' });
|
||||
} else if (!session.musician_access && !session.approval_required) {
|
||||
return t("list.descriptions.rsvp_session", { ns: "sessions" });
|
||||
}
|
||||
};
|
||||
|
||||
const invitedNote = session => {
|
||||
if (session.invitations.find(i => i.receiver_id === currentUser.id)) {
|
||||
return t('list.notes.invited', { ns: 'sessions' });
|
||||
}
|
||||
};
|
||||
|
||||
const hasFriendNote = session => {
|
||||
if (session.participants.find(p => p.user.is_friend)) {
|
||||
return t('list.notes.has_friend', { ns: 'sessions' })
|
||||
}
|
||||
};
|
||||
|
||||
const actionButtons = () => {
|
||||
return (
|
||||
<div>
|
||||
<Button onClick={joinSession} color="primary" data-testid="joinBtn" className="btn-join mr-1 mb-1">
|
||||
<FontAwesomeIcon icon="arrow-right" transform="shrink-4 down-1" className="mr-1" />
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={() =>
|
||||
console.log(`TODO: If the user
|
||||
clicks this button, we open an audio stream using Icecast server to let the user hear what’s being played currently in the session`)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon="volume-up" transform="shrink-4 down-1" className="mr-1" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{greaterThan.sm ? (
|
||||
<tr key={session.id}>
|
||||
<td>
|
||||
<div>
|
||||
<u>
|
||||
<small>
|
||||
<strong>{invitedNote(session)}</strong>
|
||||
</small>
|
||||
</u>
|
||||
</div>
|
||||
<div>
|
||||
<u>
|
||||
<small>{hasFriendNote(session)}</small>
|
||||
</u>
|
||||
</div>
|
||||
<div>{sessionDescription(session)}</div>
|
||||
</td>
|
||||
<td>
|
||||
{session.participants.map(participant => (
|
||||
<JKSessionUser key={participant.id} user={participant.user} />
|
||||
))}
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
{session.participants.map(participant => (
|
||||
<JKUserLatencyBadge user={participant.user} />
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{session.participants.map(participant => (
|
||||
<div className="d-flex flex-row" key={participant.id} data-testid={`Participant${participant.id}Tracks`}>
|
||||
{participant.tracks.map(track => (
|
||||
<div key={track.id}>{track.instrument}</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</td>
|
||||
<td>{actionButtons()}</td>
|
||||
</tr>
|
||||
) : (
|
||||
<Row>
|
||||
<Col>
|
||||
<div>
|
||||
<u>
|
||||
<small>
|
||||
<strong>{invitedNote(session)}</strong>
|
||||
</small>
|
||||
</u>
|
||||
</div>
|
||||
<div>
|
||||
<u>
|
||||
<small>{hasFriendNote(session)}</small>
|
||||
</u>
|
||||
</div>
|
||||
<div>{sessionDescription(session)}</div>
|
||||
<div className="d-flex flex-row justify-content-between mt-3">
|
||||
<div className="ml-2 ms-2">
|
||||
<h5>{t('list.header.musicians', { ns: 'sessions'})}</h5>
|
||||
</div>
|
||||
<div className="ml-2 ms-2">
|
||||
<strong>{t('list.header.latency', { ns: 'sessions'})}</strong>
|
||||
</div>
|
||||
</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">{actionButtons()}</div>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
JKSession.propTypes = { session: PropTypes.object.isRequired };
|
||||
|
||||
export default JKSession;
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import React from 'react';
|
||||
import { Table } from 'reactstrap';
|
||||
import JKSession from './JKSession';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function JKSessionList({ sessions }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Table striped bordered className="fs--1" data-testid="sessionsListTable">
|
||||
<thead className="bg-200 text-900">
|
||||
<tr>
|
||||
<th scope="col">{t('list.header.session', { ns: 'sessions' })}</th>
|
||||
<th scope="col" style={{ minWidth: 250 }}>
|
||||
{t('list.header.musicians', { ns: 'sessions' })}
|
||||
</th>
|
||||
<th scope="col">{t('list.header.latency', { ns: 'sessions' })}</th>
|
||||
<th scope="col">{t('list.header.instruments', { ns: 'sessions' })}</th>
|
||||
<th scope="col">{t('actions', { ns: 'common' })}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="list">
|
||||
{sessions.map(session => (
|
||||
<JKSession key={session.id} session={session} />
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
JKSessionList.propTypes = { sessions: PropTypes.array.isRequired };
|
||||
|
||||
export default JKSessionList;
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
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 JKSession from './JKSession';
|
||||
|
||||
// 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';
|
||||
|
||||
SwiperCore.use([Navigation, Pagination, Scrollbar, A11y]);
|
||||
|
||||
const JKSessionSwiper = ({ sessions }) => {
|
||||
|
||||
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'
|
||||
}}
|
||||
>
|
||||
{sessions.map((session, index) => (
|
||||
<SwiperSlide key={`sessions-swiper-item-${session.id}`}>
|
||||
<Card className="swiper-person-card">
|
||||
<CardHeader className="bg-200">
|
||||
<div className="avatar avatar-xl d-inline-block me-2 mr-2">
|
||||
</div>
|
||||
<h5 className="d-inline-block align-top mt-1">{session.name}</h5>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<JKSession session={session} 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
JKSessionSwiper.propTypes = {
|
||||
sessions: PropTypes.arrayOf(PropTypes.instanceOf(Object)).isRequired,
|
||||
};
|
||||
|
||||
export default JKSessionSwiper;
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { fetchPerson } from '../../store/features/peopleSlice';
|
||||
import { useResponsive } from '@farfetch/react-context-responsive';
|
||||
|
||||
import JKLatencyBadge from '../profile/JKLatencyBadge';
|
||||
import JKProfileSidePanel from '../profile/JKProfileSidePanel';
|
||||
import JKProfileAvatar from '../profile/JKProfileAvatar';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function JKSessionUser({ user }) {
|
||||
const dispatch = useDispatch();
|
||||
const latencyData = useSelector(state => state.latency.latencies.find(l => l.user_id === user.id));
|
||||
const [showSidePanel, setShowSidePanel] = useState(false);
|
||||
const { greaterThan } = useResponsive();
|
||||
|
||||
const toggleMoreDetails = async e => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await dispatch(fetchPerson({ userId: user.id })).unwrap();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
setShowSidePanel(prev => !prev);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{!greaterThan.sm ? (
|
||||
<div className="d-flex flex-row justify-content-between">
|
||||
<div className="avatar avatar-sm">
|
||||
<JKProfileAvatar url={user.photo_url} />
|
||||
</div>
|
||||
<div className="ml-2 ms-2" style={{ width: '70%'}}>
|
||||
<a href="/#" onClick={toggleMoreDetails}>
|
||||
{user.name}
|
||||
</a>
|
||||
</div>
|
||||
<div className="ml-2 ms-2" style={{ marginRight: 'auto'}}>
|
||||
<JKLatencyBadge latencyData={latencyData} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="d-flex flex-row align-items-center mb-1 fs-0">
|
||||
<div className="avatar avatar-xl">
|
||||
<JKProfileAvatar url={user.photo_url} />
|
||||
</div>
|
||||
<div className="ml-2 ms-2">
|
||||
<strong>{user.name}</strong>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<JKProfileSidePanel show={showSidePanel} user={user} toggle={toggleMoreDetails} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
JKSessionUser.propTypes = { user: PropTypes.object.isRequired };
|
||||
|
||||
export default JKSessionUser;
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import jkCustomUrlScheme from '../../helpers/jkCustomUrlScheme';
|
||||
import JKProfileAvatar from '../profile/JKProfileAvatar';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { useAuth } from '../../context/UserAuth';
|
||||
import SessionUserLatency from './SessionUserLatency';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { fetchUserLatencies } from '../../store/features/latencySlice';
|
||||
import SessionUser from './SessionUser';
|
||||
|
||||
function SessionRow({session}) {
|
||||
const { currentUser } = useAuth();
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
const participantIds = session.participants.map(p => p.user.id)
|
||||
const options = { currentUserId: currentUser.id, participantIds }
|
||||
dispatch(fetchUserLatencies(options))
|
||||
}, [])
|
||||
|
||||
function joinSession(){
|
||||
const q = `joinSessionId~${session.id}`
|
||||
const urlScheme = jkCustomUrlScheme('findSession', q)
|
||||
window.open(urlScheme)
|
||||
}
|
||||
|
||||
const sessionDescription = (session) => {
|
||||
if(session.description){
|
||||
return session.description;
|
||||
}else if(session.musician_access && !session.approval_required){
|
||||
return "Public, open session. Feel free to join!"
|
||||
}else if(session.musician_access && session.approval_required){
|
||||
return "Private session. Click the enter button in the right column to request to join"
|
||||
}else if(!session.musician_access && !session.approval_required){
|
||||
return "Only RSVP musicians may join"
|
||||
}
|
||||
}
|
||||
|
||||
const invitedNote = (session) => {
|
||||
if(session.invitations.find(i => i.receiver_id === currentUser.id)){
|
||||
return "YOU WERE INVITED TO THIS SESSION"
|
||||
}
|
||||
}
|
||||
|
||||
const hasFriendNote = session => {
|
||||
if(session.participants.find(p => p.user.is_friend)){
|
||||
return "YOU HAVE A FRIEND IN THIS SESSION"
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={session.id}>
|
||||
<td>
|
||||
<div>
|
||||
<u>
|
||||
<small>
|
||||
<strong>{invitedNote(session)}</strong>
|
||||
</small>
|
||||
</u>
|
||||
</div>
|
||||
<div>
|
||||
<u>
|
||||
<small>{hasFriendNote(session)}</small>
|
||||
</u>
|
||||
</div>
|
||||
<div>{sessionDescription(session)}</div>
|
||||
</td>
|
||||
<td>
|
||||
{session.participants.map(participant => (
|
||||
<SessionUser key={participant.id} user={participant.user} />
|
||||
))}
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
{session.participants.map(participant => (
|
||||
<SessionUserLatency key={participant.id} user={participant.user} />
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="d-flex flex-row">
|
||||
{session.participants.map(participant =>
|
||||
participant.tracks.map(track => <div key={track.id}>{track.instrument}</div>)
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a onClick={joinSession} className="btn btn-sm btn-outline-secondary mr-1">
|
||||
<FontAwesomeIcon icon="arrow-right" />
|
||||
</a>
|
||||
<a href="" className="btn btn-sm btn-outline-secondary">
|
||||
<FontAwesomeIcon icon="volume-up" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default SessionRow;
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import JKProfileSidePanel from '../profile/JKProfileSidePanel';
|
||||
import JKProfileAvatar from '../profile/JKProfileAvatar';
|
||||
import { fetchPerson } from '../../store/features/peopleSlice';
|
||||
|
||||
function SessionUser({ user }) {
|
||||
const dispatch = useDispatch()
|
||||
const latencyData = useSelector(state => state.latency.latencies.find(l => l.user_id === user.id));
|
||||
const [showSidePanel, setShowSidePanel] = useState(false);
|
||||
|
||||
const toggleMoreDetails = async e => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await dispatch(fetchPerson({ userId: user.id })).unwrap();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
setShowSidePanel(prev => !prev);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className="d-flex flex-row">
|
||||
<div className="avatar avatar-xl">
|
||||
<JKProfileAvatar url={user.photo_url} />
|
||||
</div>
|
||||
<div className="ml-2 ms-2">
|
||||
<a href="/#" onClick={toggleMoreDetails}>{user.name}</a>
|
||||
</div>
|
||||
</div>
|
||||
<JKProfileSidePanel user={user} latencyData={latencyData} show={showSidePanel} setShow={setShowSidePanel} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SessionUser;
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import JKLatencyBadge from '../profile/JKLatencyBadge';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
function SessionUserLatency({user}) {
|
||||
|
||||
const latencyData = useSelector(state => state.latency.latencies.find(l => l.user_id === user.id));
|
||||
|
||||
return <JKLatencyBadge latencyData={latencyData} />;
|
||||
}
|
||||
|
||||
export default SessionUserLatency;
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
"loading": "Loading",
|
||||
"more": "More",
|
||||
"actions": "Actions",
|
||||
"no_records": "No Records!",
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"friends": "Friends",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,18 @@
|
|||
"latency": "Latency",
|
||||
"instruments": "Instruments",
|
||||
"actions": "Actions"
|
||||
},
|
||||
"alerts" : {
|
||||
"join_request_sent": "You have requested to join this private session. You will be notified when you are approved."
|
||||
},
|
||||
"descriptions": {
|
||||
"public_open_session": "Public, open session. Feel free to join!",
|
||||
"private_session": "Private session. Click the enter button in the right column to request to join",
|
||||
"rsvp_session": "Only RSVP musicians may join"
|
||||
},
|
||||
"notes": {
|
||||
"invited": "YOU WERE INVITED TO THIS SESSION",
|
||||
"has_friend": "YOU HAVE A FRIEND IN THIS SESSION"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue