Added TypeScript for frontend

Added type definitions for components, functions, data
Added guards for network responses
fixes #8
This commit is contained in:
2023-07-12 18:59:17 +03:00
parent 8fc85e415f
commit a8b7cfbffa
52 changed files with 1616 additions and 1651 deletions

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from "react"
import { FormEventHandler, useEffect, useState } from "react"
import { Form, Button, Card } from "react-bootstrap"
import { MapContainer, TileLayer } from 'react-leaflet'
import { latLng } from "leaflet"
@ -7,21 +7,22 @@ import { ClickHandler, LocationMarker, TrashboxMarkers } from "../components"
import { useAddAnnouncement, useTrashboxes } from "../hooks/api"
import { categoryNames } from "../assets/category"
import { stations, lines } from "../assets/metro"
import { stations, lines, lineNames } from "../assets/metro"
import { isObject } from "../utils/types"
function AddPage() {
const [addressPosition, setAddressPosition] = useState(latLng(59.972, 30.3227))
const [address, setAddress] = useState('')
const { data: trashboxes, trashboxes_loading, trashboxes_error } = useTrashboxes(addressPosition)
const trashboxes = useTrashboxes(addressPosition)
const [selectedTrashbox, setSelectedTrashbox] = useState({ index: -1, category: '' })
useEffect(() => {
(async () => {
void (async () => {
try {
const res = await fetch(location.protocol + "//nominatim.openstreetmap.org/search?format=json&q=" + address)
const fetchData = await res.json()
const fetchData: unknown = await res.json()
console.log("f", fetchData)
@ -32,11 +33,15 @@ function AddPage() {
}, [address])
useEffect(() => {
(async () => {
void (async () => {
try {
const res = await fetch(location.protocol + "//nominatim.openstreetmap.org/reverse?format=json&accept-language=ru&lat=" + addressPosition.lat + "&lon=" + addressPosition.lng)
const res = await fetch(`${location.protocol}//nominatim.openstreetmap.org/reverse?format=json&accept-language=ru&lat=${addressPosition.lat}&lon=${addressPosition.lng}`)
const fetchData = await res.json()
const fetchData: unknown = await res.json()
if (!isObject<{ display_name: string }>(fetchData, { "display_name": "string" })) {
throw new Error("Malformed server response")
}
setAddress(fetchData.display_name)
@ -48,18 +53,18 @@ function AddPage() {
const { doAdd, status } = useAddAnnouncement()
const handleSubmit = (event) => {
const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault()
event.stopPropagation()
const formData = new FormData(event.target)
const formData = new FormData(event.currentTarget)
formData.append("latitude", addressPosition.lat)
formData.append("longtitude", addressPosition.lng)
formData.append("latitude", addressPosition.lat.toString())
formData.append("longtitude", addressPosition.lng.toString())
formData.append("address", address)
formData.set("bestBy", new Date(formData.get("bestBy")).getTime())
formData.set("bestBy", new Date((formData.get("bestBy") as number | null) || 0).getTime().toString())
doAdd(formData)
void doAdd(formData)
}
return (
@ -137,10 +142,10 @@ function AddPage() {
<option value="">
Укажите ближайщую станцию метро
</option>
{Object.entries(stations).map(
([line, stations]) =>
<optgroup key={line} label={lines[line]}>
{Array.from(stations).map(metro =>
{lines.map(
line =>
<optgroup key={line} label={lineNames[line]}>
{Array.from(stations[line]).map(metro =>
<option key={metro} value={metro}>{metro}</option>
)}
</optgroup>
@ -151,17 +156,17 @@ function AddPage() {
<Form.Group className="mb-3" controlId="password">
<Form.Label>Пункт сбора мусора</Form.Label>
<div className="mb-3">
{trashboxes_loading
{trashboxes.loading
? (
<div style={{ height: 400 }}>
<p>Загрузка</p>
</div>
) : (
trashboxes_error ? (
trashboxes.error ? (
<p
style={{ height: 400 }}
className="text-danger"
>{trashboxes_error}</p>
>{trashboxes.error}</p>
) : (
<MapContainer
scrollWheelZoom={false}
@ -175,7 +180,7 @@ function AddPage() {
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<TrashboxMarkers
trashboxes={trashboxes}
trashboxes={trashboxes.data}
selectTrashbox={setSelectedTrashbox}
/>
</MapContainer>
@ -185,7 +190,7 @@ function AddPage() {
</div>
{selectedTrashbox.index > -1 ? (
<p>Выбран пункт сбора мусора на {
trashboxes[selectedTrashbox.index].Address
trashboxes.data[selectedTrashbox.index].Address
} с категорией {selectedTrashbox.category}</p>
) : (
<p>Выберите пунк сбора мусора и категорию</p>

View File

@ -4,22 +4,25 @@ import Stories from 'react-insta-stories'
import { BottomNavBar, AnnouncementDetails, Filters } from '../components'
import { useStoryDimensions } from '../hooks'
import { useHomeAnnouncementList } from '../hooks/api'
import { defaultFilters } from '../utils/filters'
import puffSpinner from '../assets/puff.svg'
import { categoryGraphics } from '../assets/category'
import { Announcement } from '../hooks/api/useHomeAnnouncementList'
import { Story } from 'react-insta-stories/dist/interfaces'
function generateStories(announcements) {
function generateStories(announcements: Announcement[]): Story[] {
return announcements.map(announcement => {
return ({
id: announcement.id,
url: announcement.src || categoryGraphics.get(announcement.category),
type: announcement.src?.endsWith("mp4") ? "video" : undefined,
seeMore: ({ close }) => <AnnouncementDetails close={close} announcement={announcement} />
seeMore: ({ close }: { close: () => void }) => <AnnouncementDetails close={close} announcement={announcement} />
})
})
}
function fallbackGenerateStories(announcementsFetch) {
function fallbackGenerateStories(announcementsFetch: ReturnType<typeof useHomeAnnouncementList>) {
const stories = generateStories(announcementsFetch.data)
if (announcementsFetch.loading)
@ -34,7 +37,7 @@ function fallbackGenerateStories(announcementsFetch) {
return stories
}
const fallbackStory = (text, isError = false) => [{
const fallbackStory = (text = '', isError = false): Story[] => [{
content: ({ action }) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => { action('pause') }, [action])
@ -45,11 +48,8 @@ const fallbackStory = (text, isError = false) => [{
</div>
)
},
header: { heading: text }
}]
const defaultFilters = { userId: null, category: null, metro: null, bookedBy: null }
function HomePage() {
const { height, width } = useStoryDimensions(16 / 10)

View File

@ -1,3 +1,4 @@
import { FormEventHandler } from 'react'
import { Form, Button, Card, Tabs, Tab } from "react-bootstrap"
import { useNavigate } from "react-router-dom";
@ -7,25 +8,27 @@ import { setToken } from "../utils/auth";
function LoginPage() {
const navigate = useNavigate()
const doAuth = useAuth()
const { doAuth } = useAuth() // TODO: Add loading and error handling
const handleAuth = (newAccount) => (event) => {
const handleAuth = (newAccount: boolean): FormEventHandler<HTMLFormElement> => async (event) => {
event.preventDefault();
event.stopPropagation();
const formData = new FormData(event.currentTarget)
const data = {
email: formData.get('email'),
name: newAccount ? formData.get('name') : undefined,
password: formData.get('password')
email: formData.get('email') as string,
name: newAccount ? formData.get('name') as string : undefined,
surname: newAccount ? formData.get('surname') as string : undefined,
password: formData.get('password') as string
}
const token = "a" // doAuth(data, newAccount)
const token = import.meta.env.PROD ? await doAuth(data, newAccount) : "a"
setToken(token)
navigate("/")
if (token) {
setToken(token)
navigate("/")
}
}
return (

View File

@ -1,7 +0,0 @@
function UserPage() {
/* TODO */
return <></>
}
export default UserPage

View File

@ -0,0 +1,9 @@
import { Link } from "react-router-dom"
function UserPage() {
/* TODO */
return <h1>For Yet Go <Link to="/">Home</Link></h1>
}
export default UserPage