Code styling changes

This commit is contained in:
Dmitriy Shishkov 2023-07-13 18:38:31 +03:00
parent 9437c44054
commit 7b0ccc525c
Signed by: dm1sh
GPG Key ID: 027994B0AA357688
33 changed files with 358 additions and 387 deletions

View File

@ -23,5 +23,14 @@ module.exports = {
{ allowConstantExport: true },
],
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/quotes': [
'error',
'single',
{
'avoidEscape': true,
'allowTemplateLiterals': true
}
],
'jsx-quotes': [2, 'prefer-single'],
},
}

24
front/.gitignore vendored
View File

@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -1,11 +1,9 @@
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'
import { HomePage, AddPage, LoginPage, UserPage } from './pages'
import { WithToken } from './components'
import 'leaflet/dist/leaflet.css'
import './App.css'
function App() {
@ -13,17 +11,17 @@ function App() {
<Router>
<Routes>
<Route index element={<HomePage />} />
<Route path="/add" element={
<Route path='/add' element={
<WithToken>
<AddPage />
</WithToken>
} />
<Route path="/user" element={
<Route path='/user' element={
<WithToken>
<UserPage />
</WithToken>
} />
<Route path="/login" element={<LoginPage />} />
<Route path='/login' element={<LoginPage />} />
</Routes>
</Router>
)

View File

@ -1,39 +1,39 @@
import { isLiteralUnion } from "../utils/types"
import { isLiteralUnion } from '../utils/types'
const categories = ["PORRIDGE", "conspects", "milk", "bred", "wathing", "cloth",
"fruits_vegatables", "soup", "dinner", "conserves", "pens", "other_things"] as const
const categories = ['PORRIDGE', 'conspects', 'milk', 'bred', 'wathing', 'cloth',
'fruits_vegatables', 'soup', 'dinner', 'conserves', 'pens', 'other_things'] as const
type Category = typeof categories[number]
const isCategory = (obj: unknown): obj is Category => isLiteralUnion(obj, categories)
const categoryGraphics: Record<Category, string> = {
"PORRIDGE": "static/PORRIDGE.jpg",
"conspects": "static/conspects.jpg",
"milk": "static/milk.jpg",
"bred": "static/bred.jpg",
"wathing": "static/wathing.jpg",
"cloth": "static/cloth.jpg",
"fruits_vegatables": "static/fruits_vegatables.jpg",
"soup": "static/soup.jpg",
"dinner": "static/dinner.jpg",
"conserves": "static/conserves.jpg",
"pens": "static/pens.jpg",
"other_things": "static/other_things.jpg",
'PORRIDGE': 'static/PORRIDGE.jpg',
'conspects': 'static/conspects.jpg',
'milk': 'static/milk.jpg',
'bred': 'static/bred.jpg',
'wathing': 'static/wathing.jpg',
'cloth': 'static/cloth.jpg',
'fruits_vegatables': 'static/fruits_vegatables.jpg',
'soup': 'static/soup.jpg',
'dinner': 'static/dinner.jpg',
'conserves': 'static/conserves.jpg',
'pens': 'static/pens.jpg',
'other_things': 'static/other_things.jpg',
}
const categoryNames: Record<Category, string> = {
"PORRIDGE": "PORRIDGE",
"conspects": "Конспекты",
"milk": "Молочные продукты",
"bred": "Хлебобулочные изделия",
"wathing": "Моющие средства",
"cloth": "Одежда",
"fruits_vegatables": "Фрукты и овощи",
"soup": "Супы",
"dinner": "Ужин",
"conserves": "Консервы",
"pens": "Канцелярия",
"other_things": "Всякая всячина",
'PORRIDGE': 'PORRIDGE',
'conspects': 'Конспекты',
'milk': 'Молочные продукты',
'bred': 'Хлебобулочные изделия',
'wathing': 'Моющие средства',
'cloth': 'Одежда',
'fruits_vegatables': 'Фрукты и овощи',
'soup': 'Супы',
'dinner': 'Ужин',
'conserves': 'Консервы',
'pens': 'Канцелярия',
'other_things': 'Всякая всячина',
}
export type { Category }

View File

@ -3,102 +3,102 @@ type Lines = typeof lines[number]
const stations: Record<Lines, Set<string>> = {
red: new Set([
"Девяткино",
"Гражданский проспект",
"Академическая",
"Политехническая",
"Площадь Мужества",
"Лесная",
"Выборгская",
"Площадь Ленина",
"Чернышевская",
"Площадь Восстания",
"Владимирская",
"Пушкинская",
"Технологический институт",
"Балтийская",
"Нарвская",
"Кировский завод",
"Автово",
"Ленинский проспект",
"Проспект Ветеранов"
'Девяткино',
'Гражданский проспект',
'Академическая',
'Политехническая',
'Площадь Мужества',
'Лесная',
'Выборгская',
'Площадь Ленина',
'Чернышевская',
'Площадь Восстания',
'Владимирская',
'Пушкинская',
'Технологический институт',
'Балтийская',
'Нарвская',
'Кировский завод',
'Автово',
'Ленинский проспект',
'Проспект Ветеранов'
]),
blue: new Set([
"Парнас",
"Проспект Просвещения",
"Озерки",
"Удельная",
"Пионерская",
"Чёрная речка",
"Петроградская",
"Горьковская",
"Невский проспект",
"Сенная площадь",
"Технологический институт",
"Фрунзенская",
"Московские ворота",
"Электросила",
"Парк Победы",
"Московская",
"Звёздная",
"Купчино"
'Парнас',
'Проспект Просвещения',
'Озерки',
'Удельная',
'Пионерская',
'Чёрная речка',
'Петроградская',
'Горьковская',
'Невский проспект',
'Сенная площадь',
'Технологический институт',
'Фрунзенская',
'Московские ворота',
'Электросила',
'Парк Победы',
'Московская',
'Звёздная',
'Купчино'
]),
green: new Set([
"Приморская",
"Беговая",
"Василеостровская",
"Гостиный двор",
"Маяковская",
"Площадь Александра Невского",
"Елизаровская",
"Ломоносовская",
"Пролетарская",
"Обухово",
"Рыбацкое"
'Приморская',
'Беговая',
'Василеостровская',
'Гостиный двор',
'Маяковская',
'Площадь Александра Невского',
'Елизаровская',
'Ломоносовская',
'Пролетарская',
'Обухово',
'Рыбацкое'
]),
orange: new Set([
"Спасская",
"Достоевская",
"Лиговский проспект",
"Площадь Александра Невского",
"Новочеркасская",
"Ладожская",
"Проспект Большевиков",
"Улица Дыбенко"
'Спасская',
'Достоевская',
'Лиговский проспект',
'Площадь Александра Невского',
'Новочеркасская',
'Ладожская',
'Проспект Большевиков',
'Улица Дыбенко'
]),
violet: new Set([
"Комендантский проспект",
"Старая Деревня",
"Крестовский остров",
"Чкаловская",
"Спортивная",
"Адмиралтейская",
"Садовая",
"Звенигородская",
"Обводный канал",
"Волковская",
"Бухарестская",
"Международная",
"Проспект славы",
"Дунайскай",
"Шушары"
'Комендантский проспект',
'Старая Деревня',
'Крестовский остров',
'Чкаловская',
'Спортивная',
'Адмиралтейская',
'Садовая',
'Звенигородская',
'Обводный канал',
'Волковская',
'Бухарестская',
'Международная',
'Проспект славы',
'Дунайскай',
'Шушары'
]),
}
const colors: Record<Lines, string> = {
red: "#D6083B",
blue: "#0078C9",
green: "#009A49",
orange: "#EA7125",
violet: "#702785",
red: '#D6083B',
blue: '#0078C9',
green: '#009A49',
orange: '#EA7125',
violet: '#702785',
}
const lineNames: Record<Lines, string> = {
red: "Красная",
blue: "Синяя",
green: "Зелёная",
orange: "Оранжевая",
violet: "Фиолетовая",
red: 'Красная',
blue: 'Синяя',
green: 'Зелёная',
orange: 'Оранжевая',
violet: 'Фиолетовая',
}
const lineByName = (name: string) =>

View File

@ -16,10 +16,10 @@ function AnnouncementDetails({ close, announcement: { id, name, category, bestBy
return (
<div
className="modal"
style={{ display: 'flex', alignItems: "center", justifyContent: "center" }}
className='modal'
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}
>
<Modal.Dialog style={{ minWidth: "50vw" }}>
<Modal.Dialog style={{ minWidth: '50vw' }}>
<Modal.Header closeButton onHide={close}>
<Modal.Title>
Подробнее
@ -35,10 +35,10 @@ function AnnouncementDetails({ close, announcement: { id, name, category, bestBy
<p className='mb-3'>{description}</p>
<MapContainer style={{ width: "100%", minHeight: 300 }} center={[lat, lng]} zoom={16} >
<MapContainer style={{ width: '100%', minHeight: 300 }} center={[lat, lng]} zoom={16} >
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
/>
<Marker position={[lat, lng]}>
@ -53,7 +53,7 @@ function AnnouncementDetails({ close, announcement: { id, name, category, bestBy
<Modal.Footer>
<Button variant='success' onClick={() => void handleBook()}>
{bookStatus || "Забронировать"}
{bookStatus || 'Забронировать'}
</Button>
</Modal.Footer>
</Modal.Dialog>
@ -61,4 +61,4 @@ function AnnouncementDetails({ close, announcement: { id, name, category, bestBy
)
}
export default AnnouncementDetails
export default AnnouncementDetails

View File

@ -1,5 +1,5 @@
import { FormEventHandler } from "react"
import { Button, Form } from "react-bootstrap"
import { FormEventHandler } from 'react'
import { Button, Form } from 'react-bootstrap'
type AuthFormProps = {
register: boolean
@ -9,43 +9,43 @@ type AuthFormProps = {
}
const AuthForm = ({ handleAuth, register, loading, error }: AuthFormProps) => {
const buttonText = loading ? "Загрузка..." : (error || (register ? "Зарегистрироваться" : "Войти"))
const buttonText = loading ? 'Загрузка...' : (error || (register ? 'Зарегистрироваться' : 'Войти'))
return (
<Form onSubmit={handleAuth}>
<Form.Group className="mb-3" controlId="email">
<Form.Group className='mb-3' controlId='email'>
<Form.Label>Почта</Form.Label>
<Form.Control type="email" required />
<Form.Control type='email' required />
</Form.Group>
{register && <>
<Form.Group className="mb-3" controlId="name">
<Form.Group className='mb-3' controlId='name'>
<Form.Label>Имя</Form.Label>
<Form.Control type="text" required />
<Form.Control type='text' required />
</Form.Group>
<Form.Group className="mb-3" controlId="surname">
<Form.Group className='mb-3' controlId='surname'>
<Form.Label>Фамилия</Form.Label>
<Form.Control type="text" required />
<Form.Control type='text' required />
</Form.Group>
</>}
<Form.Group className="mb-3" controlId="password">
<Form.Group className='mb-3' controlId='password'>
<Form.Label>Пароль</Form.Label>
<Form.Control type="password" required />
<Form.Control type='password' required />
</Form.Group>
{register &&
<Form.Group className="mb-3" controlId="privacyPolicyConsent">
<Form.Check label="<a>условиями обработки персональных данных</a>">
<Form.Check.Input type="checkbox" required />
<Form.Group className='mb-3' controlId='privacyPolicyConsent'>
<Form.Check>
<Form.Check.Input type='checkbox' required />
<Form.Check.Label>
Я согласен с <a href={`${document.location.origin}/privacy_policy.pdf`} target="_blank" rel="noopener noreferrer">условиями обработки персональных данных</a>
Я согласен с <a href={`${document.location.origin}/privacy_policy.pdf`} target='_blank' rel='noopener noreferrer'>условиями обработки персональных данных</a>
</Form.Check.Label>
</Form.Check>
</Form.Group>
}
<Button variant="success" type="submit">
<Button variant='success' type='submit'>
{buttonText}
</Button>
</Form>

View File

@ -7,22 +7,22 @@ import userIcon from '../assets/userIcon.svg'
const navBarStyles: React.CSSProperties = {
backgroundColor: 'var(--bs-success)',
height: 56,
width: "100%",
width: '100%',
}
const navBarGroupStyles: React.CSSProperties = {
display: "flex",
flexDirection: "row",
height: "100%",
margin: "auto"
display: 'flex',
flexDirection: 'row',
height: '100%',
margin: 'auto'
}
const navBarElementStyles: React.CSSProperties = {
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center"
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}
type BottomNavBarProps = {
@ -36,15 +36,15 @@ function BottomNavBar({ width, toggleFilters }: BottomNavBarProps) {
<div style={{ ...navBarGroupStyles, width: width }}>
<a style={navBarElementStyles} onClick={() => toggleFilters(true)}>
<img src={filterIcon} alt="Фильтровать объявления" title='Фильтровать объявления' />
<img src={filterIcon} alt='Фильтровать объявления' title='Фильтровать объявления' />
</a>
<Link style={navBarElementStyles} to="/add" >
<img src={addIcon} alt="Опубликовать объявление" title='Опубликовать объявление' />
<Link style={navBarElementStyles} to='/add' >
<img src={addIcon} alt='Опубликовать объявление' title='Опубликовать объявление' />
</Link>
<Link style={navBarElementStyles} to={"/user"} >
<img src={userIcon} alt="Личный кабинет" title='Личный кабинет' />
<Link style={navBarElementStyles} to={'/user'} >
<img src={userIcon} alt='Личный кабинет' title='Личный кабинет' />
</Link>
</div>

View File

@ -1,6 +1,7 @@
import { useMapEvent } from "react-leaflet"
import { SetState } from "../utils/types"
import { LatLng } from "leaflet"
import { useMapEvent } from 'react-leaflet'
import { LatLng } from 'leaflet'
import { SetState } from '../utils/types'
function ClickHandler({ setPosition }: { setPosition: SetState<LatLng> }) {
const map = useMapEvent('click', (e) => {

View File

@ -1,10 +1,10 @@
import { Button, Form, Modal } from "react-bootstrap"
import { Button, Form, Modal } from 'react-bootstrap'
import { FormEventHandler } from 'react'
import { categories, categoryNames } from "../assets/category"
import { categories, categoryNames } from '../assets/category'
import { stations, lines, lineNames } from '../assets/metro'
import { FiltersType } from "../utils/filters"
import { SetState } from "../utils/types"
import { FormEventHandler } from "react"
import { FiltersType } from '../utils/filters'
import { SetState } from '../utils/types'
type FiltersProps = {
filter: FiltersType,
@ -23,8 +23,8 @@ function Filters({ filter, setFilter, filterShown, setFilterShown }: FiltersProp
setFilter(prev => ({
...prev,
category: (formData.get("category") as (FiltersType['category'] | null)) || undefined,
metro: (formData.get("metro") as (FiltersType['metro'] | null)) || undefined
category: (formData.get('category') as (FiltersType['category'] | null)) || undefined,
metro: (formData.get('metro') as (FiltersType['metro'] | null)) || undefined
}))
setFilterShown(false)
@ -40,13 +40,13 @@ function Filters({ filter, setFilter, filterShown, setFilterShown }: FiltersProp
<Modal.Body>
<Form onSubmit={handleSubmit}>
<Form.Group className="mb-3" controlId="categoryFilter">
<Form.Group className='mb-3' controlId='categoryFilter'>
<Form.Label>
Категория
</Form.Label>
<Form.Select name="category" defaultValue={filter.category || undefined}>
<option value="">
<Form.Select name='category' defaultValue={filter.category || undefined}>
<option value=''>
Выберите категорию
</option>
{categories.map(
@ -56,13 +56,13 @@ function Filters({ filter, setFilter, filterShown, setFilterShown }: FiltersProp
</Form.Select>
</Form.Group>
<Form.Group className="mb-3" controlId="metroFilter">
<Form.Group className='mb-3' controlId='metroFilter'>
<Form.Label>
Станция метро
</Form.Label>
<Form.Select name="metro" defaultValue={filter.metro || undefined}>
<option value="">
<Form.Select name='metro' defaultValue={filter.metro || undefined}>
<option value=''>
Выберите станцию метро
</option>
{lines.map(
@ -76,7 +76,7 @@ function Filters({ filter, setFilter, filterShown, setFilterShown }: FiltersProp
</Form.Select>
</Form.Group>
<Button variant="success" type="submit">
<Button variant='success' type='submit'>
Отправить
</Button>
</Form>
@ -85,4 +85,4 @@ function Filters({ filter, setFilter, filterShown, setFilterShown }: FiltersProp
)
}
export default Filters
export default Filters

View File

@ -1,6 +1,7 @@
import { Marker, Popup, useMapEvents } from "react-leaflet"
import { Marker, Popup, useMapEvents } from 'react-leaflet'
import { LatLng } from 'leaflet'
import { SetState } from "../utils/types"
import { SetState } from '../utils/types'
type LocationMarkerProps = {
address: string,
@ -32,4 +33,4 @@ const LocationMarker = ({ address, position, setPosition }: LocationMarkerProps)
)
}
export default LocationMarker
export default LocationMarker

View File

@ -1,5 +1,6 @@
import { Marker, Popup } from "react-leaflet"
import { Trashbox } from "../hooks/api/useTrashboxes"
import { Marker, Popup } from 'react-leaflet'
import { Trashbox } from '../hooks/api/useTrashboxes'
type TrashboxMarkersProps = {
trashboxes: Trashbox[],
@ -15,7 +16,7 @@ const TrashboxMarkers = ({ trashboxes, selectTrashbox }: TrashboxMarkersProps) =
<p>Тип мусора: <>
{trashbox.Categories.map((category, j) =>
<span key={trashbox.Address + category}>
<a href="#" onClick={() => selectTrashbox({ index, category })}>
<a href='#' onClick={() => selectTrashbox({ index, category })}>
{category}
</a>
{(j < trashbox.Categories.length - 1) ? ', ' : ''}

View File

@ -1,13 +1,14 @@
import { PropsWithChildren, useEffect } from "react"
import { getToken } from "../utils/auth"
import { useNavigate } from "react-router-dom"
import { PropsWithChildren, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { getToken } from '../utils/auth'
function WithToken({ children }: PropsWithChildren) {
const navigate = useNavigate()
useEffect(() => {
if (!getToken()) {
return navigate("/login")
return navigate('/login')
}
}, [navigate])

View File

@ -1,21 +1,9 @@
import AnnouncementDetails from "./AnnouncementDetails"
import BottomNavBar from "./BottomNavBar"
import Filters from "./Filters"
import LineDot from "./LineDot"
import LocationMarker from './LocationMarker'
import TrashboxMarkers from './TrashboxMarkers'
import WithToken from './WithToken'
import ClickHandler from './ClickHandler'
import AuthForm from "./AuthForm"
export {
AnnouncementDetails,
BottomNavBar,
Filters,
LineDot,
LocationMarker,
TrashboxMarkers,
WithToken,
ClickHandler,
AuthForm,
}
export { default as AnnouncementDetails } from './AnnouncementDetails'
export { default as BottomNavBar } from './BottomNavBar'
export { default as Filters } from './Filters'
export { default as LineDot } from './LineDot'
export { default as LocationMarker } from './LocationMarker'
export { default as TrashboxMarkers } from './TrashboxMarkers'
export { default as WithToken } from './WithToken'
export { default as ClickHandler } from './ClickHandler'
export { default as AuthForm } from './AuthForm'

View File

@ -1,3 +1,3 @@
const API_URL = "/api"
const API_URL = '/api'
export { API_URL }
export { API_URL }

View File

@ -1,15 +1,15 @@
import { useEffect, useRef, useState } from "react"
import { useEffect, useRef, useState } from 'react'
import { API_URL } from "../../config"
import { isLiteralUnion } from "../../utils/types"
import { handleHTTPErrors } from "../../utils"
import { API_URL } from '../../config'
import { isLiteralUnion } from '../../utils/types'
import { handleHTTPErrors } from '../../utils'
const addErrors = ["Не удалось опубликовать объявление", 'Неверный ответ от сервера', 'Неизвестная ошибка'] as const
const addErrors = ['Не удалось опубликовать объявление', 'Неверный ответ от сервера', 'Неизвестная ошибка'] as const
type AddError = typeof addErrors[number]
const isAddError = (obj: unknown): obj is AddError => isLiteralUnion(obj, addErrors)
const buttonStates = ["Опубликовать", "Загрузка...", "Опубликовано", "Отменено"] as const
const buttonStates = ['Опубликовать', 'Загрузка...', 'Опубликовано', 'Отменено'] as const
type ButtonState = typeof buttonStates[number] | AddError
type AddResponse = {
@ -21,26 +21,26 @@ const isAddResponse = (obj: unknown): obj is AddResponse =>
const useAddAnnouncement = () => {
const [status, setStatus] = useState<ButtonState>("Опубликовать")
const [status, setStatus] = useState<ButtonState>('Опубликовать')
const timerIdRef = useRef<number>()
const abortControllerRef = useRef<AbortController>()
const doAdd = async (formData: FormData) => {
if (status === "Загрузка...") {
if (status === 'Загрузка...') {
abortControllerRef.current?.abort()
setStatus("Отменено")
timerIdRef.current = setTimeout(() => setStatus("Опубликовать"), 3000)
setStatus('Отменено')
timerIdRef.current = setTimeout(() => setStatus('Опубликовать'), 3000)
return
}
setStatus("Загрузка...")
setStatus('Загрузка...')
const abortController = new AbortController()
abortControllerRef.current = abortController
try {
const res = await fetch(API_URL + "/announcement", {
const res = await fetch(API_URL + '/announcement', {
method: 'PUT',
body: formData,
signal: abortController.signal
@ -53,13 +53,13 @@ const useAddAnnouncement = () => {
if (!isAddResponse(data)) throw new Error('Неверный ответ от сервера')
if (!data.Answer) {
throw new Error("Не удалось опубликовать объявление")
throw new Error('Не удалось опубликовать объявление')
}
setStatus("Опубликовано")
setStatus('Опубликовано')
} catch (err) {
setStatus(isAddError(err) ? err : "Неизвестная ошибка")
timerIdRef.current = setTimeout(() => setStatus("Опубликовать"), 10000)
setStatus(isAddError(err) ? err : 'Неизвестная ошибка')
timerIdRef.current = setTimeout(() => setStatus('Опубликовать'), 10000)
}
}

View File

@ -1,7 +1,8 @@
import { useState } from "react"
import { API_URL } from "../../config"
import { isConst, isObject } from "../../utils/types"
import { handleHTTPErrors } from "../../utils"
import { useState } from 'react'
import { API_URL } from '../../config'
import { isConst, isObject } from '../../utils/types'
import { handleHTTPErrors } from '../../utils'
interface AuthData {
email: string,
@ -24,11 +25,11 @@ type SignUpResponse = {
const isSignUpResponse = (obj: unknown): obj is SignUpResponse => (
isObject(obj, {
"Success": isConst(true)
'Success': isConst(true)
}) ||
isObject(obj, {
"Success": isConst(false),
"Message": "string"
'Success': isConst(false),
'Message': 'string'
})
)
@ -38,8 +39,8 @@ interface LogInResponse {
}
const isLogInResponse = (obj: unknown): obj is LogInResponse => isObject(obj, {
"access_token": "string",
"token_type": isConst("bearer")
'access_token': 'string',
'token_type': isConst('bearer')
})
function useAuth() {
@ -51,8 +52,8 @@ function useAuth() {
if (newAccount) {
try {
const res = await fetch(API_URL + "/signup", {
method: "POST",
const res = await fetch(API_URL + '/signup', {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
@ -64,7 +65,7 @@ function useAuth() {
const signupData: unknown = await res.json()
if (!isSignUpResponse(signupData)) {
throw new Error("Malformed server response")
throw new Error('Malformed server response')
}
if (signupData.Success === false) {
@ -83,7 +84,7 @@ function useAuth() {
username: data.email,
password: data.password
}).toString(), {
method: "POST",
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
@ -92,7 +93,7 @@ function useAuth() {
const logInData: unknown = await res.json()
if (!isLogInResponse(logInData)) {
throw new Error("Malformed server response")
throw new Error('Malformed server response')
}
const token = logInData.access_token
@ -111,4 +112,4 @@ function useAuth() {
return { doAuth, loading, error }
}
export default useAuth
export default useAuth

View File

@ -1,20 +1,20 @@
import { useState } from "react"
import { useNavigate } from "react-router-dom"
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"
import { handleHTTPErrors } from "../../utils"
import { getToken } from '../../utils/auth'
import { API_URL } from '../../config'
import { isObject } from '../../utils/types'
import { handleHTTPErrors } from '../../utils'
type BookResponse = {
Success: boolean
}
const isBookResponse = (obj: unknown): obj is BookResponse => isObject(obj, {
"Success": "boolean"
'Success': 'boolean'
})
type BookStatus = "" | "Загрузка..." | "Забронировано" | "Ошибка бронирования"
type BookStatus = '' | 'Загрузка...' | 'Забронировано' | 'Ошибка бронирования'
function useBook(id: number) {
const navigate = useNavigate()
@ -25,7 +25,7 @@ function useBook(id: number) {
const token = getToken()
if (token) {
setStatus("Загрузка...")
setStatus('Загрузка...')
try {
@ -45,24 +45,24 @@ function useBook(id: number) {
const data: unknown = await res.json()
if (!isBookResponse(data)) {
throw new Error("Malformed server response")
throw new Error('Malformed server response')
}
if (data.Success === true) {
setStatus('Забронировано')
} else {
throw new Error("Server refused to book")
throw new Error('Server refused to book')
}
}
catch (err) {
setStatus("Ошибка бронирования")
setStatus('Ошибка бронирования')
if (import.meta.env.DEV) {
console.log(err)
}
}
} else {
return navigate("/login")
return navigate('/login')
}
}

View File

@ -1,11 +1,11 @@
import { useEffect, useRef, useState } from "react"
import { useEffect, useRef, useState } from 'react'
import { handleHTTPErrors, isAborted } from '../../utils'
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 [error, setError] = useState('')
const abortControllerRef = useRef<AbortController>()
@ -25,7 +25,7 @@ const useFetch = <T>(url: string, params: RequestInit | undefined, initialData:
})
.then(data => {
if (!dataGuard(data)) {
throw new Error("Неверный ответ от сервера")
throw new Error('Неверный ответ от сервера')
}
setData(data)
@ -33,7 +33,7 @@ const useFetch = <T>(url: string, params: RequestInit | undefined, initialData:
})
.catch(err => {
if (err instanceof Error && !isAborted(err)) {
setError("Ошибка сети")
setError('Ошибка сети')
}
setLoading(false)

View File

@ -1,7 +1,7 @@
import useFetch from './useFetch'
import { API_URL } from '../../config'
import { FiltersType, filterNames } from '../../utils/filters'
import { isArrayOf, isObject } from '../../utils/types'
import { API_URL } from '../../config'
import { Category, isCategory } from '../../assets/category'
const initialAnnouncements = { list_of_announcements: [], Success: true }
@ -12,8 +12,8 @@ type AnnouncementsListResponse = {
}
const isAnnouncementsListResponse = (obj: unknown): obj is AnnouncementsListResponse => isObject(obj, {
"list_of_announcements": obj => isArrayOf<AnnouncementResponse>(obj, isAnnouncementResponse),
"Success": "boolean"
'list_of_announcements': obj => isArrayOf<AnnouncementResponse>(obj, isAnnouncementResponse),
'Success': 'boolean'
})
type AnnouncementResponse = {
@ -33,19 +33,19 @@ type AnnouncementResponse = {
}
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"
'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 = {

View File

@ -1,9 +1,9 @@
import { LatLng } from "leaflet"
import { LatLng } from 'leaflet'
import { API_URL } from "../../config"
import { isArrayOf, isObject } from "../../utils/types"
import useFetch from "./useFetch"
import { isString } from "../../utils/types"
import { API_URL } from '../../config'
import { isArrayOf, isObject } from '../../utils/types'
import useFetch from './useFetch'
import { isString } from '../../utils/types'
type Trashbox = {
Lat: number,
@ -13,15 +13,15 @@ type Trashbox = {
}
const isTrashbox = (obj: unknown): obj is Trashbox => isObject(obj, {
"Lat": "number",
"Lng": "number",
"Address": "string",
"Categories": obj => isArrayOf<string>(obj, isString)
'Lat': 'number',
'Lng': 'number',
'Address': 'string',
'Categories': obj => isArrayOf<string>(obj, isString)
})
const useTrashboxes = (position: LatLng) => {
return useFetch(
API_URL + "/trashbox?" + new URLSearchParams({
API_URL + '/trashbox?' + new URLSearchParams({
lat: position.lat.toString(),
lng: position.lng.toString()
}).toString(),
@ -32,4 +32,4 @@ const useTrashboxes = (position: LatLng) => {
}
export type { Trashbox }
export default useTrashboxes
export default useTrashboxes

View File

@ -1,5 +1 @@
import useStoryDimensions from "./useStoryDimensions"
export {
useStoryDimensions,
}
export { default as useStoryDimensions } from './useStoryDimensions'

View File

@ -17,8 +17,8 @@ function useStoryDimensions(maxRatio = 16/9) {
setWindowDimensions(getWindowDimensions());
}
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
const height = windowDimensions.height - 56
@ -31,4 +31,4 @@ function useStoryDimensions(maxRatio = 16/9) {
}
}
export default useStoryDimensions
export default useStoryDimensions

View File

@ -2,4 +2,4 @@
padding: 0;
margin: 0;
list-style: none;
}
}

View File

@ -1,5 +1,6 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'

View File

@ -1,15 +1,14 @@
import { FormEventHandler, useEffect, useState } from "react"
import { Form, Button, Card } from "react-bootstrap"
import { FormEventHandler, useEffect, useState } from 'react'
import { Form, Button, Card } from 'react-bootstrap'
import { MapContainer, TileLayer } from 'react-leaflet'
import { latLng } from "leaflet"
import { latLng } from 'leaflet'
import { ClickHandler, LocationMarker, TrashboxMarkers } from "../components"
import { useAddAnnouncement, useTrashboxes } from "../hooks/api"
import { categories, categoryNames } from "../assets/category"
import { stations, lines, lineNames } from "../assets/metro"
import { isObject } from "../utils/types"
import { handleHTTPErrors } from "../utils"
import { ClickHandler, LocationMarker, TrashboxMarkers } from '../components'
import { useAddAnnouncement, useTrashboxes } from '../hooks/api'
import { isObject } from '../utils/types'
import { handleHTTPErrors } from '../utils'
import { categories, categoryNames } from '../assets/category'
import { stations, lines, lineNames } from '../assets/metro'
function AddPage() {
const [addressPosition, setAddressPosition] = useState(latLng(59.972, 30.3227))
@ -21,13 +20,13 @@ function AddPage() {
useEffect(() => {
void (async () => {
try {
const res = await fetch(location.protocol + "//nominatim.openstreetmap.org/search?format=json&q=" + address)
const res = await fetch(location.protocol + '//nominatim.openstreetmap.org/search?format=json&q=' + address)
handleHTTPErrors(res)
const fetchData: unknown = await res.json()
console.log("f", fetchData)
console.log('f', fetchData)
} catch (err) {
console.error(err)
@ -44,8 +43,8 @@ function AddPage() {
const fetchData: unknown = await res.json()
if (!isObject<{ display_name: string }>(fetchData, { "display_name": "string" })) {
throw new Error("Malformed server response")
if (!isObject<{ display_name: string }>(fetchData, { 'display_name': 'string' })) {
throw new Error('Malformed server response')
}
setAddress(fetchData.display_name)
@ -64,27 +63,27 @@ function AddPage() {
const formData = new FormData(event.currentTarget)
formData.append("latitude", addressPosition.lat.toString())
formData.append("longtitude", addressPosition.lng.toString())
formData.append("address", address)
formData.set("bestBy", new Date((formData.get("bestBy") as number | null) || 0).getTime().toString())
formData.append('latitude', addressPosition.lat.toString())
formData.append('longtitude', addressPosition.lng.toString())
formData.append('address', address)
formData.set('bestBy', new Date((formData.get('bestBy') as number | null) || 0).getTime().toString())
void doAdd(formData)
}
return (
<Card className="m-4" style={{ height: 'calc(100vh - 3rem)' }}>
<Card.Body style={{ overflowY: "auto" }} >
<Card className='m-4' style={{ height: 'calc(100vh - 3rem)' }}>
<Card.Body style={{ overflowY: 'auto' }} >
<Form onSubmit={handleSubmit}>
<Form.Group className="mb-3" controlId="name">
<Form.Group className='mb-3' controlId='name'>
<Form.Label>Заголовок объявления</Form.Label>
<Form.Control type="text" required name="name" />
<Form.Control type='text' required name='name' />
</Form.Group>
<Form.Group className="mb-3" controlId="category">
<Form.Group className='mb-3' controlId='category'>
<Form.Label>Категория</Form.Label>
<Form.Select required name="category">
<option value="" hidden>
<Form.Select required name='category'>
<option value='' hidden>
Выберите категорию
</option>
{categories.map(category =>
@ -93,23 +92,23 @@ function AddPage() {
</Form.Select>
</Form.Group>
<Form.Group className="mb-3" controlId="bestBy">
<Form.Group className='mb-3' controlId='bestBy'>
<Form.Label>Срок годности</Form.Label>
<Form.Control type="date" required name="bestBy" />
<Form.Control type='date' required name='bestBy' />
</Form.Group>
<Form.Group className="mb-3" controlId="address">
<Form.Group className='mb-3' controlId='address'>
<Form.Label>Адрес выдачи</Form.Label>
<div className="mb-3">
<div className='mb-3'>
<MapContainer
scrollWheelZoom={false}
style={{ width: "100%", height: 400 }}
style={{ width: '100%', height: 400 }}
center={addressPosition}
zoom={13}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
/>
<LocationMarker
address={address}
@ -124,26 +123,26 @@ function AddPage() {
<p>Адрес: {address}</p>
</Form.Group>
<Form.Group className="mb-3" controlId="description">
<Form.Group className='mb-3' controlId='description'>
<Form.Label>Описание</Form.Label>
<Form.Control as="textarea" name="description" rows={3} placeholder="Укажите свои контакты, а так же, когда вам будет удобно передать продукт" />
<Form.Control as='textarea' name='description' rows={3} placeholder='Укажите свои контакты, а так же, когда вам будет удобно передать продукт' />
</Form.Group>
<Form.Group className="mb-3" controlId="src">
<Form.Group className='mb-3' controlId='src'>
<Form.Label>Иллюстрация (фото или видео)</Form.Label>
<Form.Control
type="file"
name="src"
accept="video/mp4,video/mkv, video/x-m4v,video/*, image/*"
capture="environment"
type='file'
name='src'
accept='video/mp4,video/mkv, video/x-m4v,video/*, image/*'
capture='environment'
/>
</Form.Group>
<Form.Group className="mb-3" controlId="metro">
<Form.Group className='mb-3' controlId='metro'>
<Form.Label>
Станция метро
</Form.Label>
<Form.Select name="metro">
<option value="">
<Form.Select name='metro'>
<option value=''>
Укажите ближайщую станцию метро
</option>
{lines.map(
@ -157,9 +156,9 @@ function AddPage() {
</Form.Select>
</Form.Group>
<Form.Group className="mb-3" controlId="password">
<Form.Group className='mb-3' controlId='password'>
<Form.Label>Пункт сбора мусора</Form.Label>
<div className="mb-3">
<div className='mb-3'>
{trashboxes.loading
? (
<div style={{ height: 400 }}>
@ -169,19 +168,19 @@ function AddPage() {
trashboxes.error ? (
<p
style={{ height: 400 }}
className="text-danger"
className='text-danger'
>{trashboxes.error}</p>
) : (
<MapContainer
scrollWheelZoom={false}
style={{ width: "100%", height: 400 }}
style={{ width: '100%', height: 400 }}
center={addressPosition}
zoom={13}
className=""
className=''
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
/>
<TrashboxMarkers
trashboxes={trashboxes.data}
@ -201,7 +200,7 @@ function AddPage() {
)}
</Form.Group>
<Button variant="success" type="submit">
<Button variant='success' type='submit'>
{status}
</Button>
</Form>

View File

@ -1,22 +1,21 @@
import { useEffect, useState } from 'react'
import Stories from 'react-insta-stories'
import { Story } from 'react-insta-stories/dist/interfaces'
import { BottomNavBar, AnnouncementDetails, Filters } from '../components'
import { useStoryDimensions } from '../hooks'
import { useHomeAnnouncementList } from '../hooks/api'
import { defaultFilters } from '../utils/filters'
import { Announcement } from '../hooks/api/useHomeAnnouncementList'
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: Announcement[]): Story[] {
return announcements.map(announcement => {
return ({
id: announcement.id,
url: announcement.src || categoryGraphics[announcement.category],
type: announcement.src?.endsWith("mp4") ? "video" : undefined,
type: announcement.src?.endsWith('mp4') ? 'video' : undefined,
seeMore: ({ close }: { close: () => void }) => <AnnouncementDetails close={close} announcement={announcement} />
})
})
@ -32,7 +31,7 @@ function fallbackGenerateStories(announcementsFetch: ReturnType<typeof useHomeAn
return fallbackStory(announcementsFetch.error, true)
if (stories.length === 0)
return fallbackStory("Здесь пока пусто")
return fallbackStory('Здесь пока пусто')
return stories
}
@ -43,7 +42,7 @@ const fallbackStory = (text = '', isError = false): Story[] => [{
useEffect(() => { action('pause') }, [action])
return (
<div style={{ margin: 'auto' }} className={isError ? "text-danger" : ''}>
<div style={{ margin: 'auto' }} className={isError ? 'text-danger' : ''}>
{text || <img src={puffSpinner} />}
</div>
)
@ -62,7 +61,7 @@ function HomePage() {
return (<>
<Filters filter={filter} setFilter={setFilter} filterShown={filterShown} setFilterShown={setFilterShown} />
<div style={{ display: "flex", justifyContent: "center", backgroundColor: "rgb(17, 17, 17)" }}>
<div style={{ display: 'flex', justifyContent: 'center', backgroundColor: 'rgb(17, 17, 17)' }}>
<Stories
stories={stories}
defaultInterval={11000}

View File

@ -1,9 +1,9 @@
import { FormEventHandler } from 'react'
import { Card, Tabs, Tab } from "react-bootstrap"
import { useNavigate } from "react-router-dom";
import { Card, Tabs, Tab } from 'react-bootstrap'
import { useNavigate } from 'react-router-dom';
import { useAuth } from "../hooks/api";
import { setToken } from "../utils/auth";
import { useAuth } from '../hooks/api';
import { setToken } from '../utils/auth';
import { AuthForm } from '../components';
function LoginPage() {
@ -24,22 +24,22 @@ function LoginPage() {
password: formData.get('password') as string
}
const token = import.meta.env.PROD ? await doAuth(data, newAccount) : "a"
const token = import.meta.env.PROD ? await doAuth(data, newAccount) : 'a'
if (token) {
setToken(token)
navigate("/")
navigate('/')
}
}
return (
<Card className="m-4">
<Card className='m-4'>
<Card.Body>
<Tabs defaultActiveKey="register" fill justify className="mb-3">
<Tab eventKey="register" title="Регистрация">
<Tabs defaultActiveKey='register' fill justify className='mb-3'>
<Tab eventKey='register' title='Регистрация'>
<AuthForm handleAuth={handleAuth(true)} register={true} loading={loading} error={error} />
</Tab>
<Tab eventKey="login" title="Вход">
<Tab eventKey='login' title='Вход'>
<AuthForm handleAuth={handleAuth(false)} register={false} loading={loading} error={error} />
</Tab>
</Tabs>
@ -48,4 +48,4 @@ function LoginPage() {
)
}
export default LoginPage
export default LoginPage

View File

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

View File

@ -1,5 +1,5 @@
const getToken = () => {
const token = localStorage.getItem("Token")
const token = localStorage.getItem('Token')
/* check expirity */
@ -7,7 +7,7 @@ const getToken = () => {
}
const setToken = (token: string) => {
localStorage.setItem("Token", token)
localStorage.setItem('Token', token)
}
export { getToken, setToken }
export { getToken, setToken }

View File

@ -1,6 +1,6 @@
import { Announcement } from "../hooks/api/useHomeAnnouncementList"
import { Announcement } from '../hooks/api/useHomeAnnouncementList'
const filterNames = ["userId", "category", "metro", "bookedBy"] as const
const filterNames = ['userId', 'category', 'metro', 'bookedBy'] as const
type FilterNames = typeof filterNames[number]
type FiltersType = Partial<Pick<Announcement, FilterNames>>

View File

@ -4,11 +4,11 @@ const handleHTTPErrors = (res: Response) => {
if (!res.ok) {
switch (res.status) {
case 401:
throw new Error("Ошибка авторизации")
throw new Error('Ошибка авторизации')
case 404:
throw new Error("Объект не найден")
throw new Error('Объект не найден')
default: {
throw new Error("Ошибка ответа от сервера")
throw new Error('Ошибка ответа от сервера')
}
}
}

View File

@ -5,7 +5,7 @@ const isRecord = <K extends string | number | symbol>(obj: unknown): obj is Reco
obj !== null
)
type Primitive = "bigint" | "boolean" | "function" | "number" | "object" | "string" | "symbol" | "undefined"
type Primitive = 'bigint' | 'boolean' | 'function' | 'number' | 'object' | 'string' | 'symbol' | 'undefined'
type PropertyGuard = Primitive | `${Primitive}?` | ((obj: unknown) => boolean)