diff --git a/jam-ui/cypress/integration/friends/friends-list.spec.js b/jam-ui/cypress/integration/friends/friends-list.spec.js index a2d97166a..64757135e 100644 --- a/jam-ui/cypress/integration/friends/friends-list.spec.js +++ b/jam-ui/cypress/integration/friends/friends-list.spec.js @@ -1,6 +1,23 @@ /// -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' }); diff --git a/jam-ui/src/components/page/JKPeople.js b/jam-ui/src/components/page/JKPeople.js index e6d92141d..af9b795f8 100644 --- a/jam-ui/src/components/page/JKPeople.js +++ b/jam-ui/src/components/page/JKPeople.js @@ -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 ( - + setShowSearch(!showSearch)}> @@ -93,13 +124,13 @@ const JKPeople = ({ className }) => { - {loading ? ( + { loadingStatus === 'loading' && people.length === 0 ? ( ) : isIterableArray(people) ? ( //Start Find Friends table hidden on small screens - + <> - + @@ -107,7 +138,7 @@ const JKPeople = ({ className }) => { - + > ) : ( diff --git a/jam-ui/src/components/page/JKPeopleSearch.js b/jam-ui/src/components/page/JKPeopleSearch.js index 4a39e255c..4fe0898a4 100644 --- a/jam-ui/src/components/page/JKPeopleSearch.js +++ b/jam-ui/src/components/page/JKPeopleSearch.js @@ -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 = { diff --git a/jam-ui/src/components/profile/JKMessageButton.js b/jam-ui/src/components/profile/JKMessageButton.js index b2c9ff5b5..7c70b5ae3 100644 --- a/jam-ui/src/components/profile/JKMessageButton.js +++ b/jam-ui/src/components/profile/JKMessageButton.js @@ -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} > diff --git a/jam-ui/src/components/profile/JKMessageModal.js b/jam-ui/src/components/profile/JKMessageModal.js index d6776962c..7e72e045a 100644 --- a/jam-ui/src/components/profile/JKMessageModal.js +++ b/jam-ui/src/components/profile/JKMessageModal.js @@ -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) => ( - - - - - - - - {message.senderName} - - { - 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`; - }} - /> - + { isIterableArray(messages) ? + messages.map((message, index) => ( + + + + + + + + {message.senderName} + + { + 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`; + }} + /> + + + {message.message} - {message.message} - - ))} + )) + : + + + + No messags yet! + + + + } {messagesArrived && ( diff --git a/jam-ui/src/helpers/rest.js b/jam-ui/src/helpers/rest.js index fd5cd4925..b0f7518b6 100644 --- a/jam-ui/src/helpers/rest.js +++ b/jam-ui/src/helpers/rest.js @@ -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)) + }) + } \ No newline at end of file diff --git a/jam-ui/src/store/features/peopleSlice.js b/jam-ui/src/store/features/peopleSlice.js new file mode 100644 index 000000000..1f7237d9f --- /dev/null +++ b/jam-ui/src/store/features/peopleSlice.js @@ -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; \ No newline at end of file diff --git a/jam-ui/src/store/features/textMessagesSlice.js b/jam-ui/src/store/features/textMessagesSlice.js index 8c2692447..cb34812ee 100644 --- a/jam-ui/src/store/features/textMessagesSlice.js +++ b/jam-ui/src/store/features/textMessagesSlice.js @@ -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) => { diff --git a/jam-ui/src/store/store.js b/jam-ui/src/store/store.js index 775c5363b..3505c8d37 100644 --- a/jam-ui/src/store/store.js +++ b/jam-ui/src/store/store.js @@ -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 } }) \ No newline at end of file