This commit is contained in:
DmitryGantimurov 2023-07-16 16:15:05 +03:00
commit 349d40daa4
92 changed files with 3020 additions and 2500 deletions

View File

@ -1,219 +1,440 @@
#подключение библиотек <<<<<<< HEAD
from fastapi import FastAPI, Response, Path, Depends, Body, Form, Query, status, HTTPException, APIRouter, UploadFile, File #подключение библиотек
from fastapi.responses import HTMLResponse, FileResponse, JSONResponse, RedirectResponse from fastapi import FastAPI, Response, Path, Depends, Body, Form, Query, status, HTTPException, APIRouter, UploadFile, File
from fastapi.staticfiles import StaticFiles from fastapi.responses import HTMLResponse, FileResponse, JSONResponse, RedirectResponse
from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer
from fastapi.requests import Request from fastapi.templating import Jinja2Templates
from fastapi.requests import Request
from pydantic import json
from pydantic import json
from starlette.staticfiles import StaticFiles
from starlette.staticfiles import StaticFiles
import requests
from uuid import uuid4 import requests
from uuid import uuid4
import ast
import pathlib import ast
import shutil import pathlib
import os import shutil
import os
from .utils import *
from .models import Announcement, Trashbox, UserDatabase, Base from .utils import *
from .db import engine, SessionLocal from .models import Announcement, Trashbox, UserDatabase, Base
from .db import engine, SessionLocal
from . import schema
from . import schema
Base.metadata.create_all(bind=engine)
Base.metadata.create_all(bind=engine)
db = SessionLocal()
db = SessionLocal()
app = FastAPI()
app = FastAPI()
templates = Jinja2Templates(directory="./front/dist")
templates = Jinja2Templates(directory="./front/dist")
app.mount("/static", StaticFiles(directory = "./front/dist"))
# app.mount("/uploads", StaticFiles(directory = "./uploads")) app.mount("/static", StaticFiles(directory = "./front/dist"))
app.mount("/uploads", StaticFiles(directory = "./uploads"))
@app.get("/api/announcements")#адрес объявлений
def annoncements_list(user_id: int = None, metro: str = None, category: str = None, booked_by: int = -1): @app.get("/api/announcements")#адрес объявлений
# Считываем данные из Body и отображаем их на странице. def annoncements_list(user_id: int = None, metro: str = None, category: str = None, booked_by: int = -1):
# В последствии будем вставлять данные в html-форму # Считываем данные из Body и отображаем их на странице.
# В последствии будем вставлять данные в html-форму
a = db.query(Announcement)
b = db.query(Announcement) a = db.query(Announcement)
c = db.query(Announcement) b = db.query(Announcement)
d = db.query(Announcement) c = db.query(Announcement)
e = db.query(Announcement) d = db.query(Announcement)
e = db.query(Announcement)
if user_id != None:
b = a.filter(Announcement.user_id == user_id) if user_id != None:
b = a.filter(Announcement.user_id == user_id)
if metro != None:
c = a.filter(Announcement.metro == metro) if metro != None:
c = a.filter(Announcement.metro == metro)
if category != None:
d = a.filter(Announcement.category == category) if category != None:
d = a.filter(Announcement.category == category)
if booked_by != -1:
e = a.filter(Announcement.booked_by == booked_by) if booked_by != -1:
e = a.filter(Announcement.booked_by == booked_by)
if not any([category, user_id, metro]) and booked_by == -1:
result = a.all() if not any([category, user_id, metro]) and booked_by == -1:
result = a.all()
else:
result = b.intersect(c, d, e).all() else:
result = b.intersect(c, d, e).all()
return {"Success" : True, "list_of_announcements": result}
return {"Success" : True, "list_of_announcements": result}
@app.get("/api/announcement")#адрес объявлений
def single_annoncement(user_id:int): @app.get("/api/announcement")#адрес объявлений
# Считываем данные из Body и отображаем их на странице. def single_annoncement(user_id:int):
# В последствии будем вставлять данные в html-форму # Считываем данные из Body и отображаем их на странице.
try: # В последствии будем вставлять данные в html-форму
annoncement = db.get(Announcement, user_id) try:
return {"id": annoncement.id, "user_id": annoncement.user_id, "name": annoncement.name, annoncement = db.get(Announcement, user_id)
"category": annoncement.category, "best_by": annoncement.best_by, "address": annoncement.address, return {"id": annoncement.id, "user_id": annoncement.user_id, "name": annoncement.name,
"description": annoncement.description, "metro": annoncement.metro, "latitude": annoncement.latitude, "category": annoncement.category, "best_by": annoncement.best_by, "address": annoncement.address,
"longtitude":annoncement.longtitude, "trashId": annoncement.trashId, "src":annoncement.src, "description": annoncement.description, "metro": annoncement.metro, "latitude": annoncement.latitude,
"booked_by":annoncement.booked_by} "longtitude":annoncement.longtitude, "trashId": annoncement.trashId, "src":annoncement.src,
except: "booked_by":annoncement.booked_by}
return {"Answer" : False} #если неуданый доступ, то сообщаем об этом except:
return {"Answer" : False} #если неуданый доступ, то сообщаем об этом
# Занести объявление в базу
@app.put("/api/announcement")#адрес объявлений # Занести объявление в базу
def put_in_db(name: Annotated[str, Form()], category: Annotated[str, Form()], bestBy: Annotated[int, Form()], address: Annotated[str, Form()], longtitude: Annotated[float, Form()], latitude: Annotated[float, Form()], description: Annotated[str, Form()], src: UploadFile, metro: Annotated[str, Form()], trashId: Annotated[int, Form()] = None): @app.put("/api/announcement")#адрес объявлений
# try: def put_in_db(name: Annotated[str, Form()], category: Annotated[str, Form()], bestBy: Annotated[int, Form()], address: Annotated[str, Form()], longtitude: Annotated[float, Form()], latitude: Annotated[float, Form()], description: Annotated[str, Form()], src: UploadFile, metro: Annotated[str, Form()], trashId: Annotated[int, Form()] = None):
userId = 1 # temporary # try:
userId = 1 # temporary
uploaded_name = ""
uploaded_name = ""
f = src.file
f.seek(0, os.SEEK_END) f = src.file
if f.tell() > 0: f.seek(0, os.SEEK_END)
f.seek(0) if f.tell() > 0:
destination = pathlib.Path("./uploads/" + str(hash(src.file)) + pathlib.Path(src.filename).suffix.lower()) f.seek(0)
with destination.open('wb') as buffer: destination = pathlib.Path("./uploads/" + str(hash(src.file)) + pathlib.Path(src.filename).suffix.lower())
shutil.copyfileobj(src.file, buffer) with destination.open('wb') as buffer:
shutil.copyfileobj(src.file, buffer)
uploaded_name = "/uploads/"+destination.name
uploaded_name = "/uploads/"+destination.name
temp_ancmt = Announcement(user_id=userId, name=name, category=category, best_by=bestBy, address=address, longtitude=longtitude, latitude=latitude, description=description, src=uploaded_name, metro=metro, trashId=trashId)
db.add(temp_ancmt) # добавляем в бд temp_ancmt = Announcement(user_id=userId, name=name, category=category, best_by=bestBy, address=address, longtitude=longtitude, latitude=latitude, description=description, src=uploaded_name, metro=metro, trashId=trashId)
db.commit() # сохраняем изменения db.add(temp_ancmt) # добавляем в бд
db.refresh(temp_ancmt) # обновляем состояние объекта db.commit() # сохраняем изменения
return {"Answer" : True} db.refresh(temp_ancmt) # обновляем состояние объекта
# except: return {"Answer" : True}
# return {"Answer" : False} # except:
# return {"Answer" : False}
# Удалить объявления из базы
@app.delete("/api/announcement") #адрес объявления # Удалить объявления из базы
def delete_from_db(data = Body()):#функция удаления объекта из БД @app.delete("/api/announcement") #адрес объявления
try: def delete_from_db(data = Body()):#функция удаления объекта из БД
db.delete(user_id=data.user_id)#удаление из БД try:
db.commit() # сохраняем изменения db.delete(user_id=data.user_id)#удаление из БД
return {"Answer" : True} db.commit() # сохраняем изменения
except: return {"Answer" : True}
return {"Answer" : False} except:
return {"Answer" : False}
# Забронировать объявление
@app.post("/api/book") # Забронировать объявление
def change_book_status(data: schema.Book): @app.post("/api/book")
try: def change_book_status(data: schema.Book):
# Получаем id пользователя, который бронирует объявление try:
temp_user_id = 1 # Получаем id пользователя, который бронирует объявление
# Находим объявление по данному id temp_user_id = 1
announcement_to_change = db.query(Announcement).filter(id == data.id).first() # Находим объявление по данному id
# Изменяем поле booked_status на полученный id announcement_to_change = db.query(Announcement).filter(id == data.id).first()
announcement_to_change.booked_status = temp_user_id # Изменяем поле booked_status на полученный id
return {"Success": True} announcement_to_change.booked_status = temp_user_id
except: return {"Success": True}
return {"Success": False} except:
return {"Success": False}
@app.post("/api/signup")
def create_user(data = Body()): @app.post("/api/signup")
if db.query(UserDatabase).filter(UserDatabase.email == data["email"]).first() == None: def create_user(data = Body()):
new_user = UserDatabase(id=data["id"], email=data["email"], password=data["password"], name=data["name"], surname=data["surname"]) if db.query(UserDatabase).filter(UserDatabase.email == data["email"]).first() == None:
db.add(new_user) new_user = UserDatabase(id=data["id"], email=data["email"], password=data["password"], name=data["name"], surname=data["surname"])
db.commit() db.add(new_user)
db.refresh(new_user) # обновляем состояние объекта db.commit()
return {"Success": True} db.refresh(new_user) # обновляем состояние объекта
return {"Success": False, "Message": "Пользователь с таким email уже зарегестрирован."} return {"Success": True}
return {"Success": False, "Message": "Пользователь с таким email уже зарегестрирован."}
@app.post("/api/token", response_model=Token)
async def login_for_access_token( @app.post("/api/token", response_model=Token)
form_data: Annotated[OAuth2PasswordRequestForm, Depends()] async def login_for_access_token(
): form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
# разобраться с первым параметром ):
user = authenticate_user(db.query(UserDatabase).all(), form_data.username, form_data.password) # разобраться с первым параметром
if not user: user = authenticate_user(db.query(UserDatabase).all(), form_data.username, form_data.password)
raise HTTPException( if not user:
status_code=status.HTTP_401_UNAUTHORIZED, raise HTTPException(
detail="Incorrect username or password", status_code=status.HTTP_401_UNAUTHORIZED,
headers={"WWW-Authenticate": "Bearer"}, detail="Incorrect username or password",
) headers={"WWW-Authenticate": "Bearer"},
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) )
access_token = create_access_token( access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
data={"user_id": user.id}, expires_delta=access_token_expires access_token = create_access_token(
) data={"user_id": user.id}, expires_delta=access_token_expires
return {"access_token": access_token} )
return {"access_token": access_token}
@app.get("/api/users/me/", response_model=User)
async def read_users_me( @app.get("/api/users/me/", response_model=User)
current_user: Annotated[User, Depends(get_current_active_user)] async def read_users_me(
): current_user: Annotated[User, Depends(get_current_active_user)]
return current_user ):
return current_user
@app.get("/api/users/me/items/")
async def read_own_items( @app.get("/api/users/me/items/")
current_user: Annotated[User, Depends(get_current_active_user)] async def read_own_items(
): current_user: Annotated[User, Depends(get_current_active_user)]
return [{"Current user name": current_user.name, "Current user surname": current_user.surname}] ):
return [{"Current user name": current_user.name, "Current user surname": current_user.surname}]
@app.get("/api/trashbox")
def get_trashboxes(lat:float, lng:float):#крутая функция для работы с api @app.get("/api/trashbox")
BASE_URL='https://geointelect2.gate.petersburg.ru'#адрес сайта и мой токин def get_trashboxes(lat:float, lng:float):#крутая функция для работы с api
my_token='eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJhU1RaZm42bHpTdURYcUttRkg1SzN5UDFhT0FxUkhTNm9OendMUExaTXhFIn0.eyJleHAiOjE3Nzg2NTk4MjEsImlhdCI6MTY4Mzk2NTQyMSwianRpIjoiOTI2ZGMyNmEtMGYyZi00OTZiLWI0NTUtMWQyYWM5YmRlMTZkIiwiaXNzIjoiaHR0cHM6Ly9rYy5wZXRlcnNidXJnLnJ1L3JlYWxtcy9lZ3MtYXBpIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImJjYjQ2NzljLTU3ZGItNDU5ZC1iNWUxLWRlOGI4Yzg5MTMwMyIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFkbWluLXJlc3QtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6IjE2MGU1ZGVkLWFmMjMtNDkyNS05OTc1LTRhMzM0ZjVmNTkyOSIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiLyoiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtZWdzLWFwaSIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiIxNjBlNWRlZC1hZjIzLTQ5MjUtOTk3NS00YTMzNGY1ZjU5MjkiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiLQktC70LDQtNC40LzQuNGAINCv0LrQvtCy0LvQtdCyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiZTBmYzc2OGRhOTA4MjNiODgwZGQzOGVhMDJjMmQ5NTciLCJnaXZlbl9uYW1lIjoi0JLQu9Cw0LTQuNC80LjRgCIsImZhbWlseV9uYW1lIjoi0K_QutC-0LLQu9C10LIifQ.BRyUIyY-KKnZ9xqTNa9vIsfKF0UN2VoA9h4NN4y7IgBVLiiS-j43QbeE6qgjIQo0pV3J8jtCAIPvJbO-Ex-GNkw_flgMiGHhKEpsHPW3WK-YZ-XsZJzVQ_pOmLte-Kql4z97WJvolqiXT0nMo2dlX2BGvNs6JNbupvcuGwL4YYpekYAaFNYMQrxi8bSN-R7FIqxP-gzZDAuQSWRRSUqVBLvmgRhphTM-FAx1sX833oXL9tR7ze3eDR_obSV0y6cKVIr4eIlKxFd82qiMrN6A6CTUFDeFjeAGERqeBPnJVXU36MHu7Ut7eOVav9OUARARWRkrZRkqzTfZ1iqEBq5Tsg' BASE_URL='https://geointelect2.gate.petersburg.ru'#адрес сайта и мой токин
head = {'Authorization': 'Bearer {}'.format(my_token)} my_token='eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJhU1RaZm42bHpTdURYcUttRkg1SzN5UDFhT0FxUkhTNm9OendMUExaTXhFIn0.eyJleHAiOjE3Nzg2NTk4MjEsImlhdCI6MTY4Mzk2NTQyMSwianRpIjoiOTI2ZGMyNmEtMGYyZi00OTZiLWI0NTUtMWQyYWM5YmRlMTZkIiwiaXNzIjoiaHR0cHM6Ly9rYy5wZXRlcnNidXJnLnJ1L3JlYWxtcy9lZ3MtYXBpIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImJjYjQ2NzljLTU3ZGItNDU5ZC1iNWUxLWRlOGI4Yzg5MTMwMyIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFkbWluLXJlc3QtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6IjE2MGU1ZGVkLWFmMjMtNDkyNS05OTc1LTRhMzM0ZjVmNTkyOSIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiLyoiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtZWdzLWFwaSIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiIxNjBlNWRlZC1hZjIzLTQ5MjUtOTk3NS00YTMzNGY1ZjU5MjkiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiLQktC70LDQtNC40LzQuNGAINCv0LrQvtCy0LvQtdCyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiZTBmYzc2OGRhOTA4MjNiODgwZGQzOGVhMDJjMmQ5NTciLCJnaXZlbl9uYW1lIjoi0JLQu9Cw0LTQuNC80LjRgCIsImZhbWlseV9uYW1lIjoi0K_QutC-0LLQu9C10LIifQ.BRyUIyY-KKnZ9xqTNa9vIsfKF0UN2VoA9h4NN4y7IgBVLiiS-j43QbeE6qgjIQo0pV3J8jtCAIPvJbO-Ex-GNkw_flgMiGHhKEpsHPW3WK-YZ-XsZJzVQ_pOmLte-Kql4z97WJvolqiXT0nMo2dlX2BGvNs6JNbupvcuGwL4YYpekYAaFNYMQrxi8bSN-R7FIqxP-gzZDAuQSWRRSUqVBLvmgRhphTM-FAx1sX833oXL9tR7ze3eDR_obSV0y6cKVIr4eIlKxFd82qiMrN6A6CTUFDeFjeAGERqeBPnJVXU36MHu7Ut7eOVav9OUARARWRkrZRkqzTfZ1iqEBq5Tsg'
head = {'Authorization': 'Bearer {}'.format(my_token)}
my_data={
'x' : f"{lng}", my_data={
'y' : f"{lat}", 'x' : f"{lng}",
'limit' : '1' 'y' : f"{lat}",
} 'limit' : '1'
}
response = requests.post(f"{BASE_URL}/nearest_recycling/get", headers=head, data=my_data)
infos = response.json() response = requests.post(f"{BASE_URL}/nearest_recycling/get", headers=head, data=my_data)
infos = response.json()
trashboxes = []
for trashbox in infos["results"]: trashboxes = []
temp_dict = {} for trashbox in infos["results"]:
for obj in trashbox["Objects"]: temp_dict = {}
coord_list = obj["geometry"] for obj in trashbox["Objects"]:
temp_dict["Lat"] = coord_list["coordinates"][1] coord_list = obj["geometry"]
temp_dict["Lng"] = coord_list["coordinates"][0] temp_dict["Lat"] = coord_list["coordinates"][1]
temp_dict["Lng"] = coord_list["coordinates"][0]
properties = obj["properties"]
temp_dict["Name"] = properties["title"] properties = obj["properties"]
temp_dict["Address"] = properties["address"] temp_dict["Name"] = properties["title"]
temp_dict["Categories"] = properties["content_text"].split(',') temp_dict["Address"] = properties["address"]
trashboxes.append(temp_dict) temp_dict["Categories"] = properties["content_text"].split(',')
trashboxes.append(temp_dict)
uniq_trashboxes = [ast.literal_eval(el1) for el1 in set([str(el2) for el2 in trashboxes])]
return JSONResponse(uniq_trashboxes) uniq_trashboxes = [ast.literal_eval(el1) for el1 in set([str(el2) for el2 in trashboxes])]
return JSONResponse(uniq_trashboxes)
@app.get("/{rest_of_path:path}")
async def react_app(req: Request, rest_of_path: str): @app.get("/{rest_of_path:path}")
return templates.TemplateResponse('index.html', { 'request': req }) async def react_app(req: Request, rest_of_path: str):
return templates.TemplateResponse('index.html', { 'request': req })
=======
#подключение библиотек
from fastapi import FastAPI, Response, Path, Depends, Body, Form, Query, status, HTTPException, APIRouter, UploadFile, File
from fastapi.responses import HTMLResponse, FileResponse, JSONResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer
from fastapi.templating import Jinja2Templates
from fastapi.requests import Request
from pydantic import json
from starlette.staticfiles import StaticFiles
import requests
from uuid import uuid4
import ast
import pathlib
import shutil
import os
from .utils import *
from .models import Announcement, Trashbox, UserDatabase, Base
from .db import engine, SessionLocal
from . import schema
Base.metadata.create_all(bind=engine)
db = SessionLocal()
app = FastAPI()
templates = Jinja2Templates(directory="./front/dist")
app.mount("/static", StaticFiles(directory = "./front/dist"))
app.mount("/uploads", StaticFiles(directory = "./uploads"))
@app.get("/api/announcements")#адрес объявлений
def annoncements_list(user_id: int = None, metro: str = None, category: str = None, booked_by: int = -1):
# Считываем данные из Body и отображаем их на странице.
# В последствии будем вставлять данные в html-форму
a = db.query(Announcement)
b = db.query(Announcement)
c = db.query(Announcement)
d = db.query(Announcement)
e = db.query(Announcement)
if user_id != None:
b = a.filter(Announcement.user_id == user_id)
if metro != None:
c = a.filter(Announcement.metro == metro)
if category != None:
d = a.filter(Announcement.category == category)
if booked_by != -1:
e = a.filter(Announcement.booked_by == booked_by)
if not any([category, user_id, metro]) and booked_by == -1:
result = a.all()
else:
result = b.intersect(c, d, e).all()
return {"Success" : True, "list_of_announcements": result}
@app.get("/api/announcement")#адрес объявлений
def single_annoncement(user_id:int):
# Считываем данные из Body и отображаем их на странице.
# В последствии будем вставлять данные в html-форму
try:
annoncement = db.get(Announcement, user_id)
return {"id": annoncement.id, "user_id": annoncement.user_id, "name": annoncement.name,
"category": annoncement.category, "best_by": annoncement.best_by, "address": annoncement.address,
"description": annoncement.description, "metro": annoncement.metro, "latitude": annoncement.latitude,
"longtitude":annoncement.longtitude, "trashId": annoncement.trashId, "src":annoncement.src,
"booked_by":annoncement.booked_by}
except:
return {"Answer" : False} #если неуданый доступ, то сообщаем об этом
# Занести объявление в базу
@app.put("/api/announcement")#адрес объявлений
def put_in_db(name: Annotated[str, Form()], category: Annotated[str, Form()], bestBy: Annotated[int, Form()], address: Annotated[str, Form()], longtitude: Annotated[float, Form()], latitude: Annotated[float, Form()], description: Annotated[str, Form()], src: Annotated[UploadFile | None, File()], metro: Annotated[str, Form()], trashId: Annotated[int | None, Form()] = -1):
# try:
userId = 1 # temporary
uploaded_name = ""
f = src.file
f.seek(0, os.SEEK_END)
if f.tell() > 0:
f.seek(0)
destination = pathlib.Path("./uploads/" + str(hash(src.file)) + pathlib.Path(src.filename).suffix.lower())
with destination.open('wb') as buffer:
shutil.copyfileobj(src.file, buffer)
uploaded_name = "/uploads/"+destination.name
temp_ancmt = Announcement(user_id=userId, name=name, category=category, best_by=bestBy, address=address, longtitude=longtitude, latitude=latitude, description=description, src=uploaded_name, metro=metro, trashId=trashId, booked_by=-1)
db.add(temp_ancmt) # добавляем в бд
db.commit() # сохраняем изменения
db.refresh(temp_ancmt) # обновляем состояние объекта
return {"Answer" : True}
# except:
# return {"Answer" : False}
# Удалить объявления из базы
@app.delete("/api/announcement") #адрес объявления
def delete_from_db(data = Body()):#функция удаления объекта из БД
try:
db.delete(user_id=data.user_id)#удаление из БД
db.commit() # сохраняем изменения
return {"Answer" : True}
except:
return {"Answer" : False}
# Забронировать объявление
@app.post("/api/book")
def change_book_status(data: schema.Book):
try:
# Получаем id пользователя, который бронирует объявление
temp_user_id = 1
# Находим объявление по данному id
announcement_to_change = db.query(Announcement).filter(id == data.id).first()
# Изменяем поле booked_status на полученный id
announcement_to_change.booked_status = temp_user_id
return {"Success": True}
except:
return {"Success": False}
@app.post("/api/signup")
def create_user(data = Body()):
if db.query(UserDatabase).filter(User.email == data["email"]).first() == None:
new_user = UserDatabase(id=data["id"], email=data["email"], password=data["password"], name=data["name"], surname=data["surname"])
db.add(new_user)
db.commit()
db.refresh(new_user) # обновляем состояние объекта
return {"Success": True}
return {"Success": False, "Message": "Пользователь с таким email уже зарегестрирован."}
@app.post("/api/token", response_model=Token)
async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
):
user = authenticate_user(db.query(UserDatabase).all(), form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"user_id": user.id}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@app.get("/api/users/me/", response_model=User)
async def read_users_me(
current_user: Annotated[User, Depends(get_current_active_user)]
):
return current_user
@app.get("/api/users/me/items/")
async def read_own_items(
current_user: Annotated[User, Depends(get_current_active_user)]
):
return [{"Current user name": current_user.name, "Current user surname": current_user.surname}]
@app.get("/api/trashbox")
def get_trashboxes(lat:float, lng:float):#крутая функция для работы с api
BASE_URL='https://geointelect2.gate.petersburg.ru'#адрес сайта и мой токин
my_token='eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJhU1RaZm42bHpTdURYcUttRkg1SzN5UDFhT0FxUkhTNm9OendMUExaTXhFIn0.eyJleHAiOjE3Nzg2NTk4MjEsImlhdCI6MTY4Mzk2NTQyMSwianRpIjoiOTI2ZGMyNmEtMGYyZi00OTZiLWI0NTUtMWQyYWM5YmRlMTZkIiwiaXNzIjoiaHR0cHM6Ly9rYy5wZXRlcnNidXJnLnJ1L3JlYWxtcy9lZ3MtYXBpIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImJjYjQ2NzljLTU3ZGItNDU5ZC1iNWUxLWRlOGI4Yzg5MTMwMyIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFkbWluLXJlc3QtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6IjE2MGU1ZGVkLWFmMjMtNDkyNS05OTc1LTRhMzM0ZjVmNTkyOSIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiLyoiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtZWdzLWFwaSIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiIxNjBlNWRlZC1hZjIzLTQ5MjUtOTk3NS00YTMzNGY1ZjU5MjkiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiLQktC70LDQtNC40LzQuNGAINCv0LrQvtCy0LvQtdCyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiZTBmYzc2OGRhOTA4MjNiODgwZGQzOGVhMDJjMmQ5NTciLCJnaXZlbl9uYW1lIjoi0JLQu9Cw0LTQuNC80LjRgCIsImZhbWlseV9uYW1lIjoi0K_QutC-0LLQu9C10LIifQ.BRyUIyY-KKnZ9xqTNa9vIsfKF0UN2VoA9h4NN4y7IgBVLiiS-j43QbeE6qgjIQo0pV3J8jtCAIPvJbO-Ex-GNkw_flgMiGHhKEpsHPW3WK-YZ-XsZJzVQ_pOmLte-Kql4z97WJvolqiXT0nMo2dlX2BGvNs6JNbupvcuGwL4YYpekYAaFNYMQrxi8bSN-R7FIqxP-gzZDAuQSWRRSUqVBLvmgRhphTM-FAx1sX833oXL9tR7ze3eDR_obSV0y6cKVIr4eIlKxFd82qiMrN6A6CTUFDeFjeAGERqeBPnJVXU36MHu7Ut7eOVav9OUARARWRkrZRkqzTfZ1iqEBq5Tsg'
head = {'Authorization': 'Bearer {}'.format(my_token)}
my_data={
'x' : f"{lng}",
'y' : f"{lat}",
'limit' : '1'
}
response = requests.post(f"{BASE_URL}/nearest_recycling/get", headers=head, data=my_data)
infos = response.json()
trashboxes = []
for trashbox in infos["results"]:
temp_dict = {}
for obj in trashbox["Objects"]:
coord_list = obj["geometry"]
temp_dict["Lat"] = coord_list["coordinates"][1]
temp_dict["Lng"] = coord_list["coordinates"][0]
properties = obj["properties"]
temp_dict["Name"] = properties["title"]
temp_dict["Address"] = properties["address"]
temp_dict["Categories"] = properties["content_text"].split(',')
trashboxes.append(temp_dict)
uniq_trashboxes = [ast.literal_eval(el1) for el1 in set([str(el2) for el2 in trashboxes])]
return JSONResponse(uniq_trashboxes)
@app.get("/{rest_of_path:path}")
async def react_app(req: Request, rest_of_path: str):
return templates.TemplateResponse('index.html', { 'request': req })
>>>>>>> 3668e8c33f71b7a79a0c83d41a106d9b55e2df71

