Added users rating in announcement details

This commit is contained in:
Dmitriy Shishkov 2023-08-07 14:08:51 +03:00
parent b93ab9794d
commit d2a3393a11
Signed by: dm1sh
GPG Key ID: 027994B0AA357688
11 changed files with 231 additions and 5 deletions

View File

@ -0,0 +1,12 @@
import { API_URL } from '../../config'
import { SendRateResponse, SendRate } from './types'
const composeSendRateURL = () => (
API_URL + '/user/rating?'
)
const processSendRate = (data: SendRateResponse): SendRate => {
return data.Success
}
export { composeSendRateURL, processSendRate }

View File

@ -0,0 +1,17 @@
import { isObject } from '../../utils/types'
type SendRateResponse = {
Success: boolean
}
const isSendRateResponse = (obj: unknown): obj is SendRateResponse => (
isObject(obj, {
'Success': 'boolean',
})
)
type SendRate = boolean
export type { SendRateResponse, SendRate }
export { isSendRateResponse }

View File

@ -0,0 +1,14 @@
import { API_URL } from '../../config'
import { UserRatingResponse, UserRating } from './types'
const initialUserRating: UserRating = 0
const composeUserRatingURL = (userId: number) => (
API_URL + '/user/rating?' + (new URLSearchParams({ user_id: userId.toString() })).toString()
)
const processUserRating = (data: UserRatingResponse): UserRating => {
return data.rating
}
export { initialUserRating, composeUserRatingURL, processUserRating }

View File

@ -0,0 +1,17 @@
import { isObject } from '../../utils/types'
type UserRatingResponse = {
rating: number
}
const isUserRatingResponse = (obj: unknown): obj is UserRatingResponse => (
isObject(obj, {
'rating': 'number',
})
)
type UserRating = number
export type { UserRatingResponse, UserRating }
export { isUserRatingResponse }

View File

