beta site current session listing with e2e tests

This commit is contained in:
Nuwan 2023-11-01 18:58:08 +05:30
parent b3922ec025
commit 6a57530a8b
19 changed files with 929 additions and 26177 deletions

View File

@ -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');
});
});
});
});
});

View File

@ -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);
})
})

View File

@ -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
);
}

View File

@ -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
);
}

View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress", "node"]
},
"include": ["**/*.ts"]
}

26150
jam-ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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>
);

View File

@ -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">

View File

@ -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;

View File

@ -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 whats 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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -4,6 +4,7 @@
"loading": "Loading",
"more": "More",
"actions": "Actions",
"no_records": "No Records!",
"navigation": {
"home": "Home",
"friends": "Friends",

View File

@ -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"
}
}
}