View File

@ -1,16 +1,36 @@
/* eslint-env node */
module.exports = { module.exports = {
root: true,
env: { browser: true, es2020: true }, env: { browser: true, es2020: true },
extends: [ extends: [
'eslint:recommended', 'eslint:recommended',
'plugin:react/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:react/jsx-runtime', 'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:react-hooks/recommended', 'plugin:react-hooks/recommended',
], ],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, parser: '@typescript-eslint/parser',
settings: { react: { version: '18.2' } }, parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: true,
tsconfigRootDir: __dirname,
},
plugins: ['react-refresh'], plugins: ['react-refresh'],
rules: { rules: {
'react-refresh/only-export-components': 'warn', 'react-refresh/only-export-components': [
'react/prop-types': 'off' 'warn',
{ allowConstantExport: true },
],
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/quotes': [
'error',
'single',
{
'avoidEscape': true,
'allowTemplateLiterals': true
}
],
'jsx-quotes': [2, 'prefer-single'],
}, },
} }

View File

@ -5,11 +5,9 @@
<link rel="icon" type="image/png" href="/favicon.png" /> <link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Porridger</title> <title>Porridger</title>
<!-- most likely will be loaded from browser cache -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.jsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

2171
front/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,38 @@
{ {
"name": "v2", "name": "front",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host 0.0.0.0", "dev": "vite --host 0.0.0.0",
"build": "vite build", "build": "tsc && vite build",
"lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview" "preview": "vite preview",
"typecheck": "tsc",
"addFetchApiRoute": "bash utils/addFetchApiRoute.sh"
}, },
"dependencies": { "dependencies": {
"bootstrap": "^5.2.3", "@types/leaflet": "^1.9.3",
"leaflet": "^1.9.3", "bootstrap": "^5.3.0",
"leaflet": "^1.9.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-bootstrap": "^2.7.4", "react-bootstrap": "^2.8.0",
"react-bootstrap-typeahead": "^6.1.2", "react-bootstrap-typeahead": "^6.2.3",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-insta-stories": "^2.5.9", "react-insta-stories": "^2.6.1",
"react-leaflet": "^4.2.1", "react-leaflet": "^4.2.1",
"react-router-dom": "^6.11.1" "react-router-dom": "^6.14.1"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.0.28", "@types/react": "^18.2.14",
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.2.6",
"@vitejs/plugin-react": "^4.0.0", "@typescript-eslint/eslint-plugin": "^5.61.0",
"eslint": "^8.38.0", "@typescript-eslint/parser": "^5.61.0",
"eslint-plugin-react": "^7.32.2", "@vitejs/plugin-react": "^4.0.1",
"eslint": "^8.44.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.3.4", "eslint-plugin-react-refresh": "^0.4.1",
"vite": "^4.3.2" "typescript": "^5.0.2",
"vite": "^4.4.0"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 94 KiB

BIN
front/public/bred.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 88 KiB

BIN
front/public/conspects.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 99 KiB

BIN
front/public/milk.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 128 KiB

BIN
front/public/wathing.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View File

@ -1,16 +1,16 @@
body { body {
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
color: white; color: white;
font-family: sans-serif; font-family: sans-serif;
} }
.modal-content, .modal-content .form-select { .modal-content, .modal-content .form-select {
background-color: rgb(17, 17, 17) !important; background-color: rgb(17, 17, 17) !important;
} }
/* В связи со сложившейся политической обстановкой */ /* В связи со сложившейся политической обстановкой */
.leaflet-attribution-flag { .leaflet-attribution-flag {
position: absolute; position: absolute;
right: -100px; right: -100px;
} }

View File

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

View File

View File

@ -0,0 +1,61 @@
import { isObject } from '../../utils/types'
import { Category, isCategory } from '../../assets/category'
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
}
export type {
Announcement,
AnnouncementResponse,
}
export {
isAnnouncementResponse,
}

View File

@ -0,0 +1,25 @@
import { API_URL } from '../../config'
import { FiltersType, URLEncodeFilters } from '../../utils/filters'
import { Announcement } from '../announcement/types'
import { AnnouncementsResponse } from './types'
const initialAnnouncements: Announcement[] = []
const composeAnnouncementsURL = (filters: FiltersType) => (
API_URL + '/announcements?' + new URLSearchParams(URLEncodeFilters(filters)).toString()
)
const processAnnouncements = (data: AnnouncementsResponse): Announcement[] => {
const annList = data.list_of_announcements
return annList.map(ann => ({
...ann,
lat: ann.latitude,
lng: ann.longtitude,
bestBy: ann.best_by,
bookedBy: ann.booked_by,
userId: ann.user_id
}))
}
export { initialAnnouncements, composeAnnouncementsURL, processAnnouncements }

View File

@ -0,0 +1,22 @@
import { isArrayOf, isObject } from '../../utils/types'
import { AnnouncementResponse, isAnnouncementResponse } from '../announcement/types'
type AnnouncementsResponse = {
list_of_announcements: AnnouncementResponse[],
Success: boolean
}
const isAnnouncementsResponse = (obj: unknown): obj is AnnouncementsResponse => (
isObject(obj, {
'list_of_announcements': obj => isArrayOf<AnnouncementResponse>(obj, isAnnouncementResponse),
'Success': 'boolean'
})
)
export type {
AnnouncementsResponse,
}
export {
isAnnouncementsResponse,
}

View File

@ -0,0 +1,14 @@
import { LatLng } from 'leaflet'
import { OsmAddressResponse } from './types'
const initialOsmAddress = ''
const composeOsmAddressURL = (addressPosition: LatLng) => (
`${location.protocol}//nominatim.openstreetmap.org/reverse?format=json&accept-language=ru&lat=${addressPosition.lat}&lon=${addressPosition.lng}`
)
const processOsmAddress = (data: OsmAddressResponse): string => (
data.display_name
)
export { initialOsmAddress, composeOsmAddressURL, processOsmAddress }

View File

@ -0,0 +1,19 @@
import { isObject } from '../../utils/types'
type OsmAddressResponse = {
display_name: string
}
const isOsmAddressResponse = (obj: unknown): obj is OsmAddressResponse => (
isObject(obj, {
'display_name': 'string',
})
)
export type {
OsmAddressResponse,
}
export {
isOsmAddressResponse,
}

View File

@ -0,0 +1,12 @@
import { LatLng } from 'leaflet'
import { API_URL } from '../../config'
const composeTrashboxURL = (position: LatLng) => (
API_URL + '/trashbox?' + new URLSearchParams({
lat: position.lat.toString(),
lng: position.lng.toString()
}).toString()
)
export { composeTrashboxURL }

View File

@ -0,0 +1,26 @@
import { isArrayOf, isObject, 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)
})
)
type TrashboxResponse = Trashbox[]
const isTrashboxResponse = (obj: unknown): obj is Trashbox[] => (
isArrayOf(obj, isTrashbox)
)
export type { Trashbox, TrashboxResponse }
export { isTrashbox, isTrashboxResponse }

