diff --git a/front/src/api/putAnnouncement/index.ts b/front/src/api/putAnnouncement/index.ts new file mode 100644 index 0000000..0cc2cb4 --- /dev/null +++ b/front/src/api/putAnnouncement/index.ts @@ -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 } diff --git a/front/src/api/putAnnouncement/types.ts b/front/src/api/putAnnouncement/types.ts new file mode 100644 index 0000000..420f8a4 --- /dev/null +++ b/front/src/api/putAnnouncement/types.ts @@ -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 } diff --git a/front/src/hooks/api/useAddAnnouncement.ts b/front/src/hooks/api/useAddAnnouncement.ts index fb610aa..5c60488 100644 --- a/front/src/hooks/api/useAddAnnouncement.ts +++ b/front/src/hooks/api/useAddAnnouncement.ts @@ -1,79 +1,31 @@ -import { useEffect, useRef, useState } from 'react' +import { useCallback } from 'react' -import { API_URL } from '../../config' -import { isLiteralUnion } from '../../utils/types' -import { handleHTTPErrors } from '../../utils' - -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' -) +import { useSend } from '..' +import { composePutAnnouncementURL, processPutAnnouncement } from '../../api/putAnnouncement' +import { isPutAnnouncementResponse } from '../../api/putAnnouncement/types' +import useSendButtonCaption from '../useSendButtonCaption' const useAddAnnouncement = () => { - const [status, setStatus] = useState('Опубликовать') + const { doSend, loading, error } = useSend( + composePutAnnouncementURL(), + 'PUT', + true, + isPutAnnouncementResponse, + processPutAnnouncement, + ) - const timerIdRef = useRef() - const abortControllerRef = useRef() + const { update, ...button } = useSendButtonCaption('Опубликовать', loading, error, 'Опубликовано') - const doAdd = async (formData: FormData) => { - if (status === 'Загрузка...') { - abortControllerRef.current?.abort() - setStatus('Отменено') - timerIdRef.current = setTimeout(() => setStatus('Опубликовать'), 3000) - return - } + const doSendWithButton = useCallback(async (formData: FormData) => { + const data = await doSend({}, { + body: formData + }) + update(data) - setStatus('Загрузка...') + return data + }, [doSend, update]) - const abortController = new AbortController() - 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 } + return { doSend: doSendWithButton, button } } export default useAddAnnouncement diff --git a/front/src/hooks/useSend.ts b/front/src/hooks/useSend.ts index 8ef4505..6928512 100644 --- a/front/src/hooks/useSend.ts +++ b/front/src/hooks/useSend.ts @@ -24,6 +24,7 @@ function useSend( /** Don't use in useEffect. If you need request result, go with useFetch instead */ const doSend = useCallback(async (urlProps?: Record, params?: Omit) => { setLoading(true) + setError(null) if (abortControllerRef.current) { abortControllerRef.current.abort() diff --git a/front/src/hooks/useSendButtonCaption.ts b/front/src/hooks/useSendButtonCaption.ts new file mode 100644 index 0000000..cb209b2 --- /dev/null +++ b/front/src/hooks/useSendButtonCaption.ts @@ -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(>(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 \ No newline at end of file diff --git a/front/src/pages/AddPage.tsx b/front/src/pages/AddPage.tsx index a58de78..ac4c292 100644 --- a/front/src/pages/AddPage.tsx +++ b/front/src/pages/AddPage.tsx @@ -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 { MapContainer, TileLayer } from 'react-leaflet' import { latLng } from 'leaflet' import { ClickHandler, LocationMarker, TrashboxMarkers } from '../components' import { useAddAnnouncement, useTrashboxes } from '../hooks/api' -import { handleHTTPErrors } from '../utils' import { categories, categoryNames } from '../assets/category' import { stations, lines, lineNames } from '../assets/metro' import { fallbackError, gotError } from '../hooks/useFetch' @@ -32,25 +31,7 @@ function AddPage() { const address = useOsmAddresses(addressPosition) - useEffect(() => { - 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 { doSend, button } = useAddAnnouncement() const handleSubmit: FormEventHandler = (event) => { event.preventDefault() @@ -63,7 +44,7 @@ function AddPage() { formData.append('address', address.data || '') // if address.error formData.set('bestBy', new Date((formData.get('bestBy') as number | null) || 0).getTime().toString()) - void doAdd(formData) + void doSend(formData) } return ( @@ -151,7 +132,7 @@ function AddPage() { - + Пункт сбора мусора
{trashboxes.loading @@ -195,9 +176,7 @@ function AddPage() { )} - +