@ -1,15 +1,16 @@
import { Modal, Button } from 'react-bootstrap' import { Modal, Button } from 'react-bootstrap'
import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet' import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet'
import { CSSProperties, useState } from 'react' import { CSSProperties, useState } from 'react'
import { LatLng } from 'leaflet'
import LineDot from './LineDot' import LineDot from './LineDot'
import Rating from './Rating'
import { categoryNames } from '../assets/category' import { categoryNames } from '../assets/category'
import { useBook, useDispose, useRemoveAnnouncement } from '../hooks/api' import { useBook, useRemoveAnnouncement } from '../hooks/api'
import { Announcement } from '../api/announcement/types' import { Announcement } from '../api/announcement/types'
import { iconItem } from '../utils/markerIcons' import { iconItem } from '../utils/markerIcons'
import { useId } from '../hooks' import { useId } from '../hooks'
import SelectDisposalTrashbox from './SelectDisposalTrashbox' import SelectDisposalTrashbox from './SelectDisposalTrashbox'
import { LatLng } from 'leaflet'
type AnnouncementDetailsProps = { type AnnouncementDetailsProps = {
close: () => void, close: () => void,
@ -30,7 +31,7 @@ const styles = {
} }
const View = ({ const View = ({
announcement: { name, category, bestBy, description, lat, lng, address, metro }, announcement: { name, category, bestBy, description, lat, lng, address, metro, userId },
}: { announcement: Announcement }) => ( }: { announcement: Announcement }) => (
<> <>
<h1>{name}</h1> <h1>{name}</h1>
@ -39,7 +40,9 @@ const View = ({
<span className='m-2'>&#x2022;</span>{/* dot */} <span className='m-2'>&#x2022;</span>{/* dot */}
<span>Годен до {new Date(bestBy).toLocaleString('ru-RU')}</span> <span>Годен до {new Date(bestBy).toLocaleString('ru-RU')}</span>
<p className='mb-3'>{description}</p> <p className='mb-0'>{description}</p>
<Rating userId={userId} className='mb-3' />
<MapContainer style={styles.map} center={[lat, lng]} zoom={16} > <MapContainer style={styles.map} center={[lat, lng]} zoom={16} >
<TileLayer <TileLayer
@ -67,7 +70,7 @@ type ControlProps = {
function Control({ function Control({
closeRefresh, closeRefresh,
announcement: { bookedBy, id, userId }, announcement: { bookedBy, id, userId },
showDispose showDispose,
}: ControlProps) { }: ControlProps) {
const { handleBook, bookButton } = useBook() const { handleBook, bookButton } = useBook()

View File

@ -0,0 +1,78 @@
import { useState } from 'react'
import { gotError, gotResponse } from '../hooks/useFetch'
import { useUserRating, useSendRate } from '../hooks/api'
import styles from '../styles/Rating.module.css'
type StarProps = {
filled: boolean,
selected: boolean,
setMyRate: () => void,
sendMyRate: () => void,
}
function Star({ filled, selected, setMyRate, sendMyRate }: StarProps) {
return (
<button
className={`${styles.star} ${filled ? styles.starFilled : ''} ${selected ? styles.starSelected : ''}`}
onMouseEnter={setMyRate}
onFocus={setMyRate}
onClick={sendMyRate}
>&#9733;</button>
)
}
type RatingProps = {
userId: number,
className: string | undefined,
}
function Rating({ userId, className }: RatingProps) {
const rating = useUserRating(userId)
const [myRate, setMyRate] = useState(0)
const { doSendRate } = useSendRate()
async function sendMyRate() {
const res = await doSendRate(myRate)
if (res) {
rating.refetch()
}
}
return (
<p className={className}>
Рейтинг пользователя:{' '}
{gotResponse(rating) ? (
gotError(rating) ? (
<span className='text-danger'>{rating.error}</span>
) : (
<span
className={styles.starContainer}
onMouseLeave={() => setMyRate(0)}
>
{...Array(5).fill(5).map(
(_, i) =>
<Star
key={i}
filled={i < rating.data}
selected={i < myRate}
setMyRate={() => setMyRate(i + 1)}
sendMyRate={() => void sendMyRate()}
/>
)}
</span>
)
) : (
<span>Загрузка...</span>
)
}
</p>
)
}
export default Rating

View File

@ -14,3 +14,4 @@ export { default as Points } from './Points'
export { default as SignOut } from './SignOut' export { default as SignOut } from './SignOut'
export { default as Poetry } from './Poetry' export { default as Poetry } from './Poetry'
export { default as SelectDisposalTrashbox } from './SelectDisposalTrashbox' export { default as SelectDisposalTrashbox } from './SelectDisposalTrashbox'
export { default as Rating } from './Rating'

View File

@ -9,3 +9,5 @@ export { default as useSignIn } from './useSignIn'
export { default as useSignUp } from './useSignUp' export { default as useSignUp } from './useSignUp'
export { default as usePoetry } from './usePoetry' export { default as usePoetry } from './usePoetry'
export { default as useDispose } from './useDispose' export { default as useDispose } from './useDispose'
export { default as useSendRate } from './useSendRate'
export { default as useUserRating } from './useUserRating'

View File

@ -0,0 +1,33 @@
import { useSend } from '..'
import { composeSendRateURL, processSendRate } from '../../api/sendRate'
import { isSendRateResponse } from '../../api/sendRate/types'
function useSendRate() {
const { doSend, ...rest } = useSend(
composeSendRateURL(),
'POST',
true,
isSendRateResponse,
processSendRate,
)
const doSendRate = (rate: number) => (
doSend({}, {
body: JSON.stringify({
rate,
}),
headers: {
'Content-Type': 'application/json',
},
})
)
return {
doSendRate,
...rest,
}
}
export default useSendRate

View File

@ -0,0 +1,30 @@
import { composeUserRatingURL, initialUserRating, processUserRating } from '../../api/userRating'
import { UserRating, isUserRatingResponse } from '../../api/userRating/types'
import useFetch, { UseFetchReturn } from '../useFetch'
const useUserRating = (userId: number): UseFetchReturn<UserRating> => (
// useFetch(
// composeUserRatingURL(userId),
// 'GET',
// false,
// isUserRatingResponse,
// processUserRating,
// initialUserRating
// )
{
data: 3,
loading: false,
error: null,
refetch: () => { return },
}
// {
// data: undefined,
// loading: true,
// error: null,
// refetch: () => { return },
// }
)
export default useUserRating

View File

@ -0,0 +1,19 @@
.star {
-webkit-text-stroke: 2px var(--bs-body-color);
font-size: 1.5rem;
color: var(--bs-modal-bg);
background: none;
border: none;
padding: 0 3px;
transition: 0.15s ease-in-out;
}
.starFilled {
color: var(--bs-body-color);
}
.starSelected {
color: var(--bs-success);
-webkit-text-stroke: 2px var(--bs-success);
transform: scale(1.3);
}