View File

@ -1,32 +0,0 @@
const categoryGraphics = new Map([
["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 = new Map([
["PORRIDGE", "PORRIDGE"],
["conspects", "Конспекты"],
["milk", "Молочные продукты"],
["bred", "Хлебобулочные изделия"],
["wathing", "Моющие средства"],
["cloth", "Одежда"],
["fruits_vegatables", "Фрукты и овощи"],
["soup", "Супы"],
["dinner", "Ужин"],
["conserves", "Консервы"],
["pens", "Канцелярия"],
["other_things", "Всякая всячина"]
])
export { categoryNames, categoryGraphics }

View File

@ -0,0 +1,42 @@
import { isLiteralUnion } from '../utils/types'
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',
}
const categoryNames: Record<Category, string> = {
'PORRIDGE': 'PORRIDGE',
'conspects': 'Конспекты',
'milk': 'Молочные продукты',
'bred': 'Хлебобулочные изделия',
'wathing': 'Моющие средства',
'cloth': 'Одежда',
'fruits_vegatables': 'Фрукты и овощи',
'soup': 'Супы',
'dinner': 'Ужин',
'conserves': 'Консервы',
'pens': 'Канцелярия',
'other_things': 'Всякая всячина',
}
export type { Category }
export { categories, categoryNames, categoryGraphics, isCategory }

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

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

