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:
Nuwan Chathuranga 2021-09-27 19:23:40 +05:30 committed by Nuwan
parent cb35148876
commit 791f536c8b
9 changed files with 236 additions and 126 deletions

View File

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

View File

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

View File

@ -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 = {

View File

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

View File

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

View File

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

View File

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

View File

@ -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) => {

View File

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