Compare commits
28 Commits
cb848739e5
...
Auth-code-
Author | SHA1 | Date | |
---|---|---|---|
6c6f96cc94 | |||
45d0b8e52d | |||
1055416640 | |||
52d9ad3399 | |||
30140f058f | |||
d5ba710885 | |||
6127dd8ba4 | |||
9e4bb1b99f | |||
d66b9004e0 | |||
626170964f | |||
1dd37a72b4 | |||
b39d9ada27 | |||
91842dcc51 | |||
09ba6a3478 | |||
de8a1abcbf
|
|||
898d590cf3 | |||
1c25d66027
|
|||
b7bbd937b4
|
|||
808edad6b4 | |||
b360b06d34 | |||
349d40daa4 | |||
8bae63231b | |||
3668e8c33f
|
|||
f4226237ab
|
|||
5b14d75048 | |||
0218cdced5
|
|||
99d2b92b03
|
|||
27ae590981
|
102
alembic.ini
Normal file
102
alembic.ini
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# path to migration scripts
|
||||||
|
script_location = migrations
|
||||||
|
|
||||||
|
# template used to generate migration files
|
||||||
|
# file_template = %%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# sys.path path, will be prepended to sys.path if present.
|
||||||
|
# defaults to the current working directory.
|
||||||
|
prepend_sys_path = .
|
||||||
|
|
||||||
|
# timezone to use when rendering the date within the migration file
|
||||||
|
# as well as the filename.
|
||||||
|
# If specified, requires the python-dateutil library that can be
|
||||||
|
# installed by adding `alembic[tz]` to the pip requirements
|
||||||
|
# string value is passed to dateutil.tz.gettz()
|
||||||
|
# leave blank for localtime
|
||||||
|
# timezone =
|
||||||
|
|
||||||
|
# max length of characters to apply to the
|
||||||
|
# "slug" field
|
||||||
|
# truncate_slug_length = 40
|
||||||
|
|
||||||
|
# set to 'true' to run the environment during
|
||||||
|
# the 'revision' command, regardless of autogenerate
|
||||||
|
# revision_environment = false
|
||||||
|
|
||||||
|
# set to 'true' to allow .pyc and .pyo files without
|
||||||
|
# a source .py file to be detected as revisions in the
|
||||||
|
# versions/ directory
|
||||||
|
# sourceless = false
|
||||||
|
|
||||||
|
# version location specification; This defaults
|
||||||
|
# to migrations/versions. When using multiple version
|
||||||
|
# directories, initial revisions must be specified with --version-path.
|
||||||
|
# The path separator used here should be the separator specified by "version_path_separator" below.
|
||||||
|
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
|
||||||
|
|
||||||
|
# version path separator; As mentioned above, this is the character used to split
|
||||||
|
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
||||||
|
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
||||||
|
# Valid values for version_path_separator are:
|
||||||
|
#
|
||||||
|
# version_path_separator = :
|
||||||
|
# version_path_separator = ;
|
||||||
|
# version_path_separator = space
|
||||||
|
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
|
||||||
|
|
||||||
|
# the output encoding used when revision files
|
||||||
|
# are written from script.py.mako
|
||||||
|
# output_encoding = utf-8
|
||||||
|
|
||||||
|
# sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||||
|
|
||||||
|
|
||||||
|
[post_write_hooks]
|
||||||
|
# post_write_hooks defines scripts or Python functions that are run
|
||||||
|
# on newly generated revision scripts. See the documentation for further
|
||||||
|
# detail and examples
|
||||||
|
|
||||||
|
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||||
|
# hooks = black
|
||||||
|
# black.type = console_scripts
|
||||||
|
# black.entrypoint = black
|
||||||
|
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
2
back/base.py
Normal file
2
back/base.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from .db import Base
|
||||||
|
from .models import UserDatabase, Announcement, Trashbox
|
14
back/db.py
14
back/db.py
@ -1,6 +1,13 @@
|
|||||||
from sqlalchemy import create_engine
|
from typing import AsyncGenerator
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
|
||||||
|
from sqlalchemy import create_engine, select
|
||||||
|
# from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
from sqlalchemy.orm import sessionmaker, Session
|
from sqlalchemy.orm import sessionmaker, Session
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
|
from fastapi import Depends
|
||||||
|
# from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase
|
||||||
|
|
||||||
|
|
||||||
SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
|
SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
|
||||||
|
|
||||||
@ -8,6 +15,7 @@ engine = create_engine(
|
|||||||
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
SessionLocal = sessionmaker(autoflush=True, bind=engine)
|
SessionLocal = sessionmaker(bind=engine, autoflush=True, autocommit=False)
|
||||||
|
|
||||||
|
database = SessionLocal()
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
70
back/main.py
70
back/main.py
@ -19,34 +19,47 @@ import shutil
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from .utils import *
|
from .utils import *
|
||||||
from .models import Announcement, Trashbox, UserDatabase, Base
|
from .db import Base, engine, SessionLocal, database
|
||||||
from .db import engine, SessionLocal
|
from .models import Announcement, Trashbox, UserDatabase
|
||||||
|
|
||||||
from . import schema
|
from . import schema
|
||||||
|
|
||||||
|
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
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("/static", StaticFiles(directory = "./front/dist"))
|
||||||
|
if not os.path.exists("./uploads"):
|
||||||
|
os.mkdir("C:/Users/38812/porridger/uploads")
|
||||||
app.mount("/uploads", StaticFiles(directory = "./uploads"))
|
app.mount("/uploads", StaticFiles(directory = "./uploads"))
|
||||||
|
|
||||||
|
|
||||||
|
# Функция, создающая сессию БД при каждом запросе к нашему API.
|
||||||
|
# Срабатывает до запуска остальных функций.
|
||||||
|
# Всегда закрывает сессию при окончании работы с ней
|
||||||
|
@app.middleware("http")
|
||||||
|
async def db_session_middleware(request: Request, call_next):
|
||||||
|
response = Response("Internal server error", status_code=500)
|
||||||
|
try:
|
||||||
|
request.state.db = SessionLocal()
|
||||||
|
response = await call_next(request)
|
||||||
|
finally:
|
||||||
|
request.state.db.close()
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/announcements")#адрес объявлений
|
@app.get("/api/announcements")#адрес объявлений
|
||||||
def annoncements_list(user_id: int = None, metro: str = None, category: str = None, booked_by: int = -1):
|
def annoncements_list(user_id: int = None, metro: str = None, category: str = None, booked_by: int = 0):
|
||||||
# Считываем данные из Body и отображаем их на странице.
|
# Считываем данные из Body и отображаем их на странице.
|
||||||
# В последствии будем вставлять данные в html-форму
|
# В последствии будем вставлять данные в html-форму
|
||||||
|
|
||||||
a = db.query(Announcement)
|
a = database.query(Announcement)
|
||||||
b = db.query(Announcement)
|
b = database.query(Announcement)
|
||||||
c = db.query(Announcement)
|
c = database.query(Announcement)
|
||||||
d = db.query(Announcement)
|
d = database.query(Announcement)
|
||||||
e = db.query(Announcement)
|
e = database.query(Announcement)
|
||||||
|
|
||||||
if user_id != None:
|
if user_id != None:
|
||||||
b = a.filter(Announcement.user_id == user_id)
|
b = a.filter(Announcement.user_id == user_id)
|
||||||
@ -74,7 +87,7 @@ def single_annoncement(user_id:int):
|
|||||||
# Считываем данные из Body и отображаем их на странице.
|
# Считываем данные из Body и отображаем их на странице.
|
||||||
# В последствии будем вставлять данные в html-форму
|
# В последствии будем вставлять данные в html-форму
|
||||||
try:
|
try:
|
||||||
annoncement = db.get(Announcement, user_id)
|
annoncement = database.get(Announcement, user_id)
|
||||||
return {"id": annoncement.id, "user_id": annoncement.user_id, "name": annoncement.name,
|
return {"id": annoncement.id, "user_id": annoncement.user_id, "name": annoncement.name,
|
||||||
"category": annoncement.category, "best_by": annoncement.best_by, "address": annoncement.address,
|
"category": annoncement.category, "best_by": annoncement.best_by, "address": annoncement.address,
|
||||||
"description": annoncement.description, "metro": annoncement.metro, "latitude": annoncement.latitude,
|
"description": annoncement.description, "metro": annoncement.metro, "latitude": annoncement.latitude,
|
||||||
@ -86,7 +99,7 @@ def single_annoncement(user_id:int):
|
|||||||
|
|
||||||
# Занести объявление в базу
|
# Занести объявление в базу
|
||||||
@app.put("/api/announcement")#адрес объявлений
|
@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):
|
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):
|
||||||
# try:
|
# try:
|
||||||
userId = 1 # temporary
|
userId = 1 # temporary
|
||||||
|
|
||||||
@ -102,7 +115,7 @@ def put_in_db(name: Annotated[str, Form()], category: Annotated[str, Form()], be
|
|||||||
|
|
||||||
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)
|
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.add(temp_ancmt) # добавляем в бд
|
||||||
db.commit() # сохраняем изменения
|
db.commit() # сохраняем изменения
|
||||||
db.refresh(temp_ancmt) # обновляем состояние объекта
|
db.refresh(temp_ancmt) # обновляем состояние объекта
|
||||||
@ -115,8 +128,8 @@ def put_in_db(name: Annotated[str, Form()], category: Annotated[str, Form()], be
|
|||||||
@app.delete("/api/announcement") #адрес объявления
|
@app.delete("/api/announcement") #адрес объявления
|
||||||
def delete_from_db(data = Body()):#функция удаления объекта из БД
|
def delete_from_db(data = Body()):#функция удаления объекта из БД
|
||||||
try:
|
try:
|
||||||
db.delete(user_id=data.user_id)#удаление из БД
|
database.delete(user_id=data.user_id)#удаление из БД
|
||||||
db.commit() # сохраняем изменения
|
database.commit() # сохраняем изменения
|
||||||
return {"Answer" : True}
|
return {"Answer" : True}
|
||||||
except:
|
except:
|
||||||
return {"Answer" : False}
|
return {"Answer" : False}
|
||||||
@ -129,21 +142,21 @@ def change_book_status(data: schema.Book):
|
|||||||
# Получаем id пользователя, который бронирует объявление
|
# Получаем id пользователя, который бронирует объявление
|
||||||
temp_user_id = 1
|
temp_user_id = 1
|
||||||
# Находим объявление по данному id
|
# Находим объявление по данному id
|
||||||
announcement_to_change = db.query(Announcement).filter(id == data.id).first()
|
announcement_to_change = database.query(Announcement).filter(id == data.id).first()
|
||||||
# Изменяем поле booked_status на полученный id
|
# Изменяем поле booked_status на полученный id
|
||||||
announcement_to_change.booked_status = temp_user_id
|
announcement_to_change.booked_status = temp_user_id
|
||||||
return {"Success": True}
|
return {"Success": True}
|
||||||
except:
|
except:
|
||||||
return {"Success": False}
|
return {"Success": False}
|
||||||
|
|
||||||
|
# reginstration
|
||||||
@app.post("/api/signup")
|
@app.post("/api/signup")
|
||||||
def create_user(data = Body()):
|
def create_user(data = Body()):
|
||||||
if db.query(UserDatabase).filter(User.email == data["email"]).first() == None:
|
if database.query(UserDatabase).filter(UserDatabase.email == data["email"]).first() == None:
|
||||||
new_user = UserDatabase(id=data["id"], email=data["email"], password=data["password"], name=data["name"], surname=data["surname"])
|
new_user = UserDatabase(id=data["id"], email=data["email"], password=data["password"], name=data["name"], surname=data["surname"])
|
||||||
db.add(new_user)
|
database.add(new_user)
|
||||||
db.commit()
|
database.commit()
|
||||||
db.refresh(new_user) # обновляем состояние объекта
|
database.refresh(new_user) # обновляем состояние объекта
|
||||||
return {"Success": True}
|
return {"Success": True}
|
||||||
return {"Success": False, "Message": "Пользователь с таким email уже зарегестрирован."}
|
return {"Success": False, "Message": "Пользователь с таким email уже зарегестрирован."}
|
||||||
|
|
||||||
@ -152,7 +165,8 @@ def create_user(data = Body()):
|
|||||||
async def login_for_access_token(
|
async def login_for_access_token(
|
||||||
form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
|
form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
|
||||||
):
|
):
|
||||||
user = authenticate_user(db.query(UserDatabase).all(), form_data.username, form_data.password)
|
# разобраться с первым параметром
|
||||||
|
user = authenticate_user(database, form_data.username, form_data.password)
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
@ -163,11 +177,11 @@ async def login_for_access_token(
|
|||||||
access_token = create_access_token(
|
access_token = create_access_token(
|
||||||
data={"user_id": user.id}, expires_delta=access_token_expires
|
data={"user_id": user.id}, expires_delta=access_token_expires
|
||||||
)
|
)
|
||||||
return {"access_token": access_token, "token_type": "bearer"}
|
return access_token
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/users/me/", response_model=User)
|
@app.get("/api/users/me/", response_model=schema.User)
|
||||||
async def read_users_me(
|
async def read_users_me( #!!!!!!!!!!!
|
||||||
current_user: Annotated[User, Depends(get_current_active_user)]
|
current_user: Annotated[User, Depends(get_current_active_user)]
|
||||||
):
|
):
|
||||||
return current_user
|
return current_user
|
||||||
@ -184,7 +198,7 @@ async def read_own_items(
|
|||||||
@app.get("/api/trashbox")
|
@app.get("/api/trashbox")
|
||||||
def get_trashboxes(lat:float, lng:float):#крутая функция для работы с api
|
def get_trashboxes(lat:float, lng:float):#крутая функция для работы с api
|
||||||
BASE_URL='https://geointelect2.gate.petersburg.ru'#адрес сайта и мой токин
|
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'
|
my_token='eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJhU1RaZm42bHpTdURYcUttRkg1SzN5UDFhT0FxUkhTNm9OendMUExaTXhFIn0.eyJleHAiOjE3ODM3ODk4NjgsImlhdCI6MTY4OTA5NTQ2OCwianRpIjoiNDUzNjQzZTgtYTkyMi00NTI4LWIzYmMtYWJiYTNmYjkyNTkxIiwiaXNzIjoiaHR0cHM6Ly9rYy5wZXRlcnNidXJnLnJ1L3JlYWxtcy9lZ3MtYXBpIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImJjYjQ2NzljLTU3ZGItNDU5ZC1iNWUxLWRlOGI4Yzg5MTMwMyIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFkbWluLXJlc3QtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6ImM2ZDJiOTZhLWMxNjMtNDAxZS05ZjMzLTI0MmE0NDcxMDY5OCIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiLyoiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtZWdzLWFwaSIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiJjNmQyYjk2YS1jMTYzLTQwMWUtOWYzMy0yNDJhNDQ3MTA2OTgiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiLQktC70LDQtNC40LzQuNGAINCv0LrQvtCy0LvQtdCyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiZTBmYzc2OGRhOTA4MjNiODgwZGQzOGVhMDJjMmQ5NTciLCJnaXZlbl9uYW1lIjoi0JLQu9Cw0LTQuNC80LjRgCIsImZhbWlseV9uYW1lIjoi0K_QutC-0LLQu9C10LIifQ.E2bW0B-c6W5Lj63eP_G8eI453NlDMnW05l11TZT0GSsAtGayXGaolHtWrmI90D5Yxz7v9FGkkCmcUZYy1ywAdO9dDt_XrtFEJWFpG-3csavuMjXmqfQQ9SmPwDw-3toO64NuZVv6qVqoUlPPj57sLx4bLtVbB4pdqgyJYcrDHg7sgwz4d1Z3tAeUfSpum9s5ZfELequfpLoZMXn6CaYZhePaoK-CxeU3KPBPTPOVPKZZ19s7QY10VdkxLULknqf9opdvLs4j8NMimtwoIiHNBFlgQz10Cr7bhDKWugfvSRsICouniIiBJo76wrj5T92s-ztf1FShJuqnQcKE_QLd2A'
|
||||||
head = {'Authorization': 'Bearer {}'.format(my_token)}
|
head = {'Authorization': 'Bearer {}'.format(my_token)}
|
||||||
|
|
||||||
my_data={
|
my_data={
|
||||||
|
@ -1,12 +1,18 @@
|
|||||||
from sqlalchemy import Column, Integer, String
|
from sqlalchemy import Column, Integer, String
|
||||||
|
from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase
|
||||||
|
from fastapi import Depends
|
||||||
from .db import Base
|
from .db import Base
|
||||||
|
|
||||||
|
|
||||||
|
class User(SQLAlchemyBaseUserTableUUID, Base):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class UserDatabase(Base):#класс пользователя
|
class UserDatabase(Base):#класс пользователя
|
||||||
__tablename__ = "users"
|
__tablename__ = "users"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)#айди пользователя
|
id = Column(Integer, primary_key=True, index=True, unique=True)#айди пользователя
|
||||||
phone = Column(Integer, nullable=True)#номер телефона пользователя
|
phone = Column(Integer, nullable=True)#номер телефона пользователя
|
||||||
email = Column(String)#электронная почта пользователя
|
email = Column(String)#электронная почта пользователя
|
||||||
password = Column(String) # пароль
|
password = Column(String) # пароль
|
||||||
@ -43,3 +49,26 @@ class Trashbox(Base):#класс мусорных баков
|
|||||||
longtitude = Column(Integer)
|
longtitude = Column(Integer)
|
||||||
category = Column(String)#категория продукта из объявления
|
category = Column(String)#категория продукта из объявления
|
||||||
|
|
||||||
|
|
||||||
|
class Poems(Base):#класс поэзии
|
||||||
|
__tablename__ = "poems"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True) #айди
|
||||||
|
poem_text = Column(String) # текст стихотворения
|
||||||
|
|
||||||
|
# from typing import AsyncGenerator
|
||||||
|
# from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
|
# from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase
|
||||||
|
# # This function can be called during the initialization of the FastAPI app.
|
||||||
|
# async def create_db_and_tables():
|
||||||
|
# async with engine.begin() as conn:
|
||||||
|
# await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
|
|
||||||
|
# async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
|
||||||
|
# async with async_session_maker() as session:
|
||||||
|
# yield session
|
||||||
|
|
||||||
|
|
||||||
|
# async def get_user_db(session: AsyncSession = Depends(get_async_session)):
|
||||||
|
# yield SQLAlchemyUserDatabase(session, User)
|
@ -1,5 +1,29 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from typing import Annotated, Union
|
||||||
|
|
||||||
class Book(BaseModel):
|
class Book(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
|
|
||||||
|
|
||||||
|
class Token(BaseModel):
|
||||||
|
access_token: str
|
||||||
|
token_type: str
|
||||||
|
|
||||||
|
|
||||||
|
class TokenData(BaseModel):
|
||||||
|
email: Union[str, None] = None
|
||||||
|
|
||||||
|
|
||||||
|
class User(BaseModel):
|
||||||
|
id: int
|
||||||
|
phone: Union[int, None] = None
|
||||||
|
email: str
|
||||||
|
name: Union[str, None] = None
|
||||||
|
surname: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
class UserInDB(User):
|
||||||
|
password: str
|
||||||
|
hashed_password: str
|
2
back/service.py
Normal file
2
back/service.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
117
back/utils.py
117
back/utils.py
@ -1,96 +1,24 @@
|
|||||||
# from passlib.context import CryptContext
|
|
||||||
# import os
|
|
||||||
# from datetime import datetime, timedelta
|
|
||||||
# from typing import Union, Any
|
|
||||||
# from jose import jwt
|
|
||||||
|
|
||||||
# ACCESS_TOKEN_EXPIRE_MINUTES = 30 # 30 minutes
|
|
||||||
# REFRESH_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days
|
|
||||||
# ALGORITHM = "HS256"
|
|
||||||
# # В предположении, что попыток взлома не будет, возьмем простейший ключ
|
|
||||||
# JWT_SECRET_KEY = "secret key" # может также быть os.environ["JWT_SECRET_KEY"]
|
|
||||||
# JWT_REFRESH_SECRET_KEY = "refresh secret key" # может также быть os.environ["JWT_REFRESH_SECRET_KEY"]
|
|
||||||
|
|
||||||
|
|
||||||
# def get_hashed_password(password: str) -> str:
|
|
||||||
# return password_context.hash(password)
|
|
||||||
|
|
||||||
|
|
||||||
# def verify_password(password: str, hashed_pass: str) -> bool:
|
|
||||||
# return password_context.verify(password, hashed_pass)
|
|
||||||
|
|
||||||
|
|
||||||
# def create_access_token(subject: Union[str, Any], expires_delta: int = None) -> str:
|
|
||||||
# if expires_delta is not None:
|
|
||||||
# expires_delta = datetime.utcnow() + expires_delta
|
|
||||||
# else:
|
|
||||||
# expires_delta = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
|
||||||
|
|
||||||
# to_encode = {"exp": expires_delta, "sub": str(subject)}
|
|
||||||
# encoded_jwt = jwt.encode(to_encode, JWT_SECRET_KEY, ALGORITHM)
|
|
||||||
# return encoded_jwt
|
|
||||||
|
|
||||||
# def create_refresh_token(subject: Union[str, Any], expires_delta: int = None) -> str:
|
|
||||||
# if expires_delta is not None:
|
|
||||||
# expires_delta = datetime.utcnow() + expires_delta
|
|
||||||
# else:
|
|
||||||
# expires_delta = datetime.utcnow() + timedelta(minutes=REFRESH_TOKEN_EXPIRE_MINUTES)
|
|
||||||
|
|
||||||
# to_encode = {"exp": expires_delta, "sub": str(subject)}
|
|
||||||
# encoded_jwt = jwt.encode(to_encode, JWT_REFRESH_SECRET_KEY, ALGORITHM)
|
|
||||||
# return encoded_jwt
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Annotated, Union
|
from typing import Annotated, Union
|
||||||
|
|
||||||
from fastapi import Depends, FastAPI, HTTPException, status
|
from fastapi import Depends, FastAPI, HTTPException, status, Response, Request
|
||||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||||
from jose import JWTError, jwt
|
from jose import JWTError, jwt
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
# to get a string like this run:
|
from sqlalchemy.orm import Session
|
||||||
# openssl rand -hex 32
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from .db import Session, database
|
||||||
|
from .models import UserDatabase
|
||||||
|
|
||||||
|
from .schema import Token, TokenData, UserInDB, User
|
||||||
|
|
||||||
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
|
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
|
||||||
ALGORITHM = "HS256"
|
ALGORITHM = "HS256"
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
||||||
|
|
||||||
|
|
||||||
# fake_users_db = {
|
|
||||||
# "johndoe": {
|
|
||||||
# "email": "johndoe",
|
|
||||||
# "full_name": "John Doe",
|
|
||||||
# "email": "johndoe@example.com",
|
|
||||||
# "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
|
|
||||||
# "disabled": False,
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
|
|
||||||
|
|
||||||
class Token(BaseModel):
|
|
||||||
access_token: str
|
|
||||||
token_type: str
|
|
||||||
|
|
||||||
|
|
||||||
class TokenData(BaseModel):
|
|
||||||
email: Union[str, None] = None
|
|
||||||
|
|
||||||
|
|
||||||
class User(BaseModel):
|
|
||||||
email: str
|
|
||||||
email: Union[str, None] = None
|
|
||||||
# password: str
|
|
||||||
# password: Union[str, None] = None
|
|
||||||
full_name: Union[str, None] = None
|
|
||||||
disabled: Union[bool, None] = None
|
|
||||||
|
|
||||||
|
|
||||||
class UserInDB(User):
|
|
||||||
hashed_password: str
|
|
||||||
|
|
||||||
|
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
||||||
@ -103,17 +31,16 @@ def get_password_hash(password):
|
|||||||
return pwd_context.hash(password)
|
return pwd_context.hash(password)
|
||||||
|
|
||||||
|
|
||||||
def get_user(db, email: str):
|
# проблема здесь
|
||||||
user = None
|
def get_user(db: Session, email: str):
|
||||||
for person_with_correct_email in db:
|
user_with_required_email = db.query(UserDatabase).filter(UserDatabase.email == email).first()
|
||||||
if person_with_correct_email.email == email:
|
print(user_with_required_email)
|
||||||
user = person_with_correct_email
|
if user_with_required_email:
|
||||||
break
|
return user_with_required_email
|
||||||
return user #UserInDB(user_email)
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate_user(db: Session, email: str, password: str):
|
||||||
def authenticate_user(db, email: str, password: str):
|
|
||||||
user = get_user(db, email)
|
user = get_user(db, email)
|
||||||
if not user:
|
if not user:
|
||||||
return False
|
return False
|
||||||
@ -133,7 +60,7 @@ def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None
|
|||||||
return encoded_jwt
|
return encoded_jwt
|
||||||
|
|
||||||
|
|
||||||
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
|
async def get_current_user(db: Session, token: Annotated[str, Depends(oauth2_scheme)]):
|
||||||
credentials_exception = HTTPException(
|
credentials_exception = HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Could not validate credentials",
|
detail="Could not validate credentials",
|
||||||
@ -147,8 +74,8 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
|
|||||||
token_data = TokenData(email=email)
|
token_data = TokenData(email=email)
|
||||||
except JWTError:
|
except JWTError:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
user = get_user(fake_users_db, email=token_data.email)
|
user = get_user(db, email=token_data.email)
|
||||||
if user is None:
|
if user == None:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@ -159,3 +86,7 @@ async def get_current_active_user(
|
|||||||
if current_user.disabled:
|
if current_user.disabled:
|
||||||
raise HTTPException(status_code=400, detail="Inactive user")
|
raise HTTPException(status_code=400, detail="Inactive user")
|
||||||
return current_user
|
return current_user
|
||||||
|
|
||||||
|
|
||||||
|
def get_db(request: Request):
|
||||||
|
return request.state.db
|
@ -5,8 +5,6 @@
|
|||||||
<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>
|
||||||
|
@ -7,7 +7,9 @@
|
|||||||
"dev": "vite --host 0.0.0.0",
|
"dev": "vite --host 0.0.0.0",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"lint": "eslint src --ext ts,tsx --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": {
|
||||||
"@types/leaflet": "^1.9.3",
|
"@types/leaflet": "^1.9.3",
|
||||||
|
@ -17,7 +17,8 @@ type AnnouncementResponse = {
|
|||||||
booked_by: number
|
booked_by: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAnnouncementResponse = (obj: unknown): obj is AnnouncementResponse => isObject(obj, {
|
const isAnnouncementResponse = (obj: unknown): obj is AnnouncementResponse => (
|
||||||
|
isObject(obj, {
|
||||||
'id': 'number',
|
'id': 'number',
|
||||||
'user_id': 'number',
|
'user_id': 'number',
|
||||||
'name': 'string',
|
'name': 'string',
|
||||||
@ -31,7 +32,8 @@ const isAnnouncementResponse = (obj: unknown): obj is AnnouncementResponse => is
|
|||||||
'metro': 'string',
|
'metro': 'string',
|
||||||
'trashId': 'number?',
|
'trashId': 'number?',
|
||||||
'booked_by': 'number'
|
'booked_by': 'number'
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
type Announcement = {
|
type Announcement = {
|
||||||
id: number,
|
id: number,
|
||||||
|
@ -5,8 +5,9 @@ import { AnnouncementsResponse } from './types'
|
|||||||
|
|
||||||
const initialAnnouncements: Announcement[] = []
|
const initialAnnouncements: Announcement[] = []
|
||||||
|
|
||||||
const composeAnnouncementsURL = (filters: FiltersType) =>
|
const composeAnnouncementsURL = (filters: FiltersType) => (
|
||||||
API_URL + '/announcements?' + new URLSearchParams(URLEncodeFilters(filters)).toString()
|
API_URL + '/announcements?' + new URLSearchParams(URLEncodeFilters(filters)).toString()
|
||||||
|
)
|
||||||
|
|
||||||
const processAnnouncements = (data: AnnouncementsResponse): Announcement[] => {
|
const processAnnouncements = (data: AnnouncementsResponse): Announcement[] => {
|
||||||
const annList = data.list_of_announcements
|
const annList = data.list_of_announcements
|
||||||
|
@ -6,10 +6,12 @@ type AnnouncementsResponse = {
|
|||||||
Success: boolean
|
Success: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAnnouncementsResponse = (obj: unknown): obj is AnnouncementsResponse => isObject(obj, {
|
const isAnnouncementsResponse = (obj: unknown): obj is AnnouncementsResponse => (
|
||||||
|
isObject(obj, {
|
||||||
'list_of_announcements': obj => isArrayOf<AnnouncementResponse>(obj, isAnnouncementResponse),
|
'list_of_announcements': obj => isArrayOf<AnnouncementResponse>(obj, isAnnouncementResponse),
|
||||||
'Success': 'boolean'
|
'Success': 'boolean'
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
AnnouncementsResponse,
|
AnnouncementsResponse,
|
||||||
|
@ -3,10 +3,12 @@ import { OsmAddressResponse } from './types'
|
|||||||
|
|
||||||
const initialOsmAddress = ''
|
const initialOsmAddress = ''
|
||||||
|
|
||||||
const composeOsmAddressURL = (addressPosition: LatLng) =>
|
const composeOsmAddressURL = (addressPosition: LatLng) => (
|
||||||
`${location.protocol}//nominatim.openstreetmap.org/reverse?format=json&accept-language=ru&lat=${addressPosition.lat}&lon=${addressPosition.lng}`
|
`${location.protocol}//nominatim.openstreetmap.org/reverse?format=json&accept-language=ru&lat=${addressPosition.lat}&lon=${addressPosition.lng}`
|
||||||
|
)
|
||||||
|
|
||||||
const processOsmAddress = (data: OsmAddressResponse): string =>
|
const processOsmAddress = (data: OsmAddressResponse): string => (
|
||||||
data.display_name
|
data.display_name
|
||||||
|
)
|
||||||
|
|
||||||
export { initialOsmAddress, composeOsmAddressURL, processOsmAddress }
|
export { initialOsmAddress, composeOsmAddressURL, processOsmAddress }
|
||||||
|
@ -4,9 +4,11 @@ type OsmAddressResponse = {
|
|||||||
display_name: string
|
display_name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOsmAddressResponse = (obj: unknown): obj is OsmAddressResponse => isObject(obj, {
|
const isOsmAddressResponse = (obj: unknown): obj is OsmAddressResponse => (
|
||||||
|
isObject(obj, {
|
||||||
'display_name': 'string',
|
'display_name': 'string',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
OsmAddressResponse,
|
OsmAddressResponse,
|
||||||
|
12
front/src/api/putAnnouncement/index.ts
Normal file
12
front/src/api/putAnnouncement/index.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { API_URL } from '../../config'
|
||||||
|
import { PutAnnouncement, PutAnnouncementResponse } from './types'
|
||||||
|
|
||||||
|
const composePutAnnouncementURL = () => (
|
||||||
|
API_URL + '/announcement?'
|
||||||
|
)
|
||||||
|
|
||||||
|
const processPutAnnouncement = (data: PutAnnouncementResponse): PutAnnouncement => {
|
||||||
|
return data.Answer
|
||||||
|
}
|
||||||
|
|
||||||
|
export { composePutAnnouncementURL, processPutAnnouncement }
|
17
front/src/api/putAnnouncement/types.ts
Normal file
17
front/src/api/putAnnouncement/types.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { isObject } from '../../utils/types'
|
||||||
|
|
||||||
|
type PutAnnouncementResponse = {
|
||||||
|
Answer: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPutAnnouncementResponse = (obj: unknown): obj is PutAnnouncementResponse => (
|
||||||
|
isObject(obj, {
|
||||||
|
'Answer': 'boolean'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
type PutAnnouncement = boolean
|
||||||
|
|
||||||
|
export type { PutAnnouncementResponse, PutAnnouncement }
|
||||||
|
|
||||||
|
export { isPutAnnouncementResponse }
|
@ -1,11 +1,16 @@
|
|||||||
import { LatLng } from 'leaflet'
|
import { LatLng } from 'leaflet'
|
||||||
|
|
||||||
import { API_URL } from '../../config'
|
import { API_URL } from '../../config'
|
||||||
|
import { Trashbox, TrashboxResponse } from './types'
|
||||||
|
|
||||||
const composeTrashboxURL = (position: LatLng) =>
|
const composeTrashboxURL = (position: LatLng) => (
|
||||||
API_URL + '/trashbox?' + new URLSearchParams({
|
API_URL + '/trashbox?' + new URLSearchParams({
|
||||||
lat: position.lat.toString(),
|
lat: position.lat.toString(),
|
||||||
lng: position.lng.toString()
|
lng: position.lng.toString()
|
||||||
}).toString()
|
}).toString()
|
||||||
|
)
|
||||||
|
|
||||||
export { composeTrashboxURL }
|
const processTrashbox = (data: TrashboxResponse): Trashbox[] =>
|
||||||
|
data
|
||||||
|
|
||||||
|
export { composeTrashboxURL, processTrashbox }
|
||||||
|
@ -7,16 +7,20 @@ type Trashbox = {
|
|||||||
Categories: string[]
|
Categories: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTrashbox = (obj: unknown): obj is Trashbox => isObject(obj, {
|
const isTrashbox = (obj: unknown): obj is Trashbox => (
|
||||||
|
isObject(obj, {
|
||||||
'Lat': 'number',
|
'Lat': 'number',
|
||||||
'Lng': 'number',
|
'Lng': 'number',
|
||||||
'Address': 'string',
|
'Address': 'string',
|
||||||
'Categories': obj => isArrayOf<string>(obj, isString)
|
'Categories': obj => isArrayOf<string>(obj, isString)
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
type TrashboxResponse = Trashbox[]
|
type TrashboxResponse = Trashbox[]
|
||||||
|
|
||||||
const isTrashboxResponse = (obj: unknown): obj is Trashbox[] => isArrayOf(obj, isTrashbox)
|
const isTrashboxResponse = (obj: unknown): obj is Trashbox[] => (
|
||||||
|
isArrayOf(obj, isTrashbox)
|
||||||
|
)
|
||||||
|
|
||||||
export type { Trashbox, TrashboxResponse }
|
export type { Trashbox, TrashboxResponse }
|
||||||
export { isTrashbox, isTrashboxResponse }
|
export { isTrashbox, isTrashboxResponse }
|
||||||
|
@ -4,7 +4,9 @@ const categories = ['PORRIDGE', 'conspects', 'milk', 'bred', 'wathing', 'cloth',
|
|||||||
'fruits_vegatables', 'soup', 'dinner', 'conserves', 'pens', 'other_things'] as const
|
'fruits_vegatables', 'soup', 'dinner', 'conserves', 'pens', 'other_things'] as const
|
||||||
type Category = typeof categories[number]
|
type Category = typeof categories[number]
|
||||||
|
|
||||||
const isCategory = (obj: unknown): obj is Category => isLiteralUnion(obj, categories)
|
const isCategory = (obj: unknown): obj is Category => (
|
||||||
|
isLiteralUnion(obj, categories)
|
||||||
|
)
|
||||||
|
|
||||||
const categoryGraphics: Record<Category, string> = {
|
const categoryGraphics: Record<Category, string> = {
|
||||||
'PORRIDGE': 'static/PORRIDGE.jpg',
|
'PORRIDGE': 'static/PORRIDGE.jpg',
|
||||||
|
@ -101,9 +101,9 @@ const lineNames: Record<Lines, string> = {
|
|||||||
violet: 'Фиолетовая',
|
violet: 'Фиолетовая',
|
||||||
}
|
}
|
||||||
|
|
||||||
const lineByName = (name: string) =>
|
const lineByName = (name: string) => (
|
||||||
lines.find(line => stations[line].has(name))
|
lines.find(line => stations[line].has(name))
|
||||||
|
)
|
||||||
|
|
||||||
export type { Lines }
|
export type { Lines }
|
||||||
export { lines, stations, colors, lineNames, lineByName }
|
export { lines, stations, colors, lineNames, lineByName }
|
||||||
|
@ -6,19 +6,28 @@ import { categoryNames } from '../assets/category'
|
|||||||
import { useBook } from '../hooks/api'
|
import { useBook } from '../hooks/api'
|
||||||
import { Announcement } from '../api/announcement/types'
|
import { Announcement } from '../api/announcement/types'
|
||||||
import { iconItem } from '../utils/markerIcons'
|
import { iconItem } from '../utils/markerIcons'
|
||||||
|
import { CSSProperties } from 'react'
|
||||||
|
|
||||||
type AnnouncementDetailsProps = {
|
type AnnouncementDetailsProps = {
|
||||||
close: () => void,
|
close: () => void,
|
||||||
announcement: Announcement
|
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) {
|
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}>
|
||||||
|
@ -8,8 +8,9 @@ type AuthFormProps = {
|
|||||||
error: string
|
error: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthForm = ({ handleAuth, register, loading, error }: AuthFormProps) => {
|
function AuthForm ({ handleAuth, register, loading, error }: AuthFormProps) {
|
||||||
const buttonText = loading ? 'Загрузка...' : (error || (register ? 'Зарегистрироваться' : 'Войти'))
|
const buttonText = loading ? 'Загрузка...' : (error || (register ? 'Зарегистрироваться' : 'Войти'))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleAuth}>
|
<Form onSubmit={handleAuth}>
|
||||||
<Form.Group className='mb-3' controlId='email'>
|
<Form.Group className='mb-3' controlId='email'>
|
||||||
|
@ -3,28 +3,31 @@ import { Link } from 'react-router-dom'
|
|||||||
import addIcon from '../assets/addIcon.svg'
|
import addIcon from '../assets/addIcon.svg'
|
||||||
import filterIcon from '../assets/filterIcon.svg'
|
import filterIcon from '../assets/filterIcon.svg'
|
||||||
import userIcon from '../assets/userIcon.svg'
|
import userIcon from '../assets/userIcon.svg'
|
||||||
|
import { CSSProperties } from 'react'
|
||||||
|
|
||||||
const navBarStyles: React.CSSProperties = {
|
const styles = {
|
||||||
|
navBar: {
|
||||||
backgroundColor: 'var(--bs-success)',
|
backgroundColor: 'var(--bs-success)',
|
||||||
height: 56,
|
height: 56,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
}
|
} as CSSProperties,
|
||||||
|
navBarGroup: {
|
||||||
const navBarGroupStyles: React.CSSProperties = {
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
margin: 'auto'
|
margin: 'auto'
|
||||||
}
|
} as CSSProperties,
|
||||||
|
navBarElement: {
|
||||||
const navBarElementStyles: React.CSSProperties = {
|
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center'
|
justifyContent: 'center'
|
||||||
|
} as CSSProperties,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
type BottomNavBarProps = {
|
type BottomNavBarProps = {
|
||||||
width: number,
|
width: number,
|
||||||
toggleFilters: (p: boolean) => void
|
toggleFilters: (p: boolean) => void
|
||||||
@ -32,18 +35,18 @@ type BottomNavBarProps = {
|
|||||||
|
|
||||||
function BottomNavBar({ width, toggleFilters }: BottomNavBarProps) {
|
function BottomNavBar({ width, toggleFilters }: BottomNavBarProps) {
|
||||||
return (
|
return (
|
||||||
<div style={navBarStyles}>
|
<div style={styles.navBar}>
|
||||||
<div style={{ ...navBarGroupStyles, width: width }}>
|
<div style={{ ...styles.navBarGroup, width: width }}>
|
||||||
|
|
||||||
<a style={navBarElementStyles} onClick={() => toggleFilters(true)}>
|
<a style={styles.navBarElement} onClick={() => toggleFilters(true)}>
|
||||||
<img src={filterIcon} alt='Фильтровать объявления' title='Фильтровать объявления' />
|
<img src={filterIcon} alt='Фильтровать объявления' title='Фильтровать объявления' />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<Link style={navBarElementStyles} to='/add' >
|
<Link style={styles.navBarElement} to='/add' >
|
||||||
<img src={addIcon} alt='Опубликовать объявление' title='Опубликовать объявление' />
|
<img src={addIcon} alt='Опубликовать объявление' title='Опубликовать объявление' />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link style={navBarElementStyles} to={'/user'} >
|
<Link style={styles.navBarElement} to={'/user'} >
|
||||||
<img src={userIcon} alt='Личный кабинет' title='Личный кабинет' />
|
<img src={userIcon} alt='Личный кабинет' title='Личный кабинет' />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ type LocationMarkerProps = {
|
|||||||
setPosition: SetState<LatLng>
|
setPosition: SetState<LatLng>
|
||||||
}
|
}
|
||||||
|
|
||||||
const LocationMarker = ({ address, position, setPosition }: LocationMarkerProps) => {
|
function LocationMarker({ address, position, setPosition }: LocationMarkerProps) {
|
||||||
|
|
||||||
const map = useMapEvents({
|
const map = useMapEvents({
|
||||||
dragend: () => {
|
dragend: () => {
|
||||||
|
@ -8,7 +8,7 @@ type TrashboxMarkersProps = {
|
|||||||
selectTrashbox: ({ index, category }: { index: number, category: string }) => void
|
selectTrashbox: ({ index, category }: { index: number, category: string }) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const TrashboxMarkers = ({ trashboxes, selectTrashbox }: TrashboxMarkersProps) => {
|
function TrashboxMarkers({ trashboxes, selectTrashbox }: TrashboxMarkersProps) {
|
||||||
return (
|
return (
|
||||||
<>{trashboxes.map((trashbox, index) => (
|
<>{trashboxes.map((trashbox, index) => (
|
||||||
<Marker icon={iconTrash} key={`${trashbox.Lat}${trashbox.Lng}`} position={[trashbox.Lat, trashbox.Lng]}>
|
<Marker icon={iconTrash} key={`${trashbox.Lat}${trashbox.Lng}`} position={[trashbox.Lat, trashbox.Lng]}>
|
||||||
|
@ -1,77 +1,31 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
import { API_URL } from '../../config'
|
|
||||||
import { isLiteralUnion } from '../../utils/types'
|
|
||||||
import { handleHTTPErrors } from '../../utils'
|
|
||||||
|
|
||||||
const addErrors = ['Не удалось опубликовать объявление', 'Неверный ответ от сервера', 'Неизвестная ошибка'] as const
|
|
||||||
type AddError = typeof addErrors[number]
|
|
||||||
|
|
||||||
const isAddError = (obj: unknown): obj is AddError => isLiteralUnion(obj, addErrors)
|
|
||||||
|
|
||||||
const buttonStates = ['Опубликовать', 'Загрузка...', 'Опубликовано', 'Отменено'] as const
|
|
||||||
type ButtonState = typeof buttonStates[number] | AddError
|
|
||||||
|
|
||||||
type AddResponse = {
|
|
||||||
Answer: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAddResponse = (obj: unknown): obj is AddResponse =>
|
|
||||||
typeof obj === 'object' && obj !== null && typeof Reflect.get(obj, 'Answer') === 'boolean'
|
|
||||||
|
|
||||||
|
import { useSend } from '..'
|
||||||
|
import { composePutAnnouncementURL, processPutAnnouncement } from '../../api/putAnnouncement'
|
||||||
|
import { isPutAnnouncementResponse } from '../../api/putAnnouncement/types'
|
||||||
|
import useSendButtonCaption from '../useSendButtonCaption'
|
||||||
|
|
||||||
const useAddAnnouncement = () => {
|
const useAddAnnouncement = () => {
|
||||||
const [status, setStatus] = useState<ButtonState>('Опубликовать')
|
const { doSend, loading, error } = useSend(
|
||||||
|
composePutAnnouncementURL(),
|
||||||
|
'PUT',
|
||||||
|
true,
|
||||||
|
isPutAnnouncementResponse,
|
||||||
|
processPutAnnouncement,
|
||||||
|
)
|
||||||
|
|
||||||
const timerIdRef = useRef<number>()
|
const { update, ...button } = useSendButtonCaption('Опубликовать', loading, error, 'Опубликовано')
|
||||||
const abortControllerRef = useRef<AbortController>()
|
|
||||||
|
|
||||||
const doAdd = async (formData: FormData) => {
|
const doSendWithButton = useCallback(async (formData: FormData) => {
|
||||||
if (status === 'Загрузка...') {
|
const data = await doSend({}, {
|
||||||
abortControllerRef.current?.abort()
|
body: formData
|
||||||
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
|
|
||||||
})
|
})
|
||||||
|
update(data)
|
||||||
|
|
||||||
handleHTTPErrors(res)
|
return data
|
||||||
|
}, [doSend, update])
|
||||||
|
|
||||||
const data: unknown = await res.json()
|
return { doSend: doSendWithButton, button }
|
||||||
|
|
||||||
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
|
export default useAddAnnouncement
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import useFetch from './useFetch'
|
import { useFetch } from '../'
|
||||||
import { FiltersType } from '../../utils/filters'
|
import { FiltersType } from '../../utils/filters'
|
||||||
import { composeAnnouncementsURL, initialAnnouncements, processAnnouncements } from '../../api/announcements'
|
import { composeAnnouncementsURL, initialAnnouncements, processAnnouncements } from '../../api/announcements'
|
||||||
|
|
||||||
import { isAnnouncementsResponse } from '../../api/announcements/types'
|
import { isAnnouncementsResponse } from '../../api/announcements/types'
|
||||||
|
|
||||||
const useAnnouncements = (filters: FiltersType) =>
|
const useAnnouncements = (filters: FiltersType) => (
|
||||||
useFetch(
|
useFetch(
|
||||||
composeAnnouncementsURL(filters),
|
composeAnnouncementsURL(filters),
|
||||||
'GET',
|
'GET',
|
||||||
@ -13,6 +13,6 @@ const useAnnouncements = (filters: FiltersType) =>
|
|||||||
processAnnouncements,
|
processAnnouncements,
|
||||||
initialAnnouncements
|
initialAnnouncements
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
export default useAnnouncements
|
export default useAnnouncements
|
||||||
|
@ -38,16 +38,18 @@ interface LogInResponse {
|
|||||||
token_type: 'bearer'
|
token_type: 'bearer'
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLogInResponse = (obj: unknown): obj is LogInResponse => isObject(obj, {
|
const isLogInResponse = (obj: unknown): obj is LogInResponse => (
|
||||||
|
isObject(obj, {
|
||||||
'access_token': 'string',
|
'access_token': 'string',
|
||||||
'token_type': isConst('bearer')
|
'token_type': isConst('bearer')
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
function useAuth() {
|
function useAuth() {
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
const doAuth = async (data: AuthData, newAccount: boolean) => {
|
async function doAuth(data: AuthData, newAccount: boolean) {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
if (newAccount) {
|
if (newAccount) {
|
||||||
|
@ -10,9 +10,11 @@ type BookResponse = {
|
|||||||
Success: boolean
|
Success: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const isBookResponse = (obj: unknown): obj is BookResponse => isObject(obj, {
|
const isBookResponse = (obj: unknown): obj is BookResponse => (
|
||||||
|
isObject(obj, {
|
||||||
'Success': 'boolean'
|
'Success': 'boolean'
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
type BookStatus = '' | 'Загрузка...' | 'Забронировано' | 'Ошибка бронирования'
|
type BookStatus = '' | 'Загрузка...' | 'Забронировано' | 'Ошибка бронирования'
|
||||||
|
|
||||||
|
@ -1,128 +0,0 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
|
||||||
|
|
||||||
import { handleHTTPErrors, isAborted } from '../../utils'
|
|
||||||
import { getToken } from '../../utils/auth'
|
|
||||||
import { useNavigate } from 'react-router-dom'
|
|
||||||
import { SetState } from '../../utils/types'
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
const useFetch = <R, T>(
|
|
||||||
url: string,
|
|
||||||
method: RequestInit['method'],
|
|
||||||
needAuth: boolean,
|
|
||||||
guardResponse: (data: unknown) => data is R,
|
|
||||||
processData: (data: R) => T,
|
|
||||||
initialData?: T,
|
|
||||||
params?: RequestInit
|
|
||||||
): UseFetchReturn<T> => {
|
|
||||||
const [data, setData] = useState(initialData)
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
const abortControllerRef = useRef<AbortController>()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (abortControllerRef.current) {
|
|
||||||
abortControllerRef.current.abort()
|
|
||||||
}
|
|
||||||
|
|
||||||
const abortController = new AbortController()
|
|
||||||
abortControllerRef.current = abortController
|
|
||||||
|
|
||||||
const headers = new Headers({
|
|
||||||
...params?.headers
|
|
||||||
})
|
|
||||||
|
|
||||||
if (needAuth) {
|
|
||||||
const token = getToken()
|
|
||||||
|
|
||||||
if (token === null) {
|
|
||||||
return navigate('/login')
|
|
||||||
}
|
|
||||||
|
|
||||||
headers.append('Auth', `Bearer ${token}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(url, {
|
|
||||||
method,
|
|
||||||
...params,
|
|
||||||
headers,
|
|
||||||
signal: abortControllerRef.current.signal,
|
|
||||||
})
|
|
||||||
.then(res => {
|
|
||||||
handleHTTPErrors(res)
|
|
||||||
|
|
||||||
return res.json()
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
if (!guardResponse(data)) {
|
|
||||||
throw new Error('Malformed server response')
|
|
||||||
}
|
|
||||||
|
|
||||||
setData(processData(data))
|
|
||||||
setLoading(false)
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
if (err instanceof Error && !isAborted(err)) {
|
|
||||||
setError('Ошибка сети')
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
console.log(url, params, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => abortControllerRef.current?.abort()
|
|
||||||
}, [url, method, needAuth, params, guardResponse, processData, navigate])
|
|
||||||
|
|
||||||
return {
|
|
||||||
...(
|
|
||||||
error === null ? ({
|
|
||||||
data: data!, error: null
|
|
||||||
}) : ({ data: undefined, error })
|
|
||||||
),
|
|
||||||
loading,
|
|
||||||
setData,
|
|
||||||
abort: abortControllerRef.current?.abort.bind(abortControllerRef.current)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useFetch
|
|
||||||
|
|
||||||
export { gotError, fallbackError }
|
|
@ -1,10 +1,10 @@
|
|||||||
import { LatLng } from 'leaflet'
|
import { LatLng } from 'leaflet'
|
||||||
|
|
||||||
import useFetch from './useFetch'
|
import { useFetch } from '../'
|
||||||
import { composeOsmAddressURL, processOsmAddress } from '../../api/osmAddress'
|
import { composeOsmAddressURL, processOsmAddress } from '../../api/osmAddress'
|
||||||
import { isOsmAddressResponse } from '../../api/osmAddress/types'
|
import { isOsmAddressResponse } from '../../api/osmAddress/types'
|
||||||
|
|
||||||
const useOsmAddresses = (addressPosition: LatLng) =>
|
const useOsmAddresses = (addressPosition: LatLng) => (
|
||||||
useFetch(
|
useFetch(
|
||||||
composeOsmAddressURL(addressPosition),
|
composeOsmAddressURL(addressPosition),
|
||||||
'GET',
|
'GET',
|
||||||
@ -13,5 +13,6 @@ const useOsmAddresses = (addressPosition: LatLng) =>
|
|||||||
processOsmAddress,
|
processOsmAddress,
|
||||||
''
|
''
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
export default useOsmAddresses
|
export default useOsmAddresses
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import { LatLng } from 'leaflet'
|
import { LatLng } from 'leaflet'
|
||||||
|
|
||||||
import useFetch from './useFetch'
|
import { useFetch } from '../'
|
||||||
import { composeTrashboxURL } from '../../api/trashbox'
|
import { composeTrashboxURL, processTrashbox } from '../../api/trashbox'
|
||||||
import { isTrashboxResponse } from '../../api/trashbox/types'
|
import { isTrashboxResponse } from '../../api/trashbox/types'
|
||||||
|
|
||||||
const useTrashboxes = (position: LatLng) =>
|
const useTrashboxes = (position: LatLng) => (
|
||||||
useFetch(
|
useFetch(
|
||||||
composeTrashboxURL(position),
|
composeTrashboxURL(position),
|
||||||
'GET',
|
'GET',
|
||||||
true,
|
true,
|
||||||
isTrashboxResponse,
|
isTrashboxResponse,
|
||||||
(data) => data,
|
processTrashbox,
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
export default useTrashboxes
|
export default useTrashboxes
|
||||||
|
@ -1 +1,3 @@
|
|||||||
export { default as useStoryDimensions } from './useStoryDimensions'
|
export { default as useStoryDimensions } from './useStoryDimensions'
|
||||||
|
export { default as useSend } from './useSend'
|
||||||
|
export { default as useFetch } from './useFetch'
|
||||||
|
75
front/src/hooks/useFetch.ts
Normal file
75
front/src/hooks/useFetch.ts
Normal 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 }
|
95
front/src/hooks/useSend.ts
Normal file
95
front/src/hooks/useSend.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
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)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
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
|
44
front/src/hooks/useSendButtonCaption.ts
Normal file
44
front/src/hooks/useSendButtonCaption.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
function useSendButtonCaption(
|
||||||
|
initial: string,
|
||||||
|
loading: boolean,
|
||||||
|
error: string | null,
|
||||||
|
result = initial,
|
||||||
|
singular = true
|
||||||
|
) {
|
||||||
|
const [caption, setCaption] = useState(initial)
|
||||||
|
const [disabled, setDisabled] = useState(false)
|
||||||
|
const [title, setTitle] = useState(initial)
|
||||||
|
|
||||||
|
const update = useCallback(<T extends NonNullable<unknown>>(data: T | undefined) => {
|
||||||
|
if (data !== undefined) {
|
||||||
|
setCaption(result)
|
||||||
|
setTitle('Отправить ещё раз')
|
||||||
|
|
||||||
|
if (singular) {
|
||||||
|
setDisabled(true)
|
||||||
|
setTitle('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [result, singular])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading) {
|
||||||
|
setCaption('Загрузка...')
|
||||||
|
setTitle('Отменить и отправить ещё раз')
|
||||||
|
}
|
||||||
|
}, [loading])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loading && error !== null) {
|
||||||
|
setCaption(error + ', нажмите, чтобы попробовать ещё раз')
|
||||||
|
setTitle('')
|
||||||
|
}
|
||||||
|
}, [error, loading])
|
||||||
|
|
||||||
|
return { update, children: caption, disabled, title }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useSendButtonCaption
|
@ -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(() => {
|
||||||
|
@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client'
|
|||||||
|
|
||||||
import App from './App.tsx'
|
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>
|
||||||
|
@ -1,16 +1,28 @@
|
|||||||
import { FormEventHandler, useEffect, useState } from 'react'
|
import { CSSProperties, FormEventHandler, useState } from 'react'
|
||||||
import { Form, Button, Card } from 'react-bootstrap'
|
import { Form, Button, Card } from 'react-bootstrap'
|
||||||
import { MapContainer, TileLayer } from 'react-leaflet'
|
import { MapContainer, TileLayer } from 'react-leaflet'
|
||||||
import { latLng } from 'leaflet'
|
import { latLng } from 'leaflet'
|
||||||
|
|
||||||
import { ClickHandler, LocationMarker, TrashboxMarkers } from '../components'
|
import { ClickHandler, LocationMarker, TrashboxMarkers } from '../components'
|
||||||
import { useAddAnnouncement, useTrashboxes } from '../hooks/api'
|
import { useAddAnnouncement, useTrashboxes } from '../hooks/api'
|
||||||
import { handleHTTPErrors } from '../utils'
|
|
||||||
import { categories, categoryNames } from '../assets/category'
|
import { categories, categoryNames } from '../assets/category'
|
||||||
import { stations, lines, lineNames } from '../assets/metro'
|
import { stations, lines, lineNames } from '../assets/metro'
|
||||||
import { fallbackError, gotError } from '../hooks/api/useFetch'
|
import { fallbackError, gotError } from '../hooks/useFetch'
|
||||||
import { useOsmAddresses } from '../hooks/api'
|
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() {
|
function AddPage() {
|
||||||
const [addressPosition, setAddressPosition] = useState(latLng(59.972, 30.3227))
|
const [addressPosition, setAddressPosition] = useState(latLng(59.972, 30.3227))
|
||||||
|
|
||||||
@ -19,25 +31,7 @@ function AddPage() {
|
|||||||
|
|
||||||
const address = useOsmAddresses(addressPosition)
|
const address = useOsmAddresses(addressPosition)
|
||||||
|
|
||||||
useEffect(() => {
|
const { doSend, button } = useAddAnnouncement()
|
||||||
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) => {
|
const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@ -50,12 +44,12 @@ function AddPage() {
|
|||||||
formData.append('address', address.data || '') // if address.error
|
formData.append('address', address.data || '') // if address.error
|
||||||
formData.set('bestBy', new Date((formData.get('bestBy') as number | null) || 0).getTime().toString())
|
formData.set('bestBy', new Date((formData.get('bestBy') as number | null) || 0).getTime().toString())
|
||||||
|
|
||||||
void doAdd(formData)
|
void doSend(formData)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className='m-4' style={{ height: 'calc(100vh - 3rem)' }}>
|
<Card className='m-4' style={styles.modal}>
|
||||||
<Card.Body style={{ overflowY: 'auto' }} >
|
<Card.Body style={styles.body} >
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit}>
|
||||||
<Form.Group className='mb-3' controlId='name'>
|
<Form.Group className='mb-3' controlId='name'>
|
||||||
<Form.Label>Заголовок объявления</Form.Label>
|
<Form.Label>Заголовок объявления</Form.Label>
|
||||||
@ -84,7 +78,7 @@ function AddPage() {
|
|||||||
<div className='mb-3'>
|
<div className='mb-3'>
|
||||||
<MapContainer
|
<MapContainer
|
||||||
scrollWheelZoom={false}
|
scrollWheelZoom={false}
|
||||||
style={{ width: '100%', height: 400 }}
|
style={styles.map}
|
||||||
center={addressPosition}
|
center={addressPosition}
|
||||||
zoom={13}
|
zoom={13}
|
||||||
>
|
>
|
||||||
@ -138,24 +132,24 @@ function AddPage() {
|
|||||||
</Form.Select>
|
</Form.Select>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group className='mb-3' controlId='password'>
|
<Form.Group className='mb-3' controlId='trashbox'>
|
||||||
<Form.Label>Пункт сбора мусора</Form.Label>
|
<Form.Label>Пункт сбора мусора</Form.Label>
|
||||||
<div className='mb-3'>
|
<div className='mb-3'>
|
||||||
{trashboxes.loading
|
{trashboxes.loading
|
||||||
? (
|
? (
|
||||||
<div style={{ height: 400 }}>
|
<div style={styles.map}>
|
||||||
<p>Загрузка...</p>
|
<p>Загрузка...</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
gotError(trashboxes) ? (
|
gotError(trashboxes) ? (
|
||||||
<p
|
<p
|
||||||
style={{ height: 400 }}
|
style={styles.map}
|
||||||
className='text-danger'
|
className='text-danger'
|
||||||
>{trashboxes.error}</p>
|
>{trashboxes.error}</p>
|
||||||
) : (
|
) : (
|
||||||
<MapContainer
|
<MapContainer
|
||||||
scrollWheelZoom={false}
|
scrollWheelZoom={false}
|
||||||
style={{ width: '100%', height: 400 }}
|
style={styles.map}
|
||||||
center={addressPosition}
|
center={addressPosition}
|
||||||
zoom={13}
|
zoom={13}
|
||||||
className=''
|
className=''
|
||||||
@ -182,9 +176,7 @@ function AddPage() {
|
|||||||
)}
|
)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Button variant='success' type='submit'>
|
<Button variant='success' type='submit' {...button} />
|
||||||
{status}
|
|
||||||
</Button>
|
|
||||||
</Form>
|
</Form>
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
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 { Story } from 'react-insta-stories/dist/interfaces'
|
||||||
|
|
||||||
@ -10,7 +10,7 @@ import { Announcement } from '../api/announcement/types'
|
|||||||
import { categoryGraphics } from '../assets/category'
|
import { categoryGraphics } from '../assets/category'
|
||||||
|
|
||||||
import puffSpinner from '../assets/puff.svg'
|
import puffSpinner from '../assets/puff.svg'
|
||||||
import { gotError } from '../hooks/api/useFetch'
|
import { gotError } from '../hooks/useFetch'
|
||||||
|
|
||||||
function generateStories(announcements: Announcement[]): Story[] {
|
function generateStories(announcements: Announcement[]): Story[] {
|
||||||
return announcements.map(announcement => {
|
return announcements.map(announcement => {
|
||||||
@ -44,21 +44,26 @@ const fallbackStory = (text = '', isError = false): Story[] => [{
|
|||||||
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>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
const storiesContainerCSS = {
|
const styles = {
|
||||||
|
container: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
backgroundColor: 'rgb(17, 17, 17)'
|
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)
|
||||||
@ -69,7 +74,7 @@ function HomePage() {
|
|||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<Filters filter={filter} setFilter={setFilter} filterShown={filterShown} setFilterShown={setFilterShown} />
|
<Filters filter={filter} setFilter={setFilter} filterShown={filterShown} setFilterShown={setFilterShown} />
|
||||||
<div style={storiesContainerCSS}>
|
<div style={styles.container}>
|
||||||
<Stories
|
<Stories
|
||||||
stories={stories}
|
stories={stories}
|
||||||
defaultInterval={11000}
|
defaultInterval={11000}
|
||||||
|
@ -28,7 +28,7 @@ function LoginPage() {
|
|||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
setToken(token)
|
setToken(token)
|
||||||
navigate('/')
|
navigate(-1 - Number(import.meta.env.DEV))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ const getToken = () => {
|
|||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
const setToken = (token: string) => {
|
function setToken(token: string) {
|
||||||
localStorage.setItem('Token', token)
|
localStorage.setItem('Token', token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,10 +7,12 @@ type FiltersType = Partial<Pick<Announcement, FilterNames>>
|
|||||||
|
|
||||||
const defaultFilters: FiltersType = { userId: undefined, category: undefined, metro: undefined, bookedBy: undefined }
|
const defaultFilters: FiltersType = { userId: undefined, category: undefined, metro: undefined, bookedBy: undefined }
|
||||||
|
|
||||||
const URLEncodeFilters = (filters: FiltersType) => Object.fromEntries(
|
const URLEncodeFilters = (filters: FiltersType) => (
|
||||||
|
Object.fromEntries(
|
||||||
filterNames.map(
|
filterNames.map(
|
||||||
fName => [fName, filters[fName]?.toString()]
|
fName => [fName, filters[fName]?.toString()]
|
||||||
).filter((p): p is [string, string] => typeof p[1] !== 'undefined')
|
).filter((p): p is [string, string] => typeof p[1] !== 'undefined')
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
export type { FilterNames, FiltersType }
|
export type { FilterNames, FiltersType }
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
const isAborted = (err: Error) => err.name === 'AbortError'
|
const isAborted = (err: Error) => (
|
||||||
|
err.name === 'AbortError'
|
||||||
|
)
|
||||||
|
|
||||||
const handleHTTPErrors = (res: Response) => {
|
function handleHTTPErrors(res: Response) {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
switch (res.status) {
|
switch (res.status) {
|
||||||
case 401:
|
case 401:
|
||||||
|
@ -20,7 +20,9 @@ const isObject = <T>(obj: unknown, properties: PropertiesGuards): obj is T => (
|
|||||||
Object.entries(properties).every(([name, guard]) => {
|
Object.entries(properties).every(([name, guard]) => {
|
||||||
const param: unknown = Reflect.get(obj, name)
|
const param: unknown = Reflect.get(obj, name)
|
||||||
|
|
||||||
console.log(name, param, guard)
|
if (import.meta.env.DEV) {
|
||||||
|
console.debug('isObject', name, param, guard)
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof guard === 'function') {
|
if (typeof guard === 'function') {
|
||||||
return guard(param)
|
return guard(param)
|
||||||
@ -54,7 +56,9 @@ const isArrayOf = <T>(obj: unknown, itemGuard: ((obj: unknown) => obj is T)): ob
|
|||||||
obj.every(itemGuard)
|
obj.every(itemGuard)
|
||||||
)
|
)
|
||||||
|
|
||||||
const isString = (obj: unknown): obj is string => typeof obj === 'string'
|
const isString = (obj: unknown): obj is string => (
|
||||||
|
typeof obj === 'string'
|
||||||
|
)
|
||||||
|
|
||||||
type SetState<T> = React.Dispatch<React.SetStateAction<T>>
|
type SetState<T> = React.Dispatch<React.SetStateAction<T>>
|
||||||
|
|
||||||
|
49
front/utils/addFetchApiRoute.sh
Normal file
49
front/utils/addFetchApiRoute.sh
Normal 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
|
1
migrations/README
Normal file
1
migrations/README
Normal file
@ -0,0 +1 @@
|
|||||||
|
Generic single-database configuration.
|
85
migrations/env.py
Normal file
85
migrations/env.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from sqlalchemy import engine_from_config
|
||||||
|
from sqlalchemy import pool
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
|
||||||
|
|
||||||
|
from back import db, base
|
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides
|
||||||
|
# access to the values within the .ini file in use.
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
# Interpret the config file for Python logging.
|
||||||
|
# This line sets up loggers basically.
|
||||||
|
if config.config_file_name is not None:
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
|
# add your model's MetaData object here
|
||||||
|
# for 'autogenerate' support
|
||||||
|
# from myapp import mymodel
|
||||||
|
# target_metadata = mymodel.Base.metadata
|
||||||
|
target_metadata = base.Base.metadata
|
||||||
|
# target_metadata = None
|
||||||
|
|
||||||
|
# other values from the config, defined by the needs of env.py,
|
||||||
|
# can be acquired:
|
||||||
|
# my_important_option = config.get_main_option("my_important_option")
|
||||||
|
# ... etc.
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_offline():
|
||||||
|
"""Run migrations in 'offline' mode.
|
||||||
|
|
||||||
|
This configures the context with just a URL
|
||||||
|
and not an Engine, though an Engine is acceptable
|
||||||
|
here as well. By skipping the Engine creation
|
||||||
|
we don't even need a DBAPI to be available.
|
||||||
|
|
||||||
|
Calls to context.execute() here emit the given string to the
|
||||||
|
script output.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# url = config.get_main_option("sqlalchemy.url")
|
||||||
|
url = config.get_main_option(db.SQLALCHEMY_DATABASE_URL)
|
||||||
|
context.configure(
|
||||||
|
url=url,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
literal_binds=True,
|
||||||
|
dialect_opts={"paramstyle": "named"},
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online():
|
||||||
|
"""Run migrations in 'online' mode.
|
||||||
|
|
||||||
|
In this scenario we need to create an Engine
|
||||||
|
and associate a connection with the context.
|
||||||
|
|
||||||
|
"""
|
||||||
|
configuration = config.get_section(config.config_ini_section)
|
||||||
|
configuration['sqlalchemy.url'] = db.SQLALCHEMY_DATABASE_URL
|
||||||
|
connectable = engine_from_config(
|
||||||
|
configuration,
|
||||||
|
prefix="sqlalchemy.",
|
||||||
|
poolclass=pool.NullPool,
|
||||||
|
)
|
||||||
|
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
context.configure(
|
||||||
|
connection=connection, target_metadata=target_metadata
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
24
migrations/script.py.mako
Normal file
24
migrations/script.py.mako
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = ${repr(up_revision)}
|
||||||
|
down_revision = ${repr(down_revision)}
|
||||||
|
branch_labels = ${repr(branch_labels)}
|
||||||
|
depends_on = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
${downgrades if downgrades else "pass"}
|
28
migrations/versions/0006eca30e2c_first.py
Normal file
28
migrations/versions/0006eca30e2c_first.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"""first
|
||||||
|
|
||||||
|
Revision ID: 0006eca30e2c
|
||||||
|
Revises:
|
||||||
|
Create Date: 2023-07-23 22:32:43.496939
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '0006eca30e2c'
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
pass
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
pass
|
||||||
|
# ### end Alembic commands ###
|
34
migrations/versions/18001c2231e3_poems_table_added.py
Normal file
34
migrations/versions/18001c2231e3_poems_table_added.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
"""Poems table added
|
||||||
|
|
||||||
|
Revision ID: 18001c2231e3
|
||||||
|
Revises: 33c5716276b5
|
||||||
|
Create Date: 2023-07-23 22:50:16.055961
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '18001c2231e3'
|
||||||
|
down_revision = '33c5716276b5'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('poems',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('poem_text', sa.String(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_poems_id'), 'poems', ['id'], unique=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_index(op.f('ix_poems_id'), table_name='poems')
|
||||||
|
op.drop_table('poems')
|
||||||
|
# ### end Alembic commands ###
|
@ -0,0 +1,70 @@
|
|||||||
|
"""Try to make alembic see models
|
||||||
|
|
||||||
|
Revision ID: 33c5716276b5
|
||||||
|
Revises: 0006eca30e2c
|
||||||
|
Create Date: 2023-07-23 22:42:07.532395
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '33c5716276b5'
|
||||||
|
down_revision = '0006eca30e2c'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('announcements',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('name', sa.String(), nullable=True),
|
||||||
|
sa.Column('category', sa.String(), nullable=True),
|
||||||
|
sa.Column('best_by', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('address', sa.String(), nullable=True),
|
||||||
|
sa.Column('longtitude', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('latitude', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('description', sa.String(), nullable=True),
|
||||||
|
sa.Column('src', sa.String(), nullable=True),
|
||||||
|
sa.Column('metro', sa.String(), nullable=True),
|
||||||
|
sa.Column('trashId', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('booked_by', sa.Integer(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_announcements_id'), 'announcements', ['id'], unique=False)
|
||||||
|
op.create_table('trashboxes',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(), nullable=True),
|
||||||
|
sa.Column('address', sa.String(), nullable=True),
|
||||||
|
sa.Column('latitude', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('longtitude', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('category', sa.String(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_trashboxes_id'), 'trashboxes', ['id'], unique=False)
|
||||||
|
op.create_table('users',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('phone', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('email', sa.String(), nullable=True),
|
||||||
|
sa.Column('password', sa.String(), nullable=True),
|
||||||
|
sa.Column('hashed_password', sa.String(), nullable=True),
|
||||||
|
sa.Column('name', sa.String(), nullable=True),
|
||||||
|
sa.Column('surname', sa.String(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=True)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_index(op.f('ix_users_id'), table_name='users')
|
||||||
|
op.drop_table('users')
|
||||||
|
op.drop_index(op.f('ix_trashboxes_id'), table_name='trashboxes')
|
||||||
|
op.drop_table('trashboxes')
|
||||||
|
op.drop_index(op.f('ix_announcements_id'), table_name='announcements')
|
||||||
|
op.drop_table('announcements')
|
||||||
|
# ### end Alembic commands ###
|
Reference in New Issue
Block a user