109
front/src/assets/metro.ts Normal file
View File

@ -0,0 +1,109 @@
const lines = ['red', 'blue', 'green', 'orange', 'violet'] as const
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',
}
const lineNames: Record<Lines, string> = {
red: 'Красная',
blue: 'Синяя',
green: 'Зелёная',
orange: 'Оранжевая',
violet: 'Фиолетовая',
}
const lineByName = (name: string) => (
lines.find(line => stations[line].has(name))
)
export type { Lines }
export { lines, stations, colors, lineNames, lineByName }

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -4,16 +4,32 @@ import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet'
import LineDot from './LineDot' import LineDot from './LineDot'
import { categoryNames } from '../assets/category' import { categoryNames } from '../assets/category'
import { useBook } from '../hooks/api' import { useBook } from '../hooks/api'
import { Announcement } from '../api/announcement/types'
import { iconItem } from '../utils/markerIcons'
import { CSSProperties } from 'react'
function AnnouncementDetails({ close, announcement: { id, name, category, bestBy, description, lat, lng, address, metro } }) { type AnnouncementDetailsProps = {
close: () => void,
announcement: Announcement
}
const styles = {
container: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
} as CSSProperties,
}
function AnnouncementDetails({ close, announcement: { id, name, category, bestBy, description, lat, lng, address, metro } }: AnnouncementDetailsProps) {
const { handleBook, status: bookStatus } = useBook(id) const { handleBook, status: bookStatus } = useBook(id)
return ( return (
<div <div
className="modal" className='modal'
style={{ display: 'flex', alignItems: "center", justifyContent: "center" }} style={styles.container}
> >
<Modal.Dialog style={{ minWidth: "50vw" }}> <Modal.Dialog style={{ minWidth: '50vw' }}>
<Modal.Header closeButton onHide={close}> <Modal.Header closeButton onHide={close}>
<Modal.Title> <Modal.Title>
Подробнее Подробнее
@ -23,19 +39,19 @@ function AnnouncementDetails({ close, announcement: { id, name, category, bestBy
<Modal.Body> <Modal.Body>
<h1>{name}</h1> <h1>{name}</h1>
<span>{categoryNames.get(category)}</span> <span>{categoryNames[category]}</span>
<span className='m-2'>&#x2022;</span>{/* dot */} <span className='m-2'>&#x2022;</span>{/* dot */}
<span>Годен до {new Date(bestBy).toLocaleString('ru-RU')}</span> <span>Годен до {new Date(bestBy).toLocaleString('ru-RU')}</span>
<p className='mb-3'>{description}</p> <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 <TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' 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]}> <Marker icon={iconItem} position={[lat, lng]}>
<Popup> <Popup>
{address} {address}
<br /> <br />
@ -46,8 +62,8 @@ function AnnouncementDetails({ close, announcement: { id, name, category, bestBy
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button variant='success' onClick={handleBook}> <Button variant='success' onClick={() => void handleBook()}>
{bookStatus || "Забронировать"} {bookStatus || 'Забронировать'}
</Button> </Button>
</Modal.Footer> </Modal.Footer>
</Modal.Dialog> </Modal.Dialog>
@ -55,4 +71,4 @@ function AnnouncementDetails({ close, announcement: { id, name, category, bestBy
) )
} }
export default AnnouncementDetails export default AnnouncementDetails

View File

@ -0,0 +1,56 @@
import { FormEventHandler } from 'react'
import { Button, Form } from 'react-bootstrap'
type AuthFormProps = {
register: boolean
handleAuth: FormEventHandler<HTMLFormElement>,
loading: boolean,
error: string
}
function AuthForm ({ handleAuth, register, loading, error }: AuthFormProps) {
const buttonText = loading ? 'Загрузка...' : (error || (register ? 'Зарегистрироваться' : 'Войти'))
return (
<Form onSubmit={handleAuth}>
<Form.Group className='mb-3' controlId='email'>
<Form.Label>Почта</Form.Label>
<Form.Control type='email' required />
</Form.Group>
{register && <>
<Form.Group className='mb-3' controlId='name'>
<Form.Label>Имя</Form.Label>
<Form.Control type='text' required />
</Form.Group>
<Form.Group className='mb-3' controlId='surname'>
<Form.Label>Фамилия</Form.Label>
<Form.Control type='text' required />
</Form.Group>
</>}
<Form.Group className='mb-3' controlId='password'>
<Form.Label>Пароль</Form.Label>
<Form.Control type='password' required />
</Form.Group>
{register &&
<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>
</Form.Check.Label>
</Form.Check>
</Form.Group>
}
<Button variant='success' type='submit'>
{buttonText}
</Button>
</Form>
)
}
export default AuthForm

View File

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

View File

@ -0,0 +1,58 @@
import { Link } from 'react-router-dom'
import addIcon from '../assets/addIcon.svg'
import filterIcon from '../assets/filterIcon.svg'
import userIcon from '../assets/userIcon.svg'
import { CSSProperties } from 'react'
const styles = {
navBar: {
backgroundColor: 'var(--bs-success)',
height: 56,
width: '100%',
} as CSSProperties,
navBarGroup: {
display: 'flex',
flexDirection: 'row',
height: '100%',
margin: 'auto'
} as CSSProperties,
navBarElement: {
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
} as CSSProperties,
}
type BottomNavBarProps = {
width: number,
toggleFilters: (p: boolean) => void
}
function BottomNavBar({ width, toggleFilters }: BottomNavBarProps) {
return (
<div style={styles.navBar}>
<div style={{ ...styles.navBarGroup, width: width }}>
<a style={styles.navBarElement} onClick={() => toggleFilters(true)}>
<img src={filterIcon} alt='Фильтровать объявления' title='Фильтровать объявления' />
</a>
<Link style={styles.navBarElement} to='/add' >
<img src={addIcon} alt='Опубликовать объявление' title='Опубликовать объявление' />
</Link>
<Link style={styles.navBarElement} to={'/user'} >
<img src={userIcon} alt='Личный кабинет' title='Личный кабинет' />
</Link>
</div>
</div>
)
}
export default BottomNavBar

View File

@ -1,12 +0,0 @@
import { useMapEvent } from "react-leaflet"
function ClickHandler({ setPosition }) {
const map = useMapEvent('click', (e) => {
setPosition(e.latlng)
map.setView(e.latlng)
})
return null
}
export default ClickHandler

View File

@ -0,0 +1,15 @@
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) => {
setPosition(e.latlng)
map.setView(e.latlng)
})
return null
}
export default ClickHandler

View File

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

View File

@ -1,12 +0,0 @@
import { colors, lines } from '../assets/metro'
import { lineByName } from '../utils/metro'
function LineDot({ station }) {
const line = lineByName(station)
const lineTitle = lines[line]
const color = colors[line]
return <span title={`${lineTitle} ветка`} style={{ color: color }}>&#11044;</span>
}
export default LineDot

View File

@ -0,0 +1,15 @@
import { colors, lineNames, lineByName } from '../assets/metro'
function LineDot({ station }: { station: string }) {
const line = lineByName(station)
if (line == undefined)
return <></>
const lineTitle = lineNames[line]
const color = colors[line]
return <span title={`${lineTitle} ветка`} style={{ color: color }}>&#11044;</span>
}
export default LineDot

View File

@ -1,27 +0,0 @@
import { Marker, Popup, useMapEvents } from "react-leaflet"
const LocationMarker = ({ address, position, setPosition }) => {
const map = useMapEvents({
dragend: () => {
setPosition(map.getCenter())
},
zoomend: () => {
setPosition(map.getCenter())
},
resize: () => {
setPosition(map.getCenter())
}
})
return (
<Marker position={position}>
<Popup>
{address}
{position.lat.toFixed(4)}, {position.lng.toFixed(4)}
</Popup>
</Marker>
)
}
export default LocationMarker

View File

@ -0,0 +1,37 @@
import { Marker, Popup, useMapEvents } from 'react-leaflet'
import { LatLng } from 'leaflet'
import { SetState } from '../utils/types'
import { iconItem } from '../utils/markerIcons'
type LocationMarkerProps = {
address: string,
position: LatLng,
setPosition: SetState<LatLng>
}
function LocationMarker({ address, position, setPosition }: LocationMarkerProps) {
const map = useMapEvents({
dragend: () => {
setPosition(map.getCenter())
},
zoomend: () => {
setPosition(map.getCenter())
},
resize: () => {
setPosition(map.getCenter())
}
})
return (
<Marker icon={iconItem} position={position}>
<Popup>
{address}
{position.lat.toFixed(4)}, {position.lng.toFixed(4)}
</Popup>
</Marker>
)
}
export default LocationMarker

View File

@ -1,15 +1,23 @@
import { Marker, Popup } from "react-leaflet" import { Marker, Popup } from 'react-leaflet'
const TrashboxMarkers = ({ trashboxes, selectTrashbox }) => { import { Trashbox } from '../api/trashbox/types'
import { iconTrash } from '../utils/markerIcons'
type TrashboxMarkersProps = {
trashboxes: Trashbox[],
selectTrashbox: ({ index, category }: { index: number, category: string }) => void
}
function TrashboxMarkers({ trashboxes, selectTrashbox }: TrashboxMarkersProps) {
return ( return (
<>{trashboxes.map((trashbox, index) => ( <>{trashboxes.map((trashbox, index) => (
<Marker key={trashbox.Lat + "" + trashbox.Lng} position={[trashbox.Lat, trashbox.Lng]}> <Marker icon={iconTrash} key={`${trashbox.Lat}${trashbox.Lng}`} position={[trashbox.Lat, trashbox.Lng]}>
<Popup> <Popup>
<p>{trashbox.Address}</p> <p>{trashbox.Address}</p>
<p>Тип мусора: <> <p>Тип мусора: <>
{trashbox.Categories.map((category, j) => {trashbox.Categories.map((category, j) =>
<span key={trashbox.Address + category}> <span key={trashbox.Address + category}>
<a href="#" onClick={() => selectTrashbox({ index, category })}> <a href='#' onClick={() => selectTrashbox({ index, category })}>
{category} {category}
</a> </a>
{(j < trashbox.Categories.length - 1) ? ', ' : ''} {(j < trashbox.Categories.length - 1) ? ', ' : ''}
@ -23,4 +31,4 @@ const TrashboxMarkers = ({ trashboxes, selectTrashbox }) => {
) )
} }
export default TrashboxMarkers export default TrashboxMarkers

View File

@ -1,19 +0,0 @@
import { useEffect } from "react"
import { getToken } from "../utils/auth"
import { useNavigate } from "react-router-dom"
function WithToken({ children }) {
const navigate = useNavigate()
useEffect(() => {
if (!getToken()) {
return navigate("/login")
}
}, [navigate])
return (
<>{children}</>
)
}
export default WithToken

View File

@ -0,0 +1,20 @@
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')
}
}, [navigate])
return (
<>{children}</>
)
}
export default WithToken

View File

@ -1,19 +0,0 @@
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'
export {
AnnouncementDetails,
BottomNavBar,
Filters,
LineDot,
LocationMarker,
TrashboxMarkers,
WithToken,
ClickHandler,
}

View File

@ -0,0 +1,9 @@
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 +0,0 @@
const API_URL = "/api"
export { API_URL }

3
front/src/config.ts Normal file
View File

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

View File

@ -1,5 +1,6 @@
export { default as useHomeAnnouncementList } from './useHomeAnnouncementList' export { default as useAnnouncements } from './useAnnouncements'
export { default as useBook } from './useBook' export { default as useBook } from './useBook'
export { default as useAuth } from './useAuth' export { default as useAuth } from './useAuth'
export { default as useTrashboxes } from './useTrashboxes' export { default as useTrashboxes } from './useTrashboxes'
export { default as useAddAnnouncement } from './useAddAnnouncement' export { default as useAddAnnouncement } from './useAddAnnouncement'
export { default as useOsmAddresses } from './useOsmAddress'

View File

@ -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

View File

@ -0,0 +1,79 @@
import { useEffect, useRef, useState } 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'
)
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
})
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

View File

@ -0,0 +1,18 @@
import { useFetch } from '../'
import { FiltersType } from '../../utils/filters'
import { composeAnnouncementsURL, initialAnnouncements, processAnnouncements } from '../../api/announcements'
import { isAnnouncementsResponse } from '../../api/announcements/types'
const useAnnouncements = (filters: FiltersType) => (
useFetch(
composeAnnouncementsURL(filters),
'GET',
false,
isAnnouncementsResponse,
processAnnouncements,
initialAnnouncements
)
)
export default useAnnouncements

View File

@ -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

View File

@ -0,0 +1,117 @@
import { useState } from 'react'
import { API_URL } from '../../config'
import { isConst, isObject } from '../../utils/types'
import { handleHTTPErrors } from '../../utils'
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('')
async function doAuth(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'
}
})
handleHTTPErrors(res)
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

View File

@ -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

View File

@ -0,0 +1,74 @@
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'
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'
}
})
handleHTTPErrors(res)
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

View File

@ -1,57 +0,0 @@
import { useEffect, useRef, useState } from "react"
import { isAborted } from '../../utils'
const useFetch = (url, params, initialData) => {
const [data, setData] = useState(initialData)
const [loading, setLoading] = useState(true)
const [error, setError] = useState("")
const abortControllerRef = useRef(null)
useEffect(() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
const abortController = new AbortController()
abortControllerRef.current = abortController
fetch(url, { ...params, signal: abortControllerRef.current.signal })
.then(res => {
if (!res.ok) {
switch (res.status) {
case 401:
throw new Error("Ошибка авторизации")
case 404:
throw new Error("Объект не найден")
default: {
throw new Error("Ошибка ответа от сервера")
}
}
}
return res.json()
})
.then(data => {
setData(data)
setLoading(false)
})
.catch(err => {
if (!isAborted(err)) {
setError("Ошибка сети")
}
setLoading(false)
if (import.meta.env.DEV) {
console.log(url, params, err)
}
})
return () => abortControllerRef.current.abort()
}, [url, params])
return { data, loading, error, abort: abortControllerRef.current?.abort }
}
export default useFetch

View File

@ -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

View File

@ -0,0 +1,18 @@
import { LatLng } from 'leaflet'
import { useFetch } from '../'
import { composeOsmAddressURL, processOsmAddress } from '../../api/osmAddress'
import { isOsmAddressResponse } from '../../api/osmAddress/types'
const useOsmAddresses = (addressPosition: LatLng) => (
useFetch(
composeOsmAddressURL(addressPosition),
'GET',
false,
isOsmAddressResponse,
processOsmAddress,
''
)
)
export default useOsmAddresses

View File

@ -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

View File

@ -0,0 +1,18 @@
import { LatLng } from 'leaflet'
import { useFetch } from '../'
import { composeTrashboxURL } from '../../api/trashbox'
import { isTrashboxResponse } from '../../api/trashbox/types'
const useTrashboxes = (position: LatLng) => (
useFetch(
composeTrashboxURL(position),
'GET',
true,
isTrashboxResponse,
(data) => data,
[]
)
)
export default useTrashboxes

View File

@ -1,5 +0,0 @@
import useStoryDimensions from "./useStoryDimensions"
export {
useStoryDimensions,
}

3
front/src/hooks/index.ts Normal file
View File

@ -0,0 +1,3 @@
export { default as useStoryDimensions } from './useStoryDimensions'
export { default as useSend } from './useSend'
export { default as useFetch } from './useFetch'

View File

@ -0,0 +1,75 @@
import { useEffect, useState } from 'react'
import { SetState } from '../utils/types'
import useSend from './useSend'
type UseFetchShared = {
loading: boolean,
abort?: () => void,
}
type UseFetchSucced<T> = {
error: null,
data: T,
} & UseFetchShared
type UseFetchErrored = {
error: string,
data: undefined
} & UseFetchShared
const gotError = <T>(res: UseFetchErrored | UseFetchSucced<T>): res is UseFetchErrored => (
typeof res.error === 'string'
)
const fallbackError = <T>(res: UseFetchSucced<T> | UseFetchErrored) => (
gotError(res) ? res.error : res.data
)
type UseFetchReturn<T> = ({
error: null,
data: T
} | {
error: string,
data: undefined
}) & {
loading: boolean,
setData: SetState<T | undefined>
abort?: (() => void)
}
function useFetch<R, T>(
url: string,
method: RequestInit['method'],
needAuth: boolean,
guardResponse: (data: unknown) => data is R,
processResponse: (data: R) => T,
initialData?: T,
params?: Omit<RequestInit, 'method'>
): UseFetchReturn<T> {
const [data, setData] = useState(initialData)
const { doSend, loading, error } = useSend(url, method, needAuth, guardResponse, processResponse, params)
useEffect(() => {
doSend().then(
data => { if (data !== undefined) setData(data) }
).catch( // must never occur
err => import.meta.env.DEV && console.error('Failed to do fetch request', err)
)
}, [doSend])
return {
...(
error === null ? ({
data: data!, error: null
}) : ({ data: undefined, error })
),
loading,
setData
}
}
export default useFetch
export { gotError, fallbackError }

View File

@ -0,0 +1,94 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { getToken } from '../utils/auth'
import { handleHTTPErrors, isAborted } from '../utils'
function useSend<R, T>(
url: string,
method: RequestInit['method'],
needAuth: boolean,
guardResponse: (data: unknown) => data is R,
processResponse: (data: R) => T,
defaultParams?: Omit<RequestInit, 'method'>
) {
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const navigate = useNavigate()
const abortControllerRef = useRef<AbortController>()
useEffect(() => () => abortControllerRef.current?.abort(), [])
/** 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'>) => {
setLoading(true)
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
const abortController = new AbortController()
abortControllerRef.current = abortController
const headers = new Headers({
...defaultParams?.headers,
...params?.headers
})
if (needAuth) {
const token = getToken()
if (token === null) {
navigate('/login')
return undefined
}
headers.append('Auth', `Bearer ${token}`)
}
try {
const res = await fetch(url + new URLSearchParams(urlProps).toString(), {
method,
...defaultParams,
...params,
headers,
signal: abortControllerRef.current.signal,
})
handleHTTPErrors(res)
const data: unknown = await res.json()
if (!guardResponse(data)) {
throw new Error('Malformed server response')
}
setLoading(false)
return processResponse(data)
} catch (err) {
if (err instanceof Error && !isAborted(err)) {
setError('Ошибка сети')
if (import.meta.env.DEV) {
console.log(url, params, err)
}
}
setLoading(false)
return undefined
}
}, [defaultParams, needAuth, navigate, url, method, guardResponse, processResponse])
return {
doSend, loading, error,
abort: abortControllerRef.current?.abort.bind(abortControllerRef.current)
}
}
export default useSend

View File

@ -1,15 +1,13 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
function getWindowDimensions() { const getWindowDimensions = () => (
const { innerWidth: width, innerHeight: height } = window; {
width: window.innerWidth,
height: window.innerHeight
}
)
return { function useStoryDimensions(maxRatio = 16 / 9) {
width,
height
};
}
function useStoryDimensions(maxRatio = 16/9) {
const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions()) const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions())
useEffect(() => { useEffect(() => {
@ -17,8 +15,8 @@ function useStoryDimensions(maxRatio = 16/9) {
setWindowDimensions(getWindowDimensions()); setWindowDimensions(getWindowDimensions());
} }
window.addEventListener("resize", handleResize); window.addEventListener('resize', handleResize);
return () => window.removeEventListener("resize", handleResize); return () => window.removeEventListener('resize', handleResize);
}, []); }, []);
const height = windowDimensions.height - 56 const height = windowDimensions.height - 56
@ -31,4 +29,4 @@ function useStoryDimensions(maxRatio = 16/9) {
} }
} }
export default useStoryDimensions export default useStoryDimensions

View File

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

View File

@ -1,11 +1,11 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import App from './App.jsx' import App from './App.tsx'
import './index.css' import './index.css'
import 'bootstrap/dist/css/bootstrap.min.css'
ReactDOM.createRoot(document.getElementById('root')).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> <React.StrictMode>
<App /> <App />
</React.StrictMode>, </React.StrictMode>,

View File

@ -1,204 +0,0 @@
import { useEffect, 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 { categoryNames } from "../assets/category"
import { stations, lines } from "../assets/metro"
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 [selectedTrashbox, setSelectedTrashbox] = useState({ index: -1, category: '' })
useEffect(() => {
(async () => {
try {
const res = await fetch(location.protocol + "//nominatim.openstreetmap.org/search?format=json&q=" + address)
const fetchData = await res.json()
console.log("f", fetchData)
} catch (err) {
console.error(err)
}
})()
}, [address])
useEffect(() => {
(async () => {
try {
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()
setAddress(fetchData.display_name)
} catch (err) {
console.error(err)
}
})()
}, [addressPosition])
const { doAdd, status } = useAddAnnouncement()
const handleSubmit = (event) => {
event.preventDefault()
event.stopPropagation()
const formData = new FormData(event.target)
formData.append("latitude", addressPosition.lat)
formData.append("longtitude", addressPosition.lng)
formData.append("address", address)
formData.set("bestBy", new Date(formData.get("bestBy")).getTime())
doAdd(formData)
}
return (
<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.Label>Заголовок объявления</Form.Label>
<Form.Control type="text" required name="name" />
</Form.Group>
<Form.Group className="mb-3" controlId="category">
<Form.Label>Категория</Form.Label>
<Form.Select required name="category">
<option value="" hidden>
Выберите категорию
</option>
{Array.from(categoryNames).map(
([category, categoryName]) =>
<option key={category} value={category}>{categoryName}</option>
)}
</Form.Select>
</Form.Group>
<Form.Group className="mb-3" controlId="bestBy">
<Form.Label>Срок годности</Form.Label>
<Form.Control type="date" required name="bestBy" />
</Form.Group>
<Form.Group className="mb-3" controlId="address">
<Form.Label>Адрес выдачи</Form.Label>
<div className="mb-3">
<MapContainer
scrollWheelZoom={false}
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"
/>
<LocationMarker
address={address}
position={addressPosition}
setPosition={setAddressPosition}
/>
<ClickHandler
setPosition={setAddressPosition}
/>
</MapContainer>
</div>
<p>Адрес: {address}</p>
</Form.Group>
<Form.Group className="mb-3" controlId="description">
<Form.Label>Описание</Form.Label>
<Form.Control as="textarea" name="description" rows={3} placeholder="Укажите свои контакты, а так же, когда вам будет удобно передать продукт" />
</Form.Group>
<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"
/>
</Form.Group>
<Form.Group className="mb-3" controlId="metro">
<Form.Label>
Станция метро
</Form.Label>
<Form.Select name="metro">
<option value="">
Укажите ближайщую станцию метро
</option>
{Object.entries(stations).map(
([line, stations]) =>
<optgroup key={line} label={lines[line]}>
{Array.from(stations).map(metro =>
<option key={metro} value={metro}>{metro}</option>
)}
</optgroup>
)}
</Form.Select>
</Form.Group>
<Form.Group className="mb-3" controlId="password">
<Form.Label>Пункт сбора мусора</Form.Label>
<div className="mb-3">
{trashboxes_loading
? (
<div style={{ height: 400 }}>
<p>Загрузка</p>
</div>
) : (
trashboxes_error ? (
<p
style={{ height: 400 }}
className="text-danger"
>{trashboxes_error}</p>
) : (
<MapContainer
scrollWheelZoom={false}
style={{ width: "100%", height: 400 }}
center={addressPosition}
zoom={13}
className=""
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<TrashboxMarkers
trashboxes={trashboxes}
selectTrashbox={setSelectedTrashbox}
/>
</MapContainer>
)
)
}
</div>
{selectedTrashbox.index > -1 ? (
<p>Выбран пункт сбора мусора на {
trashboxes[selectedTrashbox.index].Address
} с категорией {selectedTrashbox.category}</p>
) : (
<p>Выберите пунк сбора мусора и категорию</p>
)}
</Form.Group>
<Button variant="success" type="submit">
{status}
</Button>
</Form>
</Card.Body>
</Card>
)
}
export default AddPage

207
front/src/pages/AddPage.tsx Normal file
View File

@ -0,0 +1,207 @@
import { CSSProperties, FormEventHandler, useEffect, 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'
import { useOsmAddresses } from '../hooks/api'
const styles = {
modal: {
height: 'calc(100vh - 3rem)',
} as CSSProperties,
body: {
overflowY: 'auto',
} as CSSProperties,
map: {
width: '100%',
height: 400,
} as CSSProperties,
}
function AddPage() {
const [addressPosition, setAddressPosition] = useState(latLng(59.972, 30.3227))
const trashboxes = useTrashboxes(addressPosition)
const [selectedTrashbox, setSelectedTrashbox] = useState({ index: -1, category: '' })
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 handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault()
event.stopPropagation()
const formData = new FormData(event.currentTarget)
formData.append('latitude', addressPosition.lat.toString())
formData.append('longtitude', addressPosition.lng.toString())
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)
}
return (
<Card className='m-4' style={styles.modal}>
<Card.Body style={styles.body} >
<Form onSubmit={handleSubmit}>
<Form.Group className='mb-3' controlId='name'>
<Form.Label>Заголовок объявления</Form.Label>
<Form.Control type='text' required name='name' />
</Form.Group>
<Form.Group className='mb-3' controlId='category'>
<Form.Label>Категория</Form.Label>
<Form.Select required name='category'>
<option value='' hidden>
Выберите категорию
</option>
{categories.map(category =>
<option key={category} value={category}>{categoryNames[category]}</option>
)}
</Form.Select>
</Form.Group>
<Form.Group className='mb-3' controlId='bestBy'>
<Form.Label>Срок годности</Form.Label>
<Form.Control type='date' required name='bestBy' />
</Form.Group>
<Form.Group className='mb-3' controlId='address'>
<Form.Label>Адрес выдачи</Form.Label>
<div className='mb-3'>
<MapContainer
scrollWheelZoom={false}
style={styles.map}
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'
/>
<LocationMarker
address={fallbackError(address)}
position={addressPosition}
setPosition={setAddressPosition}
/>
<ClickHandler
setPosition={setAddressPosition}
/>
</MapContainer>
</div>
<p>Адрес: {fallbackError(address)}</p>
</Form.Group>
<Form.Group className='mb-3' controlId='description'>
<Form.Label>Описание</Form.Label>
<Form.Control as='textarea' name='description' rows={3} placeholder='Укажите свои контакты, а так же, когда вам будет удобно передать продукт' />
</Form.Group>
<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'
/>
</Form.Group>
<Form.Group className='mb-3' controlId='metro'>
<Form.Label>
Станция метро
</Form.Label>
<Form.Select name='metro'>
<option value=''>
Укажите ближайщую станцию метро
</option>
{lines.map(
line =>
<optgroup key={line} label={lineNames[line]}>
{Array.from(stations[line]).map(metro =>
<option key={metro} value={metro}>{metro}</option>
)}
</optgroup>
)}
</Form.Select>
</Form.Group>
<Form.Group className='mb-3' controlId='password'>
<Form.Label>Пункт сбора мусора</Form.Label>
<div className='mb-3'>
{trashboxes.loading
? (
<div style={styles.map}>
<p>Загрузка...</p>
</div>
) : (
gotError(trashboxes) ? (
<p
style={styles.map}
className='text-danger'
>{trashboxes.error}</p>
) : (
<MapContainer
scrollWheelZoom={false}
style={styles.map}
center={addressPosition}
zoom={13}
className=''
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
/>
<TrashboxMarkers
trashboxes={trashboxes.data}
selectTrashbox={setSelectedTrashbox}
/>
</MapContainer>
)
)
}
</div>
{!gotError(trashboxes) && selectedTrashbox.index > -1 ? (
<p>Выбран пункт сбора мусора на {
trashboxes.data[selectedTrashbox.index].Address
} с категорией {selectedTrashbox.category}</p>
) : (
<p>Выберите пунк сбора мусора и категорию</p>
)}
</Form.Group>
<Button variant='success' type='submit'>
{status}
</Button>
</Form>
</Card.Body>
</Card>
)
}
export default AddPage

View File

@ -1,68 +1,80 @@
import { useEffect, useState } from 'react' import { CSSProperties, useEffect, useState } from 'react'
import Stories from 'react-insta-stories' import Stories from 'react-insta-stories'
import { Story } from 'react-insta-stories/dist/interfaces'
import { BottomNavBar, AnnouncementDetails, Filters } from '../components' import { BottomNavBar, AnnouncementDetails, Filters } from '../components'
import { useStoryDimensions } from '../hooks' import { useStoryDimensions } from '../hooks'
import { useHomeAnnouncementList } from '../hooks/api' import { useAnnouncements } from '../hooks/api'
import { defaultFilters } from '../utils/filters'
import puffSpinner from '../assets/puff.svg' import { Announcement } from '../api/announcement/types'
import { categoryGraphics } from '../assets/category' import { categoryGraphics } from '../assets/category'
function generateStories(announcements) { import puffSpinner from '../assets/puff.svg'
import { gotError } from '../hooks/useFetch'
function generateStories(announcements: Announcement[]): Story[] {
return announcements.map(announcement => { return announcements.map(announcement => {
return ({ return ({
id: announcement.id, id: announcement.id,
url: announcement.src || categoryGraphics.get(announcement.category), url: announcement.src || categoryGraphics[announcement.category],
type: announcement.src?.endsWith("mp4") ? "video" : undefined, 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 useAnnouncements>) {
const stories = generateStories(announcementsFetch.data)
if (announcementsFetch.loading) if (announcementsFetch.loading)
return fallbackStory() return fallbackStory()
if (announcementsFetch.error) if (gotError(announcementsFetch))
return fallbackStory(announcementsFetch.error, true) return fallbackStory(announcementsFetch.error, true)
const stories = generateStories(announcementsFetch.data)
if (stories.length === 0) if (stories.length === 0)
return fallbackStory("Здесь пока пусто") return fallbackStory('Здесь пока пусто')
return stories return stories
} }
const fallbackStory = (text, isError = false) => [{ const fallbackStory = (text = '', isError = false): Story[] => [{
content: ({ action }) => { content: ({ action }) => {
// eslint-disable-next-line react-hooks/rules-of-hooks // eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => { action('pause') }, [action]) useEffect(() => { action('pause') }, [action])
return ( return (
<div style={{ margin: 'auto' }} className={isError ? "text-danger" : ''}> <div style={styles.center} className={isError ? 'text-danger' : ''}>
{text || <img src={puffSpinner} />} {text || <img src={puffSpinner} />}
</div> </div>
) )
}, },
header: { heading: text }
}] }]
const defaultFilters = { userId: null, category: null, metro: null, bookedBy: null } const styles = {
container: {
display: 'flex',
justifyContent: 'center',
backgroundColor: 'rgb(17, 17, 17)',
} as CSSProperties,
center: {
margin: 'auto'
} as CSSProperties,
}
function HomePage() { function HomePage() {
const { height, width } = useStoryDimensions(16 / 10) const { height, width } = useStoryDimensions(16 / 9)
const [filterShown, setFilterShown] = useState(false) const [filterShown, setFilterShown] = useState(false)
const [filter, setFilter] = useState(defaultFilters) const [filter, setFilter] = useState(defaultFilters)
const announcementsFetch = useHomeAnnouncementList(filter) const announcementsFetch = useAnnouncements(filter)
const stories = fallbackGenerateStories(announcementsFetch) const stories = fallbackGenerateStories(announcementsFetch)
return (<> return (<>
<Filters filter={filter} setFilter={setFilter} filterShown={filterShown} setFilterShown={setFilterShown} /> <Filters filter={filter} setFilter={setFilter} filterShown={filterShown} setFilterShown={setFilterShown} />
<div style={{ display: "flex", justifyContent: "center", backgroundColor: "rgb(17, 17, 17)" }}> <div style={styles.container}>
<Stories <Stories
stories={stories} stories={stories}
defaultInterval={11000} defaultInterval={11000}

View File

@ -1,89 +0,0 @@
import { Form, Button, Card, Tabs, Tab } from "react-bootstrap"
import { useNavigate } from "react-router-dom";
import { useAuth } from "../hooks/api";
import { setToken } from "../utils/auth";
function LoginPage() {
const navigate = useNavigate()
const doAuth = useAuth()
const handleAuth = (newAccount) => (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')
}
const token = "a" // doAuth(data, newAccount)
setToken(token)
navigate("/")
}
return (
<Card className="m-4">
<Card.Body>
<Tabs defaultActiveKey="register" fill justify className="mb-3">
<Tab eventKey="register" title="Регистрация">
<Form onSubmit={handleAuth(true)}>
<Form.Group className="mb-3" controlId="email">
<Form.Label>Почта</Form.Label>
<Form.Control type="email" required />
</Form.Group>
<Form.Group className="mb-3" controlId="name">
<Form.Label>Имя</Form.Label>
<Form.Control type="text" required />
</Form.Group>
<Form.Group className="mb-3" controlId="surname">
<Form.Label>Фамилия</Form.Label>
<Form.Control type="text" required />
</Form.Group>
<Form.Group className="mb-3" controlId="password">
<Form.Label>Пароль</Form.Label>
<Form.Control type="password" required />
</Form.Group>
<Form.Group className="mb-3" controlId="privacyPolicyConsent">
<Form.Check type="checkbox" required label="Я согласен с условиями обработки персональных данных" />
</Form.Group>
<Button variant="success" type="submit">
Зарегистрироваться
</Button>
</Form>
</Tab>
<Tab eventKey="login" title="Вход">
<Form onSubmit={handleAuth(false)}>
<Form.Group className="mb-3" controlId="email">
<Form.Label>Почта</Form.Label>
<Form.Control type="email" required />
</Form.Group>
<Form.Group className="mb-3" controlId="password">
<Form.Label>Пароль</Form.Label>
<Form.Control type="password" required />
</Form.Group>
<Button variant="success" type="submit">
Войти
</Button>
</Form>
</Tab>
</Tabs>
</Card.Body>
</Card>
)
}
export default LoginPage

View File

@ -0,0 +1,51 @@
import { FormEventHandler } from 'react'
import { Card, Tabs, Tab } from 'react-bootstrap'
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../hooks/api';
import { setToken } from '../utils/auth';
import { AuthForm } from '../components';
function LoginPage() {
const navigate = useNavigate()
const { doAuth, loading, error } = useAuth()
const handleAuth = (newAccount: boolean): FormEventHandler<HTMLFormElement> => async (event) => {
event.preventDefault();
event.stopPropagation();
const formData = new FormData(event.currentTarget)
const data = {
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 = import.meta.env.PROD ? await doAuth(data, newAccount) : 'a'
if (token) {
setToken(token)
navigate(-1 - Number(import.meta.env.DEV))
}
}
return (
<Card className='m-4'>
<Card.Body>
<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='Вход'>
<AuthForm handleAuth={handleAuth(false)} register={false} loading={loading} error={error} />
</Tab>
</Tabs>
</Card.Body>
</Card>
)
}
export default LoginPage

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

View File

@ -1,13 +0,0 @@
const getToken = () => {
const token = localStorage.getItem("Token")
/* check expirity */
return token
}
const setToken = (token) => {
localStorage.setItem("Token", token)
}
export { getToken, setToken }

13
front/src/utils/auth.ts Normal file
View File

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

View File

@ -0,0 +1,19 @@
import { Announcement } from '../api/announcement/types'
const filterNames = ['userId', 'category', 'metro', 'bookedBy'] as const
type FilterNames = typeof filterNames[number]
type FiltersType = Partial<Pick<Announcement, FilterNames>>
const defaultFilters: FiltersType = { userId: undefined, category: undefined, metro: undefined, bookedBy: undefined }
const URLEncodeFilters = (filters: FiltersType) => (
Object.fromEntries(
filterNames.map(
fName => [fName, filters[fName]?.toString()]
).filter((p): p is [string, string] => typeof p[1] !== 'undefined')
)
)
export type { FilterNames, FiltersType }
export { defaultFilters, filterNames, URLEncodeFilters }

View File

@ -1,12 +0,0 @@
const removeNull = (obj) => Object.fromEntries(
Object.entries(obj)
.filter(([_, value]) => value != null)
.map(([key, value]) => [
key,
value === Object(value) ? removeNull(value) : value,
]),
)
const isAborted = (err) => err.name == 'AbortError'
export { removeNull, isAborted }

19
front/src/utils/index.ts Normal file
View File

@ -0,0 +1,19 @@
const isAborted = (err: Error) => (
err.name === 'AbortError'
)
function handleHTTPErrors(res: Response) {
if (!res.ok) {
switch (res.status) {
case 401:
throw new Error('Ошибка авторизации')
case 404:
throw new Error('Объект не найден')
default: {
throw new Error('Ошибка ответа от сервера')
}
}
}
}
export { isAborted, handleHTTPErrors }

View File

@ -0,0 +1,20 @@
import L from 'leaflet'
import itemMarker from '../assets/itemMarker.png'
import trashMarker from '../assets/trashMarker.png'
const iconItem = new L.Icon({
iconUrl: itemMarker,
iconRetinaUrl: itemMarker,
popupAnchor: [0, 0],
iconSize: [41, 41],
})
const iconTrash = new L.Icon({
iconUrl: trashMarker,
iconRetinaUrl: trashMarker,
popupAnchor: [0, 0],
iconSize: [34, 41],
})
export { iconItem, iconTrash }

View File

@ -1,7 +0,0 @@
import { stations } from "../assets/metro"
function lineByName(name) {
return Object.keys(stations).find(line => stations[line].has(name))
}
export { lineByName }

67
front/src/utils/types.ts Normal file
View File

@ -0,0 +1,67 @@
const isRecord = <K extends string | number | symbol>(obj: unknown): obj is Record<K, unknown> => (
typeof obj === 'object' &&
!Array.isArray(obj) &&
Object.getOwnPropertySymbols(obj).length === 0 && // We don't like symbols as keys here
obj !== null
)
type Primitive = 'bigint' | 'boolean' | 'function' | 'number' | 'object' | 'string' | 'symbol' | 'undefined'
type PropertyGuard = Primitive | `${Primitive}?` | ((obj: unknown) => boolean)
type PropertiesGuards = Record<
string | number | symbol,
PropertyGuard>
const isObject = <T>(obj: unknown, properties: PropertiesGuards): obj is T => (
typeof obj === 'object' &&
obj !== null &&
// Does not actually iterate over symbols. Hope, will not need to use them
Object.entries(properties).every(([name, guard]) => {
const param: unknown = Reflect.get(obj, name)
if (import.meta.env.DEV) {
console.debug('isObject', name, param, guard)
}
if (typeof guard === 'function') {
return guard(param)
}
if (guard[guard.length - 1] === '?')
return (
(param !== undefined && param !== null && typeof param === guard.slice(0, -1)) ||
param === undefined || param === null
)
return typeof param === guard
})
)
const isConst = <T>(val: T) => (obj: unknown): obj is T => (
obj === val ||
(
typeof obj === 'number' && isNaN(obj) &&
typeof val === 'number' && isNaN(val)
)
)
const isLiteralUnion = <T extends readonly string[]>(obj: unknown, list: T): obj is T[number] => (
typeof obj === 'string' &&
list.includes(obj as T[number])
)
const isArrayOf = <T>(obj: unknown, itemGuard: ((obj: unknown) => obj is T)): obj is T[] => (
Array.isArray(obj) &&
obj.every(itemGuard)
)
const isString = (obj: unknown): obj is string => (
typeof obj === 'string'
)
type SetState<T> = React.Dispatch<React.SetStateAction<T>>
export type { SetState }
export { isRecord, isObject, isConst, isLiteralUnion, isArrayOf, isString }

1
front/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

25
front/tsconfig.json Normal file
View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

10
front/tsconfig.node.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,49 @@
NAME=${1:-Route}
NAME=${NAME^}
mkdir -p src/api/${NAME,}
cat > src/api/${NAME,}/index.ts << EOF
import { API_URL } from '../../config'
import { ${NAME}Response, ${NAME} } from './types'
const initial${NAME}: ${NAME} = {}
const compose${NAME}URL = () => (
API_URL + '/${NAME,}?'
)
const process${NAME} = (data: ${NAME}Response): ${NAME} => {
return data
}
export { initial${NAME}, compose${NAME}URL, process${NAME} }
EOF
cat > src/api/${NAME,}/types.ts << EOF
import { isObject } from '../../utils/types'
type ${NAME}Response = {
}
const is${NAME}Response = (obj: unknown): obj is ${NAME}Response => (
isObject(obj, {
})
)
type ${NAME} = {
}
const is${NAME} = (obj: unknown): obj is ${NAME} => (
isObject(obj, {
})
)
export type { ${NAME}Response, ${NAME} }
export { is${NAME}Response, is${NAME} }
EOF