use redux for people state
reate redux store to manage state related to fetching musicians and showing them on friends page
This commit is contained in:
parent
cb35148876
commit
791f536c8b
|
|
@ -1,6 +1,23 @@
|
|||
/// <reference types="cypress" />
|
||||
|
||||
describe('Friends page', () => {
|
||||
describe('Friends page without data', () => {
|
||||
beforeEach(() => {
|
||||
cy.stubAuthenticate();
|
||||
cy.intercept('POST', /\S+\/filter/,
|
||||
{
|
||||
"musicians": []
|
||||
}
|
||||
);
|
||||
})
|
||||
|
||||
it('shows no records alert', () => {
|
||||
cy.visit('/friends');
|
||||
cy.contains('No Records!')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe('Friends page with data', () => {
|
||||
beforeEach(() => {
|
||||
cy.stubAuthenticate({ id: '2'}); //currentUser id is 2 - people.yaml fixture
|
||||
cy.intercept('POST', /\S+\/filter/, { fixture: 'people' });
|
||||
|
|
|
|||
|
|
@ -1,63 +1,103 @@
|
|||
import React, { useState, useEffect, Fragment } from 'react';
|
||||
import React, { useState, useEffect, useRef, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
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';
|
||||
// import useFakeFetch from '../../hooks/useFakeFetch';
|
||||
// import rawPeople from '../../data/people/people';
|
||||
// import peopleCategories from '../../data/people/peopleCategories';
|
||||
// import apiFetch from '../../helpers/apiFetch';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { fetchPeople } from '../../store/features/peopleSlice';
|
||||
|
||||
import JKPeopleSearch from './JKPeopleSearch';
|
||||
import JKPeopleList from './JKPeopleList';
|
||||
import { getMusicians, getPeople } from '../../helpers/rest';
|
||||
// import { getPeople } from '../../helpers/rest';
|
||||
import JKPeopleSwiper from './JKPeopleSwiper';
|
||||
|
||||
const JKPeople = ({ className }) => {
|
||||
//const { loading, data: people, setData: setPeople } = useFakeFetch(rawPeople);
|
||||
|
||||
const [people, setPeople] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
//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 [totalPages, setTotalPages] = useState(0);
|
||||
|
||||
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);
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('PEOPLE', data.musicians);
|
||||
//const users = new Set([...people, ...data.musicians]);
|
||||
//console.log("new users", users);
|
||||
//setPeople(Array.from(users));
|
||||
const peopleListRef = useRef();
|
||||
|
||||
setPeople(prev => Array.from(new Set([...prev, ...data.musicians])))
|
||||
const dispatch = useDispatch()
|
||||
|
||||
setTotalPages(data.page_count);
|
||||
})
|
||||
.catch(error => {
|
||||
//TODO: handle error
|
||||
console.log(error);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
const people = useSelector(state => state.people.people)
|
||||
const totalPages = useSelector(state => state.people.totalPages)
|
||||
const loadingStatus = useSelector(state => state.people.status)
|
||||
|
||||
useEffect(() => {
|
||||
fetchPeople(page);
|
||||
// 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);
|
||||
// throw new Error('Network response was not ok');
|
||||
// }
|
||||
// return response.json();
|
||||
// })
|
||||
// .then(data => {
|
||||
// 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);
|
||||
// });
|
||||
// }, []);
|
||||
|
||||
|
||||
|
||||
const loadPeople = React.useCallback(page => {
|
||||
try {
|
||||
dispatch(fetchPeople({page}))
|
||||
} catch (error) {
|
||||
console.log('Error fetching people', error);
|
||||
}
|
||||
|
||||
}, [page]);
|
||||
|
||||
const goNextPage = () => {
|
||||
// const loadPeople = (page) => {
|
||||
// try {
|
||||
// dispatch(fetchPeople({page}))
|
||||
// } catch (error) {
|
||||
// console.log('Error fetching people', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
useEffect(() => {
|
||||
loadPeople(page);
|
||||
}, [page]);
|
||||
|
||||
useEffect(() => {
|
||||
if(loadingStatus === 'succeeded' && peopleListRef.current && page !== 1){
|
||||
// peopleListRef.current.scrollIntoView(
|
||||
// {
|
||||
// behavior: 'smooth',
|
||||
// block: 'end',
|
||||
// inline: 'nearest'
|
||||
// })
|
||||
|
||||
}
|
||||
|
||||
}, [loadingStatus])
|
||||
|
||||
const goNextPage = (event) => {
|
||||
event.preventDefault()
|
||||
if (page < totalPages) {
|
||||
setPage(val => ++val);
|
||||
}
|
||||
|
|
@ -69,18 +109,9 @@ const JKPeople = ({ className }) => {
|
|||
}
|
||||
};
|
||||
|
||||
// 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>
|
||||
<JKPeopleSearch show={showSearch} setShow={setShowSearch} setPeople={setPeople} />
|
||||
<JKPeopleSearch show={showSearch} setShow={setShowSearch} />
|
||||
<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)}>
|
||||
|
|
@ -93,13 +124,13 @@ const JKPeople = ({ className }) => {
|
|||
</FalconCardHeader>
|
||||
|
||||
<CardBody className="pt-0">
|
||||
{loading ? (
|
||||
{ loadingStatus === 'loading' && people.length === 0 ? (
|
||||
<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">
|
||||
<div className="table-responsive-xl px-2" ref={peopleListRef}>
|
||||
<JKPeopleList people={people} goNextPage={goNextPage} page={page} totalPages={totalPages} />
|
||||
</div>
|
||||
</Row>
|
||||
|
|
@ -107,7 +138,7 @@ const JKPeople = ({ className }) => {
|
|||
<Row className="swiper-container d-block d-md-none">
|
||||
<JKPeopleSwiper people={people} goNextPage={goNextPage} />
|
||||
</Row>
|
||||
</Fragment>
|
||||
</>
|
||||
) : (
|
||||
<Row className="p-card">
|
||||
<Col>
|
||||
|
|
|
|||
|
|
@ -3,14 +3,18 @@ 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, getPeople } from '../../helpers/rest';
|
||||
import { getGenres, getInstruments } from '../../helpers/rest';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { fetchPeople } from '../../store/features/peopleSlice';
|
||||
|
||||
const JKPeopleSearch = props => {
|
||||
const { show, setShow, setPeople } = props;
|
||||
const { show, setShow } = props;
|
||||
const [instruments, setInstruments] = useState([]);
|
||||
const [genres, setGenres] = useState([]);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { register, handleSubmit, setValue, control } = useForm({
|
||||
defaultValues: {
|
||||
latency_good: true,
|
||||
|
|
@ -84,9 +88,10 @@ const JKPeopleSearch = props => {
|
|||
setShow(false);
|
||||
};
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
const onSubmit = (data) => {
|
||||
let genres = []
|
||||
let joined_within_days, active_within_days = ''
|
||||
|
||||
if(data.genres){
|
||||
genres = data.genres.map(genre => genre.value)
|
||||
}
|
||||
|
|
@ -94,21 +99,13 @@ const JKPeopleSearch = props => {
|
|||
active_within_days = data.active_within_days.value;
|
||||
|
||||
const updatedData = {...data, genres, joined_within_days, active_within_days}
|
||||
console.log('submitting...', updatedData);
|
||||
await getPeople({ data: updatedData, page: 1})
|
||||
.then(response => {
|
||||
if(!response.ok){
|
||||
//TODO: handle failure
|
||||
console.log(response);
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('people received', data);
|
||||
setPeople(data.musicians);
|
||||
})
|
||||
.catch(err => console.log(err))
|
||||
|
||||
try {
|
||||
dispatch(fetchPeople({data: updatedData, page: 1}))
|
||||
} catch (error) {
|
||||
console.log('Error fetching people', error);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const lastActiveOpts = [
|
||||
|
|
@ -286,7 +283,7 @@ const JKPeopleSearch = props => {
|
|||
JKPeopleSearch.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
setShow: PropTypes.func,
|
||||
setPeople: PropTypes.func
|
||||
//setPeople: PropTypes.func
|
||||
};
|
||||
|
||||
JKPeopleSearch.defaultProps = {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Button, Tooltip } from "reactstrap";
|
|||
import JKMessageModal from './JKMessageModal';
|
||||
|
||||
const JKMessageButton = props => {
|
||||
const { currentUser, user, cssClasses, children } = props;
|
||||
const { currentUser, user, cssClasses, children, size } = props;
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [isFriend, setIsFriend] = useState(false);
|
||||
const [pendingFriendRequest, setPendingFriendRequest] = useState(false);
|
||||
|
|
@ -28,9 +28,8 @@ const JKMessageButton = props => {
|
|||
id={"text-message-user-" + user.id}
|
||||
onClick={() => setShowModal(!showModal)}
|
||||
color="primary"
|
||||
size='sm'
|
||||
size={size}
|
||||
className={cssClasses}
|
||||
//title={buttonTitle()}
|
||||
data-testid="message"
|
||||
disabled={!isFriend || pendingFriendRequest}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { Modal, ModalHeader, ModalBody, Row, Col, Button, ModalFooter, Badge } from 'reactstrap';
|
||||
import { Modal, ModalHeader, ModalBody, Row, Col, Button, ModalFooter, Alert } from 'reactstrap';
|
||||
import { Scrollbar } from 'react-scrollbars-custom';
|
||||
import TimeAgo from 'react-timeago';
|
||||
import JKProfileAvatar from './JKProfileAvatar';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { fetchMessagesByReceiverId, postNewMessage } from '../../store/features/textMessagesSlice';
|
||||
import { isIterableArray } from '../../helpers/utils';
|
||||
|
||||
const JKMessageModal = props => {
|
||||
const { show, setShow, user } = props;
|
||||
|
|
@ -139,38 +140,48 @@ const JKMessageModal = props => {
|
|||
mobileNative={true}
|
||||
trackClickBehavior="step"
|
||||
>
|
||||
{messages.map((message, index) => (
|
||||
<div className="d-flex mb-3 mr-1 text-message-row" key={message.id}>
|
||||
<div className="avatar avatar-2xl d-inline-block">
|
||||
<JKProfileAvatar
|
||||
url={message.receiverId === currentUser.id ? currentUser.photo_url : user.photo_url}
|
||||
/>
|
||||
</div>
|
||||
<div className="d-inline-block">
|
||||
<div className="d-flex flex-column">
|
||||
<div>
|
||||
<strong>{message.senderName}</strong>
|
||||
<time className="notification-time ml-2 t-1">
|
||||
<TimeAgo
|
||||
date={message.createdAt}
|
||||
formatter={(value, unit) => {
|
||||
if (unit === 'second' && value < 15) return 'just now';
|
||||
if (unit === 'second') return 'few seconds ago';
|
||||
if (unit === 'minute') return `${value} ${value === 1 ? 'minute' : 'minutes'} ago`;
|
||||
if (unit === 'hour') return `${value} ${value === 1 ? 'hour' : 'hours'} ago`;
|
||||
if (unit === 'day') return `${value} ${value === 1 ? 'day' : 'days'} ago`;
|
||||
if (unit === 'week') return `${value} ${value === 1 ? 'week' : 'weeks'} ago`;
|
||||
if (unit === 'month') return `${value} ${value === 1 ? 'month' : 'months'} ago`;
|
||||
if (unit === 'year') return `${value} ${value === 1 ? 'year' : 'years'} ago`;
|
||||
}}
|
||||
/>
|
||||
</time>
|
||||
{ isIterableArray(messages) ?
|
||||
messages.map((message, index) => (
|
||||
<div className="d-flex mb-3 mr-1 text-message-row" key={message.id}>
|
||||
<div className="avatar avatar-2xl d-inline-block">
|
||||
<JKProfileAvatar
|
||||
url={message.receiverId === currentUser.id ? currentUser.photo_url : user.photo_url}
|
||||
/>
|
||||
</div>
|
||||
<div className="d-inline-block">
|
||||
<div className="d-flex flex-column">
|
||||
<div>
|
||||
<strong>{message.senderName}</strong>
|
||||
<time className="notification-time ml-2 t-1">
|
||||
<TimeAgo
|
||||
date={message.createdAt}
|
||||
formatter={(value, unit) => {
|
||||
if (unit === 'second' && value < 15) return 'just now';
|
||||
if (unit === 'second') return 'few seconds ago';
|
||||
if (unit === 'minute') return `${value} ${value === 1 ? 'minute' : 'minutes'} ago`;
|
||||
if (unit === 'hour') return `${value} ${value === 1 ? 'hour' : 'hours'} ago`;
|
||||
if (unit === 'day') return `${value} ${value === 1 ? 'day' : 'days'} ago`;
|
||||
if (unit === 'week') return `${value} ${value === 1 ? 'week' : 'weeks'} ago`;
|
||||
if (unit === 'month') return `${value} ${value === 1 ? 'month' : 'months'} ago`;
|
||||
if (unit === 'year') return `${value} ${value === 1 ? 'year' : 'years'} ago`;
|
||||
}}
|
||||
/>
|
||||
</time>
|
||||
</div>
|
||||
<div>{message.message}</div>
|
||||
</div>
|
||||
<div>{message.message}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))
|
||||
:
|
||||
<Row className="p-card">
|
||||
<Col>
|
||||
<Alert color="info" className="mb-0">
|
||||
No messags yet!
|
||||
</Alert>
|
||||
</Col>
|
||||
</Row>
|
||||
}
|
||||
</Scrollbar>
|
||||
{messagesArrived && (
|
||||
<Row>
|
||||
|
|
|
|||
|
|
@ -97,4 +97,13 @@ export const createTextMessage = (options) => {
|
|||
.then(response => resolve(response))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
export const getNotifications = (userId, options = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/users/${userId}/notifications?${new URLSearchParams(options)}`)
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"
|
||||
import { getPeople } from '../../helpers/rest';
|
||||
|
||||
const initialState = {
|
||||
people: [],
|
||||
status: 'idel',
|
||||
error: null,
|
||||
totalPages: 0,
|
||||
}
|
||||
|
||||
export const fetchPeople = createAsyncThunk(
|
||||
'people/fetchPeople',
|
||||
async (options, thunkAPI) => {
|
||||
//const { page, data } = options
|
||||
console.log('redux fetch', options);
|
||||
const response = await getPeople(options)
|
||||
return response.json()
|
||||
}
|
||||
)
|
||||
|
||||
export const peopleSlice = createSlice({
|
||||
name: 'people',
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(fetchPeople.pending, (state, action) => {
|
||||
state.status = 'loading'
|
||||
})
|
||||
.addCase(fetchPeople.fulfilled, (state, action) => {
|
||||
const records = new Set([...state.people, ...action.payload.musicians]);
|
||||
const unique = [];
|
||||
records.map(x => unique.filter(a => a.id === x.id).length > 0 ? null : unique.push(x))
|
||||
state.totalPages = action.payload.page_count
|
||||
state.people = unique
|
||||
state.status = 'succeeded'
|
||||
})
|
||||
.addCase(fetchPeople.rejected, (state, action) => {
|
||||
state.status = 'failed'
|
||||
state.error = action.error.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export default peopleSlice.reducer;
|
||||
|
|
@ -7,20 +7,7 @@ const initialState = {
|
|||
error: null
|
||||
}
|
||||
|
||||
export const fetchMessagesByReceiverId = createAsyncThunk(
|
||||
'textMessage/fetchMessagesByReceiverId',
|
||||
async (options, thunkAPI) => {
|
||||
const { userId, offset, limit } = options
|
||||
const response = await getTextMessages({
|
||||
target_user_id: userId,
|
||||
offset: offset,
|
||||
limit: limit
|
||||
})
|
||||
return response.json()
|
||||
}
|
||||
)
|
||||
|
||||
export const resturectureTextMessage = (args) => {
|
||||
const resturectureTextMessage = (args) => {
|
||||
const { message, sent } = args
|
||||
const messageId = message.id ? message.id : nanoid()
|
||||
const createdAt = message.created_at ? message.created_at : new Date().toISOString()
|
||||
|
|
@ -36,6 +23,19 @@ export const resturectureTextMessage = (args) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const fetchMessagesByReceiverId = createAsyncThunk(
|
||||
'textMessage/fetchMessagesByReceiverId',
|
||||
async (options, thunkAPI) => {
|
||||
const { userId, offset, limit } = options
|
||||
const response = await getTextMessages({
|
||||
target_user_id: userId,
|
||||
offset: offset,
|
||||
limit: limit
|
||||
})
|
||||
return response.json()
|
||||
}
|
||||
)
|
||||
|
||||
export const postNewMessage = createAsyncThunk(
|
||||
'textMessage/postNewMessage',
|
||||
async (message, thunkAPI) => {
|
||||
|
|
@ -65,8 +65,7 @@ export const textMessageSlice = createSlice({
|
|||
const msgs = action.payload.map(message => resturectureTextMessage({ message, sent: true }))
|
||||
const mergedMsgs = [...state.messages, ...msgs]
|
||||
const unique = [];
|
||||
mergedMsgs.map(x => unique.filter(a => a.id == x.id).length > 0 ? null : unique.push(x));
|
||||
console.log("unique PAYLOAD", unique);
|
||||
mergedMsgs.map(x => unique.filter(a => a.id === x.id).length > 0 ? null : unique.push(x));
|
||||
state.messages = unique
|
||||
})
|
||||
.addCase(fetchMessagesByReceiverId.rejected, (state, action) => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { configureStore } from "@reduxjs/toolkit"
|
||||
import textMessageReducer from "./features/textMessagesSlice"
|
||||
import peopleSlice from "./features/peopleSlice"
|
||||
|
||||
export default configureStore({
|
||||
reducer: {
|
||||
textMessage: textMessageReducer
|
||||
textMessage: textMessageReducer,
|
||||
people: peopleSlice
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue