forked from polka_billy/porridger
Added TypeScript for frontend
Added type definitions for components, functions, data Added guards for network responses fixes #8
This commit is contained in:
@ -1,54 +0,0 @@
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { API_URL } from "../../config"
|
||||
|
||||
const useAddAnnouncement = () => {
|
||||
const [status, setStatus] = useState("Опубликовать")
|
||||
|
||||
const timerIdRef = useRef()
|
||||
const abortControllerRef = useRef()
|
||||
|
||||
const doAdd = async (formData) => {
|
||||
if (status === "Загрузка") {
|
||||
abortControllerRef.current?.abort()
|
||||
setStatus("Отменено")
|
||||
timerIdRef.current = setTimeout(() => setStatus("Опубликовать"), 3000)
|
||||
return
|
||||
}
|
||||
|
||||
setStatus("Загрузка")
|
||||
|
||||
const abortController = new AbortController()
|
||||
abortControllerRef.current = abortController
|
||||
|
||||
try {
|
||||
const res = await fetch(API_URL + "/announcement", {
|
||||
method: 'PUT',
|
||||
body: formData,
|
||||
signal: abortController.signal
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
|
||||
if (!data.Answer) {
|
||||
throw new Error("Не удалось опубликовать объявление")
|
||||
}
|
||||
setStatus("Опубликовано")
|
||||
|
||||
} catch (err) {
|
||||
setStatus(err.message ?? err)
|
||||
timerIdRef.current = setTimeout(() => setStatus("Опубликовать"), 10000)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const abortController = abortControllerRef.current
|
||||
return () => {
|
||||
clearTimeout(timerIdRef.current)
|
||||
abortController?.abort()
|
||||
}
|
||||
})
|
||||
|
||||
return {doAdd, status}
|
||||
}
|
||||
|
||||
export default useAddAnnouncement
|
74
front/src/hooks/api/useAddAnnouncement.ts
Normal file
74
front/src/hooks/api/useAddAnnouncement.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
|
||||
import { API_URL } from "../../config"
|
||||
import { isLiteralUnion } from "../../utils/types"
|
||||
|
||||
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 [status, setStatus] = useState<ButtonState>("Опубликовать")
|
||||
|
||||
const timerIdRef = useRef<number>()
|
||||
const abortControllerRef = useRef<AbortController>()
|
||||
|
||||
const doAdd = async (formData: FormData) => {
|
||||
if (status === "Загрузка") {
|
||||
abortControllerRef.current?.abort()
|
||||
setStatus("Отменено")
|
||||
timerIdRef.current = setTimeout(() => setStatus("Опубликовать"), 3000)
|
||||
return
|
||||
}
|
||||
|
||||
setStatus("Загрузка")
|
||||
|
||||
const abortController = new AbortController()
|
||||
abortControllerRef.current = abortController
|
||||
|
||||
try {
|
||||
const res = await fetch(API_URL + "/announcement", {
|
||||
method: 'PUT',
|
||||
body: formData,
|
||||
signal: abortController.signal
|
||||
})
|
||||
|
||||
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
|
@ -1,61 +0,0 @@
|
||||
import { useState } from "react"
|
||||
import { API_URL } from "../../config"
|
||||
|
||||
function useAuth() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const doAuth = async (data, newAccount) => {
|
||||
setLoading(true)
|
||||
|
||||
if (newAccount) {
|
||||
try {
|
||||
const res = await fetch(API_URL + "/signup", {
|
||||
method: "POST",
|
||||
body: data,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
const signupData = await res.json()
|
||||
|
||||
if (signupData.Success === false) {
|
||||
throw new Error(signupData.Message)
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
setLoading(false)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const res = fetch(API_URL + '/auth/token' + new URLSearchParams({
|
||||
username: data.email,
|
||||
password: data.password
|
||||
}), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
|
||||
const loginData = await res.json()
|
||||
|
||||
const token = loginData.access_token
|
||||
|
||||
setError('')
|
||||
setLoading(false)
|
||||
|
||||
return token
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
setLoading(false)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return { doAuth, loading, error }
|
||||
}
|
||||
|
||||
export default useAuth
|
110
front/src/hooks/api/useAuth.ts
Normal file
110
front/src/hooks/api/useAuth.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import { useState } from "react"
|
||||
import { API_URL } from "../../config"
|
||||
import { isConst, isObject } from "../../utils/types"
|
||||
|
||||
interface AuthData {
|
||||
email: string,
|
||||
password: string,
|
||||
}
|
||||
|
||||
// interface LoginData extends AuthData { }
|
||||
|
||||
// interface SignUpData extends AuthData {
|
||||
// name: string,
|
||||
// surname: string
|
||||
// }
|
||||
|
||||
type SignUpResponse = {
|
||||
Success: true
|
||||
} | {
|
||||
Success: false,
|
||||
Message: string
|
||||
}
|
||||
|
||||
const isSignUpResponse = (obj: unknown): obj is SignUpResponse => (
|
||||
isObject(obj, {
|
||||
"Success": isConst(true)
|
||||
}) ||
|
||||
isObject(obj, {
|
||||
"Success": isConst(false),
|
||||
"Message": "string"
|
||||
})
|
||||
)
|
||||
|
||||
interface LogInResponse {
|
||||
access_token: string,
|
||||
token_type: 'bearer'
|
||||
}
|
||||
|
||||
const isLogInResponse = (obj: unknown): obj is LogInResponse => isObject(obj, {
|
||||
"access_token": "string",
|
||||
"token_type": isConst("bearer")
|
||||
})
|
||||
|
||||
function useAuth() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const doAuth = async (data: AuthData, newAccount: boolean) => {
|
||||
setLoading(true)
|
||||
|
||||
if (newAccount) {
|
||||
try {
|
||||
const res = await fetch(API_URL + "/signup", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
const signupData: unknown = await res.json()
|
||||
|
||||
if (!isSignUpResponse(signupData)) {
|
||||
throw new Error("Malformed server response")
|
||||
}
|
||||
|
||||
if (signupData.Success === false) {
|
||||
throw new Error(signupData.Message)
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : err as string)
|
||||
setLoading(false)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(API_URL + '/auth/token' + new URLSearchParams({
|
||||
username: data.email,
|
||||
password: data.password
|
||||
}).toString(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
|
||||
const logInData: unknown = await res.json()
|
||||
|
||||
if (!isLogInResponse(logInData)) {
|
||||
throw new Error("Malformed server response")
|
||||
}
|
||||
|
||||
const token = logInData.access_token
|
||||
|
||||
setError('')
|
||||
setLoading(false)
|
||||
|
||||
return token
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : err as string)
|
||||
setLoading(false)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return { doAuth, loading, error }
|
||||
}
|
||||
|
||||
export default useAuth
|
@ -1,42 +0,0 @@
|
||||
import { useState } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
|
||||
import { getToken } from "../../utils/auth"
|
||||
import { API_URL } from "../../config"
|
||||
|
||||
function useBook(id) {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [status, setStatus] = useState('')
|
||||
|
||||
const handleBook = () => {
|
||||
const token = getToken()
|
||||
|
||||
if (token) {
|
||||
setStatus("Загрузка")
|
||||
|
||||
fetch(API_URL + '/book', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
id: id
|
||||
}),
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(res => res.json()).then(data => {
|
||||
if (data.Success === true) {
|
||||
setStatus('Забронировано')
|
||||
} else {
|
||||
setStatus("Ошибка бронирования")
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return navigate("/login")
|
||||
}
|
||||
}
|
||||
|
||||
return { handleBook, status }
|
||||
}
|
||||
|
||||
export default useBook
|
69
front/src/hooks/api/useBook.ts
Normal file
69
front/src/hooks/api/useBook.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { useState } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
|
||||
import { getToken } from "../../utils/auth"
|
||||
import { API_URL } from "../../config"
|
||||
import { isObject } from "../../utils/types"
|
||||
|
||||
type BookResponse = {
|
||||
Success: boolean
|
||||
}
|
||||
|
||||
const isBookResponse = (obj: unknown): obj is BookResponse => isObject(obj, {
|
||||
"Success": "boolean"
|
||||
})
|
||||
|
||||
type BookStatus = "" | "Загрузка" | "Забронировано" | "Ошибка бронирования"
|
||||
|
||||
function useBook(id: number) {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [status, setStatus] = useState<BookStatus>('')
|
||||
|
||||
const handleBook = async () => {
|
||||
const token = getToken()
|
||||
|
||||
if (token) {
|
||||
setStatus("Загрузка")
|
||||
|
||||
try {
|
||||
|
||||
const res = await fetch(API_URL + '/book', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
id: id
|
||||
}),
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
const data: unknown = await res.json()
|
||||
|
||||
if (!isBookResponse(data)) {
|
||||
throw new Error("Malformed server response")
|
||||
}
|
||||
|
||||
if (data.Success === true) {
|
||||
setStatus('Забронировано')
|
||||
} else {
|
||||
throw new Error("Server refused to book")
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
setStatus("Ошибка бронирования")
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return navigate("/login")
|
||||
}
|
||||
}
|
||||
|
||||
return { handleBook, status }
|
||||
}
|
||||
|
||||
export default useBook
|
@ -1,12 +1,12 @@
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { isAborted } from '../../utils'
|
||||
|
||||
const useFetch = (url, params, initialData) => {
|
||||
const useFetch = <T>(url: string, params: RequestInit | undefined, initialData: T, dataGuard: (obj: unknown) => obj is T) => {
|
||||
const [data, setData] = useState(initialData)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState("")
|
||||
|
||||
const abortControllerRef = useRef(null)
|
||||
const abortControllerRef = useRef<AbortController>()
|
||||
|
||||
useEffect(() => {
|
||||
if (abortControllerRef.current) {
|
||||
@ -33,11 +33,15 @@ const useFetch = (url, params, initialData) => {
|
||||
return res.json()
|
||||
})
|
||||
.then(data => {
|
||||
if (!dataGuard(data)) {
|
||||
throw new Error("Неверный ответ от сервера")
|
||||
}
|
||||
|
||||
setData(data)
|
||||
setLoading(false)
|
||||
})
|
||||
.catch(err => {
|
||||
if (!isAborted(err)) {
|
||||
if (err instanceof Error && !isAborted(err)) {
|
||||
setError("Ошибка сети")
|
||||
}
|
||||
|
||||
@ -48,10 +52,13 @@ const useFetch = (url, params, initialData) => {
|
||||
}
|
||||
})
|
||||
|
||||
return () => abortControllerRef.current.abort()
|
||||
}, [url, params])
|
||||
return () => abortControllerRef.current?.abort()
|
||||
}, [url, params, dataGuard])
|
||||
|
||||
return { data, loading, error, abort: abortControllerRef.current?.abort }
|
||||
return {
|
||||
data, loading, error,
|
||||
abort: abortControllerRef.current?.abort.bind(abortControllerRef.current)
|
||||
}
|
||||
}
|
||||
|
||||
export default useFetch
|
@ -1,26 +0,0 @@
|
||||
import useFetch from './useFetch'
|
||||
import { API_URL } from '../../config'
|
||||
import { removeNull } from '../../utils'
|
||||
|
||||
const initialAnnouncements = { list_of_announcements: [], Success: true }
|
||||
|
||||
const useHomeAnnouncementList = (filters) => {
|
||||
const { data, loading, error } = useFetch(
|
||||
API_URL + '/announcements?' + new URLSearchParams(removeNull(filters)),
|
||||
null,
|
||||
initialAnnouncements
|
||||
)
|
||||
|
||||
const annList = data.list_of_announcements
|
||||
|
||||
const res = annList.map(ann => ({
|
||||
...ann,
|
||||
lat: ann.latitude,
|
||||
lng: ann.longtitude,
|
||||
bestBy: ann.best_by
|
||||
}))
|
||||
|
||||
return { data: error ? [] : res, loading, error }
|
||||
}
|
||||
|
||||
export default useHomeAnnouncementList
|
96
front/src/hooks/api/useHomeAnnouncementList.ts
Normal file
96
front/src/hooks/api/useHomeAnnouncementList.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import useFetch from './useFetch'
|
||||
import { API_URL } from '../../config'
|
||||
import { FiltersType, filterNames } from '../../utils/filters'
|
||||
import { isArrayOf, isObject } from '../../utils/types'
|
||||
import { Category, isCategory } from '../../assets/category'
|
||||
|
||||
const initialAnnouncements = { list_of_announcements: [], Success: true }
|
||||
|
||||
type AnnouncementsListResponse = {
|
||||
list_of_announcements: AnnouncementResponse[],
|
||||
Success: boolean
|
||||
}
|
||||
|
||||
const isAnnouncementsListResponse = (obj: unknown): obj is AnnouncementsListResponse => isObject(obj, {
|
||||
"list_of_announcements": obj => isArrayOf<AnnouncementResponse>(obj, isAnnouncementResponse),
|
||||
"Success": "boolean"
|
||||
})
|
||||
|
||||
type AnnouncementResponse = {
|
||||
id: number,
|
||||
user_id: number,
|
||||
name: string,
|
||||
category: Category,
|
||||
best_by: number,
|
||||
address: string,
|
||||
longtitude: number,
|
||||
latitude: number,
|
||||
description: string,
|
||||
src: string | null,
|
||||
metro: string,
|
||||
trashId: number | null,
|
||||
booked_by: number
|
||||
}
|
||||
|
||||
const isAnnouncementResponse = (obj: unknown): obj is AnnouncementResponse => isObject(obj, {
|
||||
"id": "number",
|
||||
"user_id": "number",
|
||||
"name": "string",
|
||||
"category": isCategory,
|
||||
"best_by": "number",
|
||||
"address": "string",
|
||||
"longtitude": "number",
|
||||
"latitude": "number",
|
||||
"description": "string",
|
||||
"src": "string?",
|
||||
"metro": "string",
|
||||
"trashId": "number?",
|
||||
"booked_by": "number"
|
||||
})
|
||||
|
||||
type Announcement = {
|
||||
id: number,
|
||||
userId: number,
|
||||
name: string,
|
||||
category: Category,
|
||||
bestBy: number,
|
||||
address: string,
|
||||
lng: number,
|
||||
lat: number,
|
||||
description: string | null,
|
||||
src: string | null,
|
||||
metro: string,
|
||||
trashId: number | null,
|
||||
bookedBy: number
|
||||
}
|
||||
|
||||
const composeFilters = (filters: FiltersType) => Object.fromEntries(
|
||||
filterNames.map(
|
||||
fName => [fName, filters[fName]?.toString()]
|
||||
).filter((p): p is [string, string] => typeof p[1] !== 'undefined')
|
||||
)
|
||||
|
||||
const useHomeAnnouncementList = (filters: FiltersType) => {
|
||||
const { data, loading, error } = useFetch(
|
||||
API_URL + '/announcements?' + new URLSearchParams(composeFilters(filters)).toString(),
|
||||
undefined,
|
||||
initialAnnouncements,
|
||||
isAnnouncementsListResponse
|
||||
)
|
||||
|
||||
const annList = data.list_of_announcements
|
||||
|
||||
const res: Announcement[] = annList.map(ann => ({
|
||||
...ann,
|
||||
lat: ann.latitude,
|
||||
lng: ann.longtitude,
|
||||
bestBy: ann.best_by,
|
||||
bookedBy: ann.booked_by,
|
||||
userId: ann.user_id
|
||||
}))
|
||||
|
||||
return { data: error ? [] : res, loading, error }
|
||||
}
|
||||
|
||||
export type { Announcement, AnnouncementsListResponse }
|
||||
export default useHomeAnnouncementList
|
@ -1,12 +0,0 @@
|
||||
import { API_URL } from "../../config"
|
||||
import useFetch from "./useFetch"
|
||||
|
||||
const useTrashboxes = (position) => {
|
||||
return useFetch(
|
||||
API_URL + "/trashbox?" + new URLSearchParams({ lat: position.lat, lng: position.lng }),
|
||||
undefined,
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
export default useTrashboxes
|
35
front/src/hooks/api/useTrashboxes.ts
Normal file
35
front/src/hooks/api/useTrashboxes.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { LatLng } from "leaflet"
|
||||
|
||||
import { API_URL } from "../../config"
|
||||
import { isArrayOf, isObject } from "../../utils/types"
|
||||
import useFetch from "./useFetch"
|
||||
import { isString } from "../../utils/types"
|
||||
|
||||
type Trashbox = {
|
||||
Lat: number,
|
||||
Lng: number,
|
||||
Address: string,
|
||||
Categories: string[]
|
||||
}
|
||||
|
||||
const isTrashbox = (obj: unknown): obj is Trashbox => isObject(obj, {
|
||||
"Lat": "number",
|
||||
"Lng": "number",
|
||||
"Address": "string",
|
||||
"Categories": obj => isArrayOf<string>(obj, isString)
|
||||
})
|
||||
|
||||
const useTrashboxes = (position: LatLng) => {
|
||||
return useFetch(
|
||||
API_URL + "/trashbox?" + new URLSearchParams({
|
||||
lat: position.lat.toString(),
|
||||
lng: position.lng.toString()
|
||||
}).toString(),
|
||||
undefined,
|
||||
[],
|
||||
(obj): obj is Trashbox[] => isArrayOf(obj, isTrashbox)
|
||||
)
|
||||
}
|
||||
|
||||
export type { Trashbox }
|
||||
export default useTrashboxes
|
Reference in New Issue
Block a user