Converted put(api/announcement) to use useSend
Added useSendButtonCaption hook Related to #19
This commit is contained in:
parent
b360b06d34
commit
b7bbd937b4
12
front/src/api/putAnnouncement/index.ts
Normal file
12
front/src/api/putAnnouncement/index.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { API_URL } from '../../config'
|
||||||
|
import { PutAnnouncement, PutAnnouncementResponse } from './types'
|
||||||
|
|
||||||
|
const composePutAnnouncementURL = () => (
|
||||||
|
API_URL + '/announcement?'
|
||||||
|
)
|
||||||
|
|
||||||
|
const processPutAnnouncement = (data: PutAnnouncementResponse): PutAnnouncement => {
|
||||||
|
return data.Answer
|
||||||
|
}
|
||||||
|
|
||||||
|
export { composePutAnnouncementURL, processPutAnnouncement }
|
17
front/src/api/putAnnouncement/types.ts
Normal file
17
front/src/api/putAnnouncement/types.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { isObject } from '../../utils/types'
|
||||||
|
|
||||||
|
type PutAnnouncementResponse = {
|
||||||
|
Answer: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPutAnnouncementResponse = (obj: unknown): obj is PutAnnouncementResponse => (
|
||||||
|
isObject(obj, {
|
||||||
|
'Answer': 'boolean'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
type PutAnnouncement = boolean
|
||||||
|
|
||||||
|
export type { PutAnnouncementResponse, PutAnnouncement }
|
||||||
|
|
||||||
|
export { isPutAnnouncementResponse }
|
@ -1,79 +1,31 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
import { API_URL } from '../../config'
|
import { useSend } from '..'
|
||||||
import { isLiteralUnion } from '../../utils/types'
|
import { composePutAnnouncementURL, processPutAnnouncement } from '../../api/putAnnouncement'
|
||||||
import { handleHTTPErrors } from '../../utils'
|
import { isPutAnnouncementResponse } from '../../api/putAnnouncement/types'
|
||||||
|
import useSendButtonCaption from '../useSendButtonCaption'
|
||||||
const addErrors = ['Не удалось опубликовать объявление', 'Неверный ответ от сервера', 'Неизвестная ошибка'] as const
|
|
||||||
type AddError = typeof addErrors[number]
|
|
||||||
|
|
||||||
const isAddError = (obj: unknown): obj is AddError => (
|
|
||||||
isLiteralUnion(obj, addErrors)
|
|
||||||
)
|
|
||||||
|
|
||||||
const buttonStates = ['Опубликовать', 'Загрузка...', 'Опубликовано', 'Отменено'] as const
|
|
||||||
type ButtonState = typeof buttonStates[number] | AddError
|
|
||||||
|
|
||||||
type AddResponse = {
|
|
||||||
Answer: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAddResponse = (obj: unknown): obj is AddResponse => (
|
|
||||||
typeof obj === 'object' && obj !== null && typeof Reflect.get(obj, 'Answer') === 'boolean'
|
|
||||||
)
|
|
||||||
|
|
||||||
const useAddAnnouncement = () => {
|
const useAddAnnouncement = () => {
|
||||||
const [status, setStatus] = useState<ButtonState>('Опубликовать')
|
const { doSend, loading, error } = useSend(
|
||||||
|
composePutAnnouncementURL(),
|
||||||
|
'PUT',
|
||||||
|
true,
|
||||||
|
isPutAnnouncementResponse,
|
||||||
|
processPutAnnouncement,
|
||||||
|
)
|
||||||
|
|
||||||
const timerIdRef = useRef<number>()
|
const { update, ...button } = useSendButtonCaption('Опубликовать', loading, error, 'Опубликовано')
|
||||||
const abortControllerRef = useRef<AbortController>()
|
|
||||||
|
|
||||||
const doAdd = async (formData: FormData) => {
|
const doSendWithButton = useCallback(async (formData: FormData) => {
|
||||||
if (status === 'Загрузка...') {
|
const data = await doSend({}, {
|
||||||
abortControllerRef.current?.abort()
|
body: formData
|
||||||
setStatus('Отменено')
|
})
|
||||||
timerIdRef.current = setTimeout(() => setStatus('Опубликовать'), 3000)
|
update(data)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setStatus('Загрузка...')
|
return data
|
||||||
|
}, [doSend, update])
|
||||||
|
|
||||||
const abortController = new AbortController()
|
return { doSend: doSendWithButton, button }
|
||||||
abortControllerRef.current = abortController
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(API_URL + '/announcement', {
|
|
||||||
method: 'PUT',
|
|
||||||
body: formData,
|
|
||||||
signal: abortController.signal
|
|
||||||
})
|
|
||||||
|
|
||||||
handleHTTPErrors(res)
|
|
||||||
|
|
||||||
const data: unknown = await res.json()
|
|
||||||
|
|
||||||
if (!isAddResponse(data)) throw new Error('Неверный ответ от сервера')
|
|
||||||
|
|
||||||
if (!data.Answer) {
|
|
||||||
throw new Error('Не удалось опубликовать объявление')
|
|
||||||
}
|
|
||||||
setStatus('Опубликовано')
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
setStatus(isAddError(err) ? err : 'Неизвестная ошибка')
|
|
||||||
timerIdRef.current = setTimeout(() => setStatus('Опубликовать'), 10000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const abortController = abortControllerRef.current
|
|
||||||
return () => {
|
|
||||||
clearTimeout(timerIdRef.current)
|
|
||||||
abortController?.abort()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return { doAdd, status }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useAddAnnouncement
|
export default useAddAnnouncement
|
||||||
|
@ -24,6 +24,7 @@ function useSend<R, T>(
|
|||||||
/** Don't use in useEffect. If you need request result, go with useFetch instead */
|
/** Don't use in useEffect. If you need request result, go with useFetch instead */
|
||||||
const doSend = useCallback(async (urlProps?: Record<string, string>, params?: Omit<RequestInit, 'method'>) => {
|
const doSend = useCallback(async (urlProps?: Record<string, string>, params?: Omit<RequestInit, 'method'>) => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
if (abortControllerRef.current) {
|
if (abortControllerRef.current) {
|
||||||
abortControllerRef.current.abort()
|
abortControllerRef.current.abort()
|
||||||
|
44
front/src/hooks/useSendButtonCaption.ts
Normal file
44
front/src/hooks/useSendButtonCaption.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
function useSendButtonCaption(
|
||||||
|
initial: string,
|
||||||
|
loading: boolean,
|
||||||
|
error: string | null,
|
||||||
|
result = initial,
|
||||||
|
singular = true
|
||||||
|
) {
|
||||||
|
const [caption, setCaption] = useState(initial)
|
||||||
|
const [disabled, setDisabled] = useState(false)
|
||||||
|
const [title, setTitle] = useState(initial)
|
||||||
|
|
||||||
|
const update = useCallback(<T extends NonNullable<unknown>>(data: T | undefined) => {
|
||||||
|
if (data !== undefined) {
|
||||||
|
setCaption(result)
|
||||||
|
setTitle('Отправить ещё раз')
|
||||||
|
|
||||||
|
if (singular) {
|
||||||
|
setDisabled(true)
|
||||||
|
setTitle('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [result, singular])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading) {
|
||||||
|
setCaption('Загрузка...')
|
||||||
|
setTitle('Отменить и отправить ещё раз')
|
||||||
|
}
|
||||||
|
}, [loading])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loading && error !== null) {
|
||||||
|
setCaption(error + ', нажмите, чтобы попробовать ещё раз')
|
||||||
|
setTitle('')
|
||||||
|
}
|
||||||
|
}, [error, loading])
|
||||||
|
|
||||||
|
return { update, children: caption, disabled, title }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useSendButtonCaption
|
@ -1,11 +1,10 @@
|
|||||||
import { CSSProperties, FormEventHandler, useEffect, useState } from 'react'
|
import { CSSProperties, FormEventHandler, useState } from 'react'
|
||||||
import { Form, Button, Card } from 'react-bootstrap'
|
import { Form, Button, Card } from 'react-bootstrap'
|
||||||
import { MapContainer, TileLayer } from 'react-leaflet'
|
import { MapContainer, TileLayer } from 'react-leaflet'
|
||||||
import { latLng } from 'leaflet'
|
import { latLng } from 'leaflet'
|
||||||
|
|
||||||
import { ClickHandler, LocationMarker, TrashboxMarkers } from '../components'
|
import { ClickHandler, LocationMarker, TrashboxMarkers } from '../components'
|
||||||
import { useAddAnnouncement, useTrashboxes } from '../hooks/api'
|
import { useAddAnnouncement, useTrashboxes } from '../hooks/api'
|
||||||
import { handleHTTPErrors } from '../utils'
|
|
||||||
import { categories, categoryNames } from '../assets/category'
|
import { categories, categoryNames } from '../assets/category'
|
||||||
import { stations, lines, lineNames } from '../assets/metro'
|
import { stations, lines, lineNames } from '../assets/metro'
|
||||||
import { fallbackError, gotError } from '../hooks/useFetch'
|
import { fallbackError, gotError } from '../hooks/useFetch'
|
||||||
@ -32,25 +31,7 @@ function AddPage() {
|
|||||||
|
|
||||||
const address = useOsmAddresses(addressPosition)
|
const address = useOsmAddresses(addressPosition)
|
||||||
|
|
||||||
useEffect(() => {
|
const { doSend, button } = useAddAnnouncement()
|
||||||
if (!gotError(address))
|
|
||||||
void (async () => {
|
|
||||||
try {
|
|
||||||
const res = await fetch(location.protocol + '//nominatim.openstreetmap.org/search?format=json&q=' + encodeURIComponent(address.data))
|
|
||||||
|
|
||||||
handleHTTPErrors(res)
|
|
||||||
|
|
||||||
const fetchData: unknown = await res.json()
|
|
||||||
|
|
||||||
console.log('f', fetchData)
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
}, [address])
|
|
||||||
|
|
||||||
const { doAdd, status } = useAddAnnouncement()
|
|
||||||
|
|
||||||
const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
|
const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@ -63,7 +44,7 @@ function AddPage() {
|
|||||||
formData.append('address', address.data || '') // if address.error
|
formData.append('address', address.data || '') // if address.error
|
||||||
formData.set('bestBy', new Date((formData.get('bestBy') as number | null) || 0).getTime().toString())
|
formData.set('bestBy', new Date((formData.get('bestBy') as number | null) || 0).getTime().toString())
|
||||||
|
|
||||||
void doAdd(formData)
|
void doSend(formData)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -151,7 +132,7 @@ function AddPage() {
|
|||||||
</Form.Select>
|
</Form.Select>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group className='mb-3' controlId='password'>
|
<Form.Group className='mb-3' controlId='trashbox'>
|
||||||
<Form.Label>Пункт сбора мусора</Form.Label>
|
<Form.Label>Пункт сбора мусора</Form.Label>
|
||||||
<div className='mb-3'>
|
<div className='mb-3'>
|
||||||
{trashboxes.loading
|
{trashboxes.loading
|
||||||
@ -195,9 +176,7 @@ function AddPage() {
|
|||||||
)}
|
)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Button variant='success' type='submit'>
|
<Button variant='success' type='submit' {...button} />
|
||||||
{status}
|
|
||||||
</Button>
|
|
||||||
</Form>
|
</Form>
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
</Card>
|
</Card>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user