Compare commits
8 Commits
Auth-code-
...
userPage
Author | SHA1 | Date | |
---|---|---|---|
9b4ef41030
|
|||
96388a9bea
|
|||
58d1996ce3
|
|||
bc154f8b6b
|
|||
7a044970f0
|
|||
7cf83d099d
|
|||
1b4eed529a
|
|||
bc55ab8f68
|
102
alembic.ini
102
alembic.ini
@ -1,102 +0,0 @@
|
|||||||
# 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
|
|
@ -1,2 +0,0 @@
|
|||||||
from .db import Base
|
|
||||||
from .models import UserDatabase, Announcement, Trashbox
|
|
16
back/db.py
16
back/db.py
@ -1,13 +1,6 @@
|
|||||||
from typing import AsyncGenerator
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
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.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker, Session
|
||||||
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"
|
||||||
|
|
||||||
@ -15,7 +8,6 @@ engine = create_engine(
|
|||||||
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
SessionLocal = sessionmaker(bind=engine, autoflush=True, autocommit=False)
|
SessionLocal = sessionmaker(autoflush=True, bind=engine)
|
||||||
|
|
||||||
database = SessionLocal()
|
Base = declarative_base()
|
||||||
Base = declarative_base()
|
|
||||||
|
59
back/main.py
59
back/main.py
@ -19,47 +19,34 @@ import shutil
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from .utils import *
|
from .utils import *
|
||||||
from .db import Base, engine, SessionLocal, database
|
from .models import Announcement, Trashbox, UserDatabase, Base
|
||||||
from .models import Announcement, Trashbox, UserDatabase
|
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()
|
||||||
|
|
||||||
|
|
||||||
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 = 0):
|
def annoncements_list(user_id: int = None, metro: str = None, category: str = None, booked_by: int = -1):
|
||||||
# Считываем данные из Body и отображаем их на странице.
|
# Считываем данные из Body и отображаем их на странице.
|
||||||
# В последствии будем вставлять данные в html-форму
|
# В последствии будем вставлять данные в html-форму
|
||||||
|
|
||||||
a = database.query(Announcement)
|
a = db.query(Announcement)
|
||||||
b = database.query(Announcement)
|
b = db.query(Announcement)
|
||||||
c = database.query(Announcement)
|
c = db.query(Announcement)
|
||||||
d = database.query(Announcement)
|
d = db.query(Announcement)
|
||||||
e = database.query(Announcement)
|
e = db.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)
|
||||||
@ -87,7 +74,7 @@ def single_annoncement(user_id:int):
|
|||||||
# Считываем данные из Body и отображаем их на странице.
|
# Считываем данные из Body и отображаем их на странице.
|
||||||
# В последствии будем вставлять данные в html-форму
|
# В последствии будем вставлять данные в html-форму
|
||||||
try:
|
try:
|
||||||
annoncement = database.get(Announcement, user_id)
|
annoncement = db.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,
|
||||||
@ -128,8 +115,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:
|
||||||
database.delete(user_id=data.user_id)#удаление из БД
|
db.delete(user_id=data.user_id)#удаление из БД
|
||||||
database.commit() # сохраняем изменения
|
db.commit() # сохраняем изменения
|
||||||
return {"Answer" : True}
|
return {"Answer" : True}
|
||||||
except:
|
except:
|
||||||
return {"Answer" : False}
|
return {"Answer" : False}
|
||||||
@ -142,21 +129,21 @@ def change_book_status(data: schema.Book):
|
|||||||
# Получаем id пользователя, который бронирует объявление
|
# Получаем id пользователя, который бронирует объявление
|
||||||
temp_user_id = 1
|
temp_user_id = 1
|
||||||
# Находим объявление по данному id
|
# Находим объявление по данному id
|
||||||
announcement_to_change = database.query(Announcement).filter(id == data.id).first()
|
announcement_to_change = db.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 database.query(UserDatabase).filter(UserDatabase.email == data["email"]).first() == None:
|
if db.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"])
|
||||||
database.add(new_user)
|
db.add(new_user)
|
||||||
database.commit()
|
db.commit()
|
||||||
database.refresh(new_user) # обновляем состояние объекта
|
db.refresh(new_user) # обновляем состояние объекта
|
||||||
return {"Success": True}
|
return {"Success": True}
|
||||||
return {"Success": False, "Message": "Пользователь с таким email уже зарегестрирован."}
|
return {"Success": False, "Message": "Пользователь с таким email уже зарегестрирован."}
|
||||||
|
|
||||||
@ -166,7 +153,7 @@ async def login_for_access_token(
|
|||||||
form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
|
form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
|
||||||
):
|
):
|
||||||
# разобраться с первым параметром
|
# разобраться с первым параметром
|
||||||
user = authenticate_user(database, form_data.username, form_data.password)
|
user = authenticate_user(db.query(UserDatabase).all(), 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,
|
||||||
@ -180,8 +167,8 @@ async def login_for_access_token(
|
|||||||
return access_token
|
return access_token
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/users/me/", response_model=schema.User)
|
@app.get("/api/users/me/", response_model=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
|
||||||
|
@ -1,18 +1,12 @@
|
|||||||
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
|
||||||
|
# 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, unique=True)#айди пользователя
|
id = Column(Integer, primary_key=True, index=True)#айди пользователя
|
||||||
phone = Column(Integer, nullable=True)#номер телефона пользователя
|
phone = Column(Integer, nullable=True)#номер телефона пользователя
|
||||||
email = Column(String)#электронная почта пользователя
|
email = Column(String)#электронная почта пользователя
|
||||||
password = Column(String) # пароль
|
password = Column(String) # пароль
|
||||||
@ -49,26 +43,3 @@ 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,29 +1,5 @@
|
|||||||
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
|
|
@ -1,2 +0,0 @@
|
|||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
@ -1,24 +1,52 @@
|
|||||||
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, Response, Request
|
from fastapi import Depends, FastAPI, HTTPException, status
|
||||||
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
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
# to get a string like this run:
|
||||||
from sqlalchemy import select
|
# openssl rand -hex 32
|
||||||
|
|
||||||
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")
|
||||||
@ -32,15 +60,17 @@ def get_password_hash(password):
|
|||||||
|
|
||||||
|
|
||||||
# проблема здесь
|
# проблема здесь
|
||||||
def get_user(db: Session, email: str):
|
def get_user(db, email: str):
|
||||||
user_with_required_email = db.query(UserDatabase).filter(UserDatabase.email == email).first()
|
user = None
|
||||||
print(user_with_required_email)
|
for person_with_correct_email in db.query(UserDatabase):
|
||||||
if user_with_required_email:
|
if person_with_correct_email.email == email:
|
||||||
return user_with_required_email
|
user = person_with_correct_email
|
||||||
return None
|
return user #UserInDB(user_email)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
@ -60,7 +90,7 @@ def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None
|
|||||||
return encoded_jwt
|
return encoded_jwt
|
||||||
|
|
||||||
|
|
||||||
async def get_current_user(db: Session, token: Annotated[str, Depends(oauth2_scheme)]):
|
async def get_current_user(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",
|
||||||
@ -74,8 +104,8 @@ async def get_current_user(db: Session, token: Annotated[str, Depends(oauth2_sch
|
|||||||
token_data = TokenData(email=email)
|
token_data = TokenData(email=email)
|
||||||
except JWTError:
|
except JWTError:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
user = get_user(db, email=token_data.email)
|
user = get_user(fake_users_db, email=token_data.email)
|
||||||
if user == None:
|
if user is None:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@ -85,8 +115,4 @@ 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
|
|
17
front/package-lock.json
generated
17
front/package-lock.json
generated
@ -20,6 +20,7 @@
|
|||||||
"react-router-dom": "^6.14.1"
|
"react-router-dom": "^6.14.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@faker-js/faker": "^8.0.2",
|
||||||
"@types/react": "^18.2.14",
|
"@types/react": "^18.2.14",
|
||||||
"@types/react-dom": "^18.2.6",
|
"@types/react-dom": "^18.2.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
||||||
@ -817,6 +818,22 @@
|
|||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@faker-js/faker": {
|
||||||
|
"version": "8.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.0.2.tgz",
|
||||||
|
"integrity": "sha512-Uo3pGspElQW91PCvKSIAXoEgAUlRnH29sX2/p89kg7sP1m2PzCufHINd0FhTXQf6DYGiUlVncdSPa2F9wxed2A==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fakerjs"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
||||||
|
"npm": ">=6.14.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.11.10",
|
"version": "0.11.10",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
"react-router-dom": "^6.14.1"
|
"react-router-dom": "^6.14.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@faker-js/faker": "^8.0.2",
|
||||||
"@types/react": "^18.2.14",
|
"@types/react": "^18.2.14",
|
||||||
"@types/react-dom": "^18.2.6",
|
"@types/react-dom": "^18.2.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
||||||
|
315
front/prototype.html
Normal file
315
front/prototype.html
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Document</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<nav>
|
||||||
|
<a href="#back" class="back">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-arrow-left" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<h1 class="heading">Иванов Иван, с нами с 17.07.2023</h1>
|
||||||
|
</nav>
|
||||||
|
<div id="root"></div>
|
||||||
|
<div class="poemContainer">
|
||||||
|
<h1>Поэзия</h1>
|
||||||
|
<div class="poemText">
|
||||||
|
<div class="eleven" style="position:relative;left:-60px">"Fury said to</div>
|
||||||
|
<div class="ten" style="position:relative;left:-40px">a mouse, That</div>
|
||||||
|
<div class="ten" style="position:relative;left:0px">he met</div>
|
||||||
|
<div class="ten" style="position:relative;left:10px">in the</div>
|
||||||
|
<div class="ten" style="position:relative;left:20px">house,</div>
|
||||||
|
<div class="ten" style="position:relative;left:17px">'Let us</div>
|
||||||
|
<div class="ten" style="position:relative;left:5px">both go</div>
|
||||||
|
<div class="ten" style="position:relative;left:-7px">to law:</div>
|
||||||
|
<div class="ten" style="position:relative;left:-23px"><i>I</i> will</div>
|
||||||
|
<div class="ten" style="position:relative;left:-26px">prosecute</div>
|
||||||
|
<div class="nine" style="position:relative;left:-40px"><i>you.</i>—</div>
|
||||||
|
<div class="nine" style="position:relative;left:-30px">Come, I'll</div>
|
||||||
|
<div class="nine" style="position:relative;left:-20px">take no</div>
|
||||||
|
<div class="nine" style="position:relative;left:-7px">denial;</div>
|
||||||
|
<div class="nine" style="position:relative;left:19px">We must</div>
|
||||||
|
<div class="nine" style="position:relative;left:45px">have a</div>
|
||||||
|
<div class="nine" style="position:relative;left:67px">trial:</div>
|
||||||
|
<div class="nine" style="position:relative;left:80px">For</div>
|
||||||
|
<div class="eight" style="position:relative;left:70px">really</div>
|
||||||
|
<div class="eight" style="position:relative;left:57px">this</div>
|
||||||
|
<div class="eight" style="position:relative;left:75px">morning</div>
|
||||||
|
<div class="eight" style="position:relative;left:95px">I've</div>
|
||||||
|
<div class="eight" style="position:relative;left:77px">nothing</div>
|
||||||
|
<div class="eight" style="position:relative;left:57px">to do.'</div>
|
||||||
|
<div class="seven" style="position:relative;left:38px">Said the</div>
|
||||||
|
<div class="seven" style="position:relative;left:30px">mouse to</div>
|
||||||
|
<div class="seven" style="position:relative;left:18px">the cur,</div>
|
||||||
|
<div class="seven" style="position:relative;left:22px">'Such a</div>
|
||||||
|
<div class="seven" style="position:relative;left:37px">trial,</div>
|
||||||
|
<div class="seven" style="position:relative;left:27px">dear sir,</div>
|
||||||
|
<div class="seven" style="position:relative;left:9px">With no</div>
|
||||||
|
<div class="seven" style="position:relative;left:-8px">jury or</div>
|
||||||
|
<div class="seven" style="position:relative;left:-18px">judge,</div>
|
||||||
|
<div class="seven" style="position:relative;left:-6px">would be</div>
|
||||||
|
<div class="seven" style="position:relative;left:7px">wasting</div>
|
||||||
|
<div class="seven" style="position:relative;left:25px">our breath.'</div>
|
||||||
|
<div class="six" style="position:relative;left:30px">'I'll be</div>
|
||||||
|
<div class="six" style="position:relative;left:24px">judge,</div>
|
||||||
|
<div class="six" style="position:relative;left:15px">I'll be</div>
|
||||||
|
<div class="six" style="position:relative;left:2px">jury,'</div>
|
||||||
|
<div class="six" style="position:relative;left:-4px">Said</div>
|
||||||
|
<div class="six" style="position:relative;left:17px">cunning</div>
|
||||||
|
<div class="six" style="position:relative;left:29px">old Fury;</div>
|
||||||
|
<div class="six" style="position:relative;left:37px">'I'll try</div>
|
||||||
|
<div class="six" style="position:relative;left:51px">the whole</div>
|
||||||
|
<div class="six" style="position:relative;left:70px">cause,</div>
|
||||||
|
<div class="six" style="position:relative;left:65px">and</div>
|
||||||
|
<div class="six" style="position:relative;left:60px">condemn</div>
|
||||||
|
<div class="six" style="position:relative;left:60px">you</div>
|
||||||
|
<div class="six" style="position:relative;left:68px">to</div>
|
||||||
|
<div class="six" style="position:relative;left:82px">death.' "</div>
|
||||||
|
</div>
|
||||||
|
<img src="uploads/mouse.jpg" class="poemImg">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const categoryGraphics = {
|
||||||
|
'PORRIDGE': 'dist/PORRIDGE.jpg',
|
||||||
|
'conspects': 'dist/conspects.jpg',
|
||||||
|
'milk': 'dist/milk.jpg',
|
||||||
|
'bred': 'dist/bred.jpg',
|
||||||
|
'wathing': 'dist/wathing.jpg',
|
||||||
|
'cloth': 'dist/cloth.jpg',
|
||||||
|
'fruits_vegatables': 'dist/fruits_vegatables.jpg',
|
||||||
|
'soup': 'dist/soup.jpg',
|
||||||
|
'dinner': 'dist/dinner.jpg',
|
||||||
|
'conserves': 'dist/conserves.jpg',
|
||||||
|
'pens': 'dist/pens.jpg',
|
||||||
|
'other_things': 'dist/other_things.jpg',
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryNames = {
|
||||||
|
'PORRIDGE': 'PORRIDGE',
|
||||||
|
'conspects': 'Конспекты',
|
||||||
|
'milk': 'Молочные продукты',
|
||||||
|
'bred': 'Хлебобулочные изделия',
|
||||||
|
'wathing': 'Моющие средства',
|
||||||
|
'cloth': 'Одежда',
|
||||||
|
'fruits_vegatables': 'Фрукты и овощи',
|
||||||
|
'soup': 'Супы',
|
||||||
|
'dinner': 'Ужин',
|
||||||
|
'conserves': 'Консервы',
|
||||||
|
'pens': 'Канцелярия',
|
||||||
|
'other_things': 'Всякая всячина',
|
||||||
|
}
|
||||||
|
|
||||||
|
const cats = ["Раздача", "Бронь", "История"]
|
||||||
|
|
||||||
|
const props = ["Годен до 01.09.2023", "Бронь ещё 5 чел.", "Забрал 16.07.2023"]
|
||||||
|
|
||||||
|
|
||||||
|
const stories = [2, 4, 1].map(
|
||||||
|
(n) => (new Array(n)).fill(1).map(
|
||||||
|
() => (
|
||||||
|
{
|
||||||
|
title: (Math.random() * Math.pow(10, Math.random() * 100)).toString(36),
|
||||||
|
category: Object.keys(categoryGraphics)[Math.round(Math.random() * (Object.keys(categoryGraphics).length - 1))]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(stories)
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
const root = document.getElementById('root')
|
||||||
|
|
||||||
|
root.innerHTML = ''
|
||||||
|
|
||||||
|
stories.forEach((c, i) => {
|
||||||
|
const section = document.createElement('section')
|
||||||
|
section.className = 'section'
|
||||||
|
section.innerHTML = `<h1>${cats[i]}</h1>`
|
||||||
|
const ul = document.createElement('ul')
|
||||||
|
c.forEach((v, j) => {
|
||||||
|
const story = document.createElement('li')
|
||||||
|
story.className = 'story'
|
||||||
|
story.innerHTML = `
|
||||||
|
<a href="#${i},${j}">
|
||||||
|
<img class="storyPic" src="${categoryGraphics[v.category]}" />
|
||||||
|
<p class="storyTitle">${v.title}</p>
|
||||||
|
<p class="storyTitle">${props[i]}</p>
|
||||||
|
`.trim()
|
||||||
|
ul.appendChild(story)
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(window.innerWidth, (window.innerHeight * 0.25 * 9 / 16), (window.innerWidth * 0.25 * 9 / 16) * c.length)
|
||||||
|
|
||||||
|
if ((window.innerWidth - 60) < (window.innerHeight * 0.25 * 9 / 16 + 10) * c.length) {
|
||||||
|
const seeMore = document.createElement('a')
|
||||||
|
seeMore.href = "#more"
|
||||||
|
seeMore.innerHTML = `<svg fill="currentColor" width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 512"><!--! Font Awesome Free 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M64 448c-8.188 0-16.38-3.125-22.62-9.375c-12.5-12.5-12.5-32.75 0-45.25L178.8 256L41.38 118.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0l160 160c12.5 12.5 12.5 32.75 0 45.25l-160 160C80.38 444.9 72.19 448 64 448z"/></svg>`
|
||||||
|
seeMore.className = 'seeMore'
|
||||||
|
|
||||||
|
ul.appendChild(seeMore)
|
||||||
|
ul.classList.add('grad')
|
||||||
|
}
|
||||||
|
|
||||||
|
section.appendChild(ul)
|
||||||
|
root.appendChild(section)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// window.addEventListener('resize', render)
|
||||||
|
document.addEventListener('DOMContentLoaded', render)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgb(185, 179, 170);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #111;
|
||||||
|
color: rgb(185, 179, 170);
|
||||||
|
width: 100%;
|
||||||
|
max-width: calc(100vh*9/16);
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: 30px;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
list-style-type: none;
|
||||||
|
padding-top: 20px;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grad::after {
|
||||||
|
content: '';
|
||||||
|
background: linear-gradient(to right, rgba(17, 17, 17, 0) 0%, rgba(17, 17, 17, 255) 100%);
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
width: 10%;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding-right: 10px;
|
||||||
|
width: calc(25vh*9/16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.storyPic {
|
||||||
|
max-height: 25vh;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storyTitle {
|
||||||
|
padding-top: 5px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
/* max-width: 100%; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.seeMore {
|
||||||
|
position: absolute;
|
||||||
|
left: calc(100% - 5% / 3 - 30px + 8px);
|
||||||
|
top: calc(50% - 15px);
|
||||||
|
z-index: 100;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 3px;
|
||||||
|
color: rgb(185, 179, 170);
|
||||||
|
/* background-color: #111; */
|
||||||
|
border-radius: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
padding: 30px;
|
||||||
|
padding-bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back {
|
||||||
|
color: rgb(185, 179, 170);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back svg {
|
||||||
|
display: block;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
padding-left: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poemContainer {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poemText {
|
||||||
|
padding: 0 60px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eleven {
|
||||||
|
font-size: 105%;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ten {
|
||||||
|
font-size: 100%;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nine {
|
||||||
|
font-size: 90%;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eight {
|
||||||
|
font-size: 80%;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seven {
|
||||||
|
font-size: 70%;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.six {
|
||||||
|
font-size: 60%;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poemImg {
|
||||||
|
max-width: 100%;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -1,16 +1,9 @@
|
|||||||
body {
|
:root {
|
||||||
height: 100vh;
|
--bs-body-bg: rgb(17, 17, 17) !important;
|
||||||
overflow: hidden;
|
|
||||||
color: white;
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content, .modal-content .form-select {
|
|
||||||
background-color: rgb(17, 17, 17) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* В связи со сложившейся политической обстановкой */
|
/* В связи со сложившейся политической обстановкой */
|
||||||
.leaflet-attribution-flag {
|
.leaflet-attribution-flag {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: -100px;
|
right: -100px;
|
||||||
}
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
import { Announcement, AnnouncementResponse } from './types'
|
||||||
|
|
||||||
|
const processAnnouncement = (data: AnnouncementResponse): Announcement => ({
|
||||||
|
...data,
|
||||||
|
lat: data.latitude,
|
||||||
|
lng: data.longtitude,
|
||||||
|
bestBy: data.best_by,
|
||||||
|
bookedBy: data.booked_by,
|
||||||
|
userId: data.user_id
|
||||||
|
})
|
||||||
|
|
||||||
|
export { processAnnouncement }
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { API_URL } from '../../config'
|
import { API_URL } from '../../config'
|
||||||
import { FiltersType, URLEncodeFilters } from '../../utils/filters'
|
import { FiltersType, URLEncodeFilters } from '../../utils/filters'
|
||||||
|
import { processAnnouncement } from '../announcement'
|
||||||
import { Announcement } from '../announcement/types'
|
import { Announcement } from '../announcement/types'
|
||||||
import { AnnouncementsResponse } from './types'
|
import { AnnouncementsResponse } from './types'
|
||||||
|
|
||||||
@ -9,17 +10,8 @@ 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
|
data.list_of_announcements.map(processAnnouncement)
|
||||||
|
)
|
||||||
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 }
|
export { initialAnnouncements, composeAnnouncementsURL, processAnnouncements }
|
||||||
|
25
front/src/api/user/index.ts
Normal file
25
front/src/api/user/index.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { API_URL } from '../../config'
|
||||||
|
import { UserResponse, User } from './types'
|
||||||
|
|
||||||
|
import { faker } from '@faker-js/faker/locale/ru'
|
||||||
|
|
||||||
|
|
||||||
|
const initialUser: User = import.meta.env.DEV ? { // Temporary, until api is realized
|
||||||
|
id: Math.random() * 100,
|
||||||
|
name: faker.person.firstName() + ' ' + faker.person.lastName(),
|
||||||
|
regDate: faker.date.anytime().getTime(),
|
||||||
|
} : {
|
||||||
|
id: -1,
|
||||||
|
name: '',
|
||||||
|
regDate: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const composeUserURL = () => (
|
||||||
|
API_URL + '/user?'
|
||||||
|
)
|
||||||
|
|
||||||
|
const processUser = (data: UserResponse): User => {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export { initialUser, composeUserURL, processUser }
|
29
front/src/api/user/types.ts
Normal file
29
front/src/api/user/types.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { isObject } from '../../utils/types'
|
||||||
|
|
||||||
|
type User = {
|
||||||
|
id: number,
|
||||||
|
name: string,
|
||||||
|
regDate: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUser = (obj: unknown): obj is User => (
|
||||||
|
isObject(obj, {
|
||||||
|
'id': 'number',
|
||||||
|
'name': 'string',
|
||||||
|
'regDate': 'number',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserResponse = User
|
||||||
|
|
||||||
|
// const isUserResponse = (obj: unknown): obj is UserResponse => (
|
||||||
|
// isObject(obj, {
|
||||||
|
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
|
||||||
|
const isUserResponse = isUser
|
||||||
|
|
||||||
|
export type { UserResponse, User }
|
||||||
|
|
||||||
|
export { isUserResponse, isUser }
|
4
front/src/assets/backArrow.svg
Normal file
4
front/src/assets/backArrow.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="rgb(185, 179, 170)" width="24" height="24" class="bi bi-arrow-left" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 329 B |
4
front/src/assets/rightAngle.svg
Normal file
4
front/src/assets/rightAngle.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg fill="rgb(185, 179, 170)" width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 512">
|
||||||
|
<!--! Font Awesome Free 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. -->
|
||||||
|
<path d="M64 448c-8.188 0-16.38-3.125-22.62-9.375c-12.5-12.5-12.5-32.75 0-45.25L178.8 256L41.38 118.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0l160 160c12.5 12.5 12.5 32.75 0 45.25l-160 160C80.38 444.9 72.19 448 64 448z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 569 B |
45
front/src/assets/userCategories.ts
Normal file
45
front/src/assets/userCategories.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { Announcement } from '../api/announcement/types'
|
||||||
|
import { FiltersType } from '../utils/filters'
|
||||||
|
|
||||||
|
const userCategories = ['givingOut', 'booked', 'history'] as const
|
||||||
|
|
||||||
|
type UserCategory = typeof userCategories[number]
|
||||||
|
|
||||||
|
const UserCategoriesNames: Record<UserCategory, string> = {
|
||||||
|
givingOut: 'Раздача',
|
||||||
|
booked: 'Бронь',
|
||||||
|
history: 'История',
|
||||||
|
}
|
||||||
|
|
||||||
|
const userCategoriesInfos: Record<UserCategory, (ann: Announcement) => string> = {
|
||||||
|
givingOut: (ann: Announcement) => (
|
||||||
|
`Годен до ${new Date(ann.bestBy).toLocaleDateString('ru')}`
|
||||||
|
),
|
||||||
|
booked: (ann: Announcement) => (
|
||||||
|
`Бронь ещё ${(ann as Announcement & { bookedBy: number[] }).bookedBy.length} чел.`
|
||||||
|
),
|
||||||
|
history: (ann: Announcement) => (
|
||||||
|
`Забрал ${new Date((ann as Announcement & { taken: number }).taken).toLocaleDateString('ru')}`
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
const composeUserCategoriesFilters: Record<UserCategory, () => FiltersType> = {
|
||||||
|
givingOut: () => {
|
||||||
|
const userId = -1
|
||||||
|
|
||||||
|
return ({ userId })
|
||||||
|
},
|
||||||
|
booked: () => {
|
||||||
|
const userId = -1
|
||||||
|
|
||||||
|
return ({ bookedBy: userId })
|
||||||
|
},
|
||||||
|
history: () => {
|
||||||
|
const userId = -1
|
||||||
|
|
||||||
|
return ({ userId, status: 'taken' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { UserCategory }
|
||||||
|
export { userCategories, UserCategoriesNames, userCategoriesInfos, composeUserCategoriesFilters }
|
25
front/src/components/BackHeader.tsx
Normal file
25
front/src/components/BackHeader.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { Navbar } from 'react-bootstrap'
|
||||||
|
|
||||||
|
import BackButton from '../assets/backArrow.svg'
|
||||||
|
|
||||||
|
type BackHeaderProps = {
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function BackHeader({ text }: BackHeaderProps) {
|
||||||
|
return (
|
||||||
|
<Navbar>
|
||||||
|
<Navbar.Brand as={Link} to='/'>
|
||||||
|
<img src={BackButton} alt='Go back' />
|
||||||
|
</Navbar.Brand>
|
||||||
|
<Navbar.Text className='me-auto'>
|
||||||
|
<h4 className='mb-0'>
|
||||||
|
{text}
|
||||||
|
</h4>
|
||||||
|
</Navbar.Text>
|
||||||
|
</Navbar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BackHeader
|
25
front/src/components/CategoryPreview.tsx
Normal file
25
front/src/components/CategoryPreview.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { StoriesPreview } from '.'
|
||||||
|
import { UserCategoriesNames, UserCategory, composeUserCategoriesFilters } from '../assets/userCategories'
|
||||||
|
import { useAnnouncements } from '../hooks/api'
|
||||||
|
import { gotError } from '../hooks/useFetch'
|
||||||
|
|
||||||
|
type CategoryPreviewProps = {
|
||||||
|
category: UserCategory
|
||||||
|
}
|
||||||
|
|
||||||
|
function CategoryPreview({ category }: CategoryPreviewProps) {
|
||||||
|
const announcements = useAnnouncements(composeUserCategoriesFilters[category]())
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<h4 className='fw-bold'>{UserCategoriesNames[category]}</h4>
|
||||||
|
{gotError(announcements) ? (
|
||||||
|
<p className='text-danger'>{announcements.error}</p>
|
||||||
|
) : (announcements.loading ? 'Загрузка...' :
|
||||||
|
<StoriesPreview announcements={announcements.data} category={category} />
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CategoryPreview
|
143
front/src/components/StoriesPreview.tsx
Normal file
143
front/src/components/StoriesPreview.tsx
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { CSSProperties, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { Button } from 'react-bootstrap'
|
||||||
|
|
||||||
|
import { UserCategory, composeUserCategoriesFilters, userCategoriesInfos } from '../assets/userCategories'
|
||||||
|
import { Announcement } from '../api/announcement/types'
|
||||||
|
import { categoryGraphics, categoryNames } from '../assets/category'
|
||||||
|
import { URLEncodeFilters } from '../utils/filters'
|
||||||
|
|
||||||
|
import rightAngleIcon from '../assets/rightAngle.svg'
|
||||||
|
|
||||||
|
type StoriesPreviewProps = {
|
||||||
|
announcements: Announcement[],
|
||||||
|
category: UserCategory,
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
container: {
|
||||||
|
transform: 'translateX(0)',
|
||||||
|
} as CSSProperties,
|
||||||
|
ul: {
|
||||||
|
display: 'flex',
|
||||||
|
gap: 10,
|
||||||
|
listStyleType: 'none',
|
||||||
|
overflow: 'scroll',
|
||||||
|
paddingLeft: 0,
|
||||||
|
scrollBehavior: 'smooth',
|
||||||
|
} as CSSProperties,
|
||||||
|
link: {
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: 'var(--bs-body-color)'
|
||||||
|
} as CSSProperties,
|
||||||
|
image: {
|
||||||
|
height: '25vh',
|
||||||
|
maxWidth: 'calc(25vh * 9 / 16)',
|
||||||
|
objectFit: 'contain',
|
||||||
|
borderRadius: 12,
|
||||||
|
marginBottom: 5,
|
||||||
|
} as CSSProperties,
|
||||||
|
title: {
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
marginBottom: 5,
|
||||||
|
} as CSSProperties,
|
||||||
|
scrollButton: {
|
||||||
|
position: 'fixed',
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
zIndex: 100,
|
||||||
|
background: 'linear-gradient(to right, rgba(17, 17, 17, 0) 0%, rgba(17, 17, 17, 255) 100%)',
|
||||||
|
display: 'block',
|
||||||
|
height: '100%',
|
||||||
|
width: '10%',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'default',
|
||||||
|
borderRadius: 0,
|
||||||
|
} as CSSProperties,
|
||||||
|
leftScrollButton: {
|
||||||
|
left: 0,
|
||||||
|
transform: 'scaleX(-1)'
|
||||||
|
} as CSSProperties,
|
||||||
|
rightScrollButton: {
|
||||||
|
right: 0,
|
||||||
|
} as CSSProperties,
|
||||||
|
}
|
||||||
|
|
||||||
|
function StoriesPreview({ announcements, category }: StoriesPreviewProps) {
|
||||||
|
const ulElement = useRef<HTMLUListElement | null>(null)
|
||||||
|
const [showScrollButtons, setShowScrollButtons] = useState({ left: false, right: false })
|
||||||
|
|
||||||
|
const determineShowScrollButtons = (ul: HTMLUListElement) => (
|
||||||
|
setShowScrollButtons({
|
||||||
|
left: ul.scrollLeft > 0,
|
||||||
|
right: ul.scrollLeft < (ul.scrollWidth - ul.clientWidth),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const ul = ulElement.current
|
||||||
|
if (ul) {
|
||||||
|
determineShowScrollButtons(ul)
|
||||||
|
|
||||||
|
const f = () => determineShowScrollButtons(ul)
|
||||||
|
|
||||||
|
ul.addEventListener('scroll', f)
|
||||||
|
|
||||||
|
return () => ul.removeEventListener('scroll', f)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const ul = ulElement.current
|
||||||
|
|
||||||
|
if (ul) {
|
||||||
|
determineShowScrollButtons(ul)
|
||||||
|
}
|
||||||
|
}, [announcements])
|
||||||
|
|
||||||
|
const doScroll = (forward: boolean) => () => {
|
||||||
|
const ul = ulElement.current
|
||||||
|
if (ul) {
|
||||||
|
const storyWidth = window.innerHeight * 0.25 * 9 / 16 + 10
|
||||||
|
ul.scrollLeft += forward ? storyWidth : -storyWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div style={styles.container}>
|
||||||
|
{showScrollButtons.left &&
|
||||||
|
<Button onClick={doScroll(false)} style={{ ...styles.scrollButton, ...styles.leftScrollButton }}>
|
||||||
|
<img src={rightAngleIcon} alt='Показать ещё' />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
<ul style={styles.ul} className='StoriesPreview_ul' ref={ulElement}>
|
||||||
|
{useMemo(() => announcements.map((ann, i) => (
|
||||||
|
<li key={`${category}${i}`}>
|
||||||
|
<Link to={'/?' + new URLSearchParams({
|
||||||
|
...URLEncodeFilters(composeUserCategoriesFilters[category]()),
|
||||||
|
storyIndex: i.toString()
|
||||||
|
}).toString()} style={styles.link}>
|
||||||
|
{ann.src?.endsWith('mp4') ? (
|
||||||
|
<video src={ann.src} style={styles.image} />
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
src={ann.src || categoryGraphics[ann.category]}
|
||||||
|
alt={'Изображение' + (ann.src ? 'предмета' : categoryNames[ann.category])}
|
||||||
|
style={styles.image}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<p style={styles.title}>{ann.name}</p>
|
||||||
|
<p style={styles.title}>{userCategoriesInfos[category](ann)}</p>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
)), [announcements, category])}
|
||||||
|
</ul>
|
||||||
|
{showScrollButtons.right &&
|
||||||
|
<Button onClick={doScroll(true)} style={{ ...styles.scrollButton, ...styles.rightScrollButton }}>
|
||||||
|
<img src={rightAngleIcon} alt='Показать ещё' />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StoriesPreview
|
@ -7,3 +7,6 @@ export { default as TrashboxMarkers } from './TrashboxMarkers'
|
|||||||
export { default as WithToken } from './WithToken'
|
export { default as WithToken } from './WithToken'
|
||||||
export { default as ClickHandler } from './ClickHandler'
|
export { default as ClickHandler } from './ClickHandler'
|
||||||
export { default as AuthForm } from './AuthForm'
|
export { default as AuthForm } from './AuthForm'
|
||||||
|
export { default as BackHeader } from './BackHeader'
|
||||||
|
export { default as CategoryPreview } from './CategoryPreview'
|
||||||
|
export { default as StoriesPreview } from './StoriesPreview'
|
||||||
|
@ -4,3 +4,4 @@ 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'
|
export { default as useOsmAddresses } from './useOsmAddress'
|
||||||
|
export { default as useUser } from './useUser'
|
||||||
|
23
front/src/hooks/api/useUser.ts
Normal file
23
front/src/hooks/api/useUser.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { initialUser } from '../../api/user'
|
||||||
|
import { User } from '../../api/user/types'
|
||||||
|
import { UseFetchReturn } from '../useFetch'
|
||||||
|
|
||||||
|
const useUser = (): UseFetchReturn<User> => (
|
||||||
|
// useFetch(
|
||||||
|
// composeUserUrl(getToken()),
|
||||||
|
// 'GET',
|
||||||
|
// true,
|
||||||
|
// isUserResponse,
|
||||||
|
// processUser,
|
||||||
|
// initialUser
|
||||||
|
// )
|
||||||
|
|
||||||
|
{
|
||||||
|
data: initialUser,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
setData: () => {0}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default useUser
|
@ -1,3 +1,5 @@
|
|||||||
export { default as useStoryDimensions } from './useStoryDimensions'
|
export { default as useStoryDimensions } from './useStoryDimensions'
|
||||||
export { default as useSend } from './useSend'
|
export { default as useSend } from './useSend'
|
||||||
export { default as useFetch } from './useFetch'
|
export { default as useFetch } from './useFetch'
|
||||||
|
export { default as useStoryIndex } from './useStoryIndex'
|
||||||
|
export { default as useFilters } from './useFilters'
|
||||||
|
@ -3,40 +3,31 @@ import { useEffect, useState } from 'react'
|
|||||||
import { SetState } from '../utils/types'
|
import { SetState } from '../utils/types'
|
||||||
import useSend from './useSend'
|
import useSend from './useSend'
|
||||||
|
|
||||||
type UseFetchShared = {
|
type UseFetchShared<T> = {
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
abort?: () => void,
|
abort?: () => void,
|
||||||
|
setData: SetState<T | undefined>
|
||||||
}
|
}
|
||||||
|
|
||||||
type UseFetchSucced<T> = {
|
type UseFetchSucced<T> = {
|
||||||
error: null,
|
error: null,
|
||||||
data: T,
|
data: T,
|
||||||
} & UseFetchShared
|
} & UseFetchShared<T>
|
||||||
|
|
||||||
type UseFetchErrored = {
|
type UseFetchErrored<T> = {
|
||||||
error: string,
|
error: string,
|
||||||
data: undefined
|
data: undefined
|
||||||
} & UseFetchShared
|
} & UseFetchShared<T>
|
||||||
|
|
||||||
const gotError = <T>(res: UseFetchErrored | UseFetchSucced<T>): res is UseFetchErrored => (
|
const gotError = <T>(res: UseFetchErrored<T> | UseFetchSucced<T>): res is UseFetchErrored<T> => (
|
||||||
typeof res.error === 'string'
|
typeof res.error === 'string'
|
||||||
)
|
)
|
||||||
|
|
||||||
const fallbackError = <T>(res: UseFetchSucced<T> | UseFetchErrored) => (
|
const fallbackError = <T>(res: UseFetchSucced<T> | UseFetchErrored<T>) => (
|
||||||
gotError(res) ? res.error : res.data
|
gotError(res) ? res.error : res.data
|
||||||
)
|
)
|
||||||
|
|
||||||
type UseFetchReturn<T> = ({
|
type UseFetchReturn<T> = UseFetchSucced<T> | UseFetchErrored<T>
|
||||||
error: null,
|
|
||||||
data: T
|
|
||||||
} | {
|
|
||||||
error: string,
|
|
||||||
data: undefined
|
|
||||||
}) & {
|
|
||||||
loading: boolean,
|
|
||||||
setData: SetState<T | undefined>
|
|
||||||
abort?: (() => void)
|
|
||||||
}
|
|
||||||
|
|
||||||
function useFetch<R, T>(
|
function useFetch<R, T>(
|
||||||
url: string,
|
url: string,
|
||||||
@ -70,6 +61,8 @@ function useFetch<R, T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type { UseFetchReturn }
|
||||||
|
|
||||||
export default useFetch
|
export default useFetch
|
||||||
|
|
||||||
export { gotError, fallbackError }
|
export { gotError, fallbackError }
|
||||||
|
47
front/src/hooks/useFilters.ts
Normal file
47
front/src/hooks/useFilters.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useSearchParams } from 'react-router-dom'
|
||||||
|
import { FiltersType, URLDecoreFilters, URLEncodeFilters, defaultFilters } from '../utils/filters'
|
||||||
|
import { SetState } from '../utils/types'
|
||||||
|
|
||||||
|
function useFilters(initialFilters: FiltersType = defaultFilters): [FiltersType, SetState<FiltersType>] {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
|
||||||
|
const [filters, setFilters] = useState(initialFilters)
|
||||||
|
|
||||||
|
const appendFiltersSearchParams = (filters: FiltersType) => (
|
||||||
|
setSearchParams(params => ({
|
||||||
|
...Object.fromEntries(params),
|
||||||
|
...URLEncodeFilters(filters)
|
||||||
|
}), { replace: true })
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const urlFilters = URLDecoreFilters(searchParams)
|
||||||
|
|
||||||
|
setFilters(prev => ({
|
||||||
|
...prev,
|
||||||
|
...urlFilters,
|
||||||
|
}))
|
||||||
|
|
||||||
|
appendFiltersSearchParams({
|
||||||
|
...URLEncodeFilters(initialFilters),
|
||||||
|
...URLEncodeFilters(urlFilters),
|
||||||
|
})
|
||||||
|
// searchParams have actual query string at first render
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const withQuery = (f: SetState<FiltersType>) => (
|
||||||
|
(nextInit: (FiltersType | ((prev: FiltersType) => FiltersType))) => {
|
||||||
|
const newFilters = (typeof nextInit === 'function') ? nextInit(filters) : nextInit
|
||||||
|
|
||||||
|
appendFiltersSearchParams(newFilters)
|
||||||
|
|
||||||
|
f(nextInit)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return [filters, withQuery(setFilters)]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useFilters
|
@ -14,12 +14,14 @@ function useStoryDimensions(maxRatio = 16 / 9) {
|
|||||||
function handleResize() {
|
function handleResize() {
|
||||||
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 bottomBarHeight = 56
|
||||||
|
|
||||||
|
const height = windowDimensions.height - bottomBarHeight
|
||||||
|
|
||||||
const ratio = Math.max(maxRatio, height / windowDimensions.width)
|
const ratio = Math.max(maxRatio, height / windowDimensions.width)
|
||||||
|
|
||||||
|
47
front/src/hooks/useStoryIndex.ts
Normal file
47
front/src/hooks/useStoryIndex.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useSearchParams } from 'react-router-dom'
|
||||||
|
import { SetState } from '../utils/types'
|
||||||
|
|
||||||
|
function useStoryIndex(annLength: number | undefined) {
|
||||||
|
const [index, setIndex] = useState(0)
|
||||||
|
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
|
||||||
|
const withReset = <T>(f: SetState<T>) => (...args: Parameters<SetState<T>>) => {
|
||||||
|
console.log('resetting index')
|
||||||
|
setIndex(0)
|
||||||
|
setSearchParams(prev => ({ ...prev, storyIndex: '0' }), { replace: true })
|
||||||
|
f(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIndex(annLength ?
|
||||||
|
Number.parseInt(searchParams.get('storyIndex') || '0') :
|
||||||
|
0)
|
||||||
|
// searchParams have actual query string at first render
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [annLength])
|
||||||
|
|
||||||
|
const increment = () => setIndex(prev => {
|
||||||
|
const newIndex = (prev + 1) % (annLength || 1)
|
||||||
|
setSearchParams(prev => ({ ...prev, storyIndex: newIndex.toString() }), { replace: true })
|
||||||
|
|
||||||
|
return newIndex
|
||||||
|
})
|
||||||
|
|
||||||
|
const decrement = () => setIndex(prev => {
|
||||||
|
const newIndex = prev > 0 ? (prev - 1) : 0
|
||||||
|
setSearchParams(prev => ({ ...prev, storyIndex: newIndex.toString() }), { replace: true })
|
||||||
|
|
||||||
|
return newIndex
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
n: index,
|
||||||
|
withReset,
|
||||||
|
increment,
|
||||||
|
decrement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useStoryIndex
|
@ -1,16 +1,16 @@
|
|||||||
import { CSSProperties, useEffect, useState } from 'react'
|
import { CSSProperties, useEffect, useMemo, 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'
|
||||||
|
|
||||||
import { BottomNavBar, AnnouncementDetails, Filters } from '../components'
|
import { BottomNavBar, AnnouncementDetails, Filters } from '../components'
|
||||||
import { useStoryDimensions } from '../hooks'
|
import { useFilters, useStoryDimensions } from '../hooks'
|
||||||
import { useAnnouncements } from '../hooks/api'
|
import { useAnnouncements } from '../hooks/api'
|
||||||
import { defaultFilters } from '../utils/filters'
|
|
||||||
import { Announcement } from '../api/announcement/types'
|
import { Announcement } from '../api/announcement/types'
|
||||||
import { categoryGraphics } from '../assets/category'
|
import { categoryGraphics } from '../assets/category'
|
||||||
|
import { UseFetchReturn, gotError } from '../hooks/useFetch'
|
||||||
|
import { useStoryIndex } from '../hooks'
|
||||||
|
|
||||||
import puffSpinner from '../assets/puff.svg'
|
import puffSpinner from '../assets/puff.svg'
|
||||||
import { gotError } from '../hooks/useFetch'
|
|
||||||
|
|
||||||
function generateStories(announcements: Announcement[]): Story[] {
|
function generateStories(announcements: Announcement[]): Story[] {
|
||||||
return announcements.map(announcement => {
|
return announcements.map(announcement => {
|
||||||
@ -23,14 +23,14 @@ function generateStories(announcements: Announcement[]): Story[] {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function fallbackGenerateStories(announcementsFetch: ReturnType<typeof useAnnouncements>) {
|
function fallbackGenerateStories(announcements: UseFetchReturn<Announcement[]>) {
|
||||||
if (announcementsFetch.loading)
|
if (announcements.loading)
|
||||||
return fallbackStory()
|
return fallbackStory()
|
||||||
|
|
||||||
if (gotError(announcementsFetch))
|
if (gotError(announcements))
|
||||||
return fallbackStory(announcementsFetch.error, true)
|
return fallbackStory(announcements.error, true)
|
||||||
|
|
||||||
const stories = generateStories(announcementsFetch.data)
|
const stories = generateStories(announcements.data)
|
||||||
|
|
||||||
if (stories.length === 0)
|
if (stories.length === 0)
|
||||||
return fallbackStory('Здесь пока пусто')
|
return fallbackStory('Здесь пока пусто')
|
||||||
@ -66,16 +66,23 @@ function HomePage() {
|
|||||||
const { height, width } = useStoryDimensions(16 / 9)
|
const { height, width } = useStoryDimensions(16 / 9)
|
||||||
|
|
||||||
const [filterShown, setFilterShown] = useState(false)
|
const [filterShown, setFilterShown] = useState(false)
|
||||||
const [filter, setFilter] = useState(defaultFilters)
|
|
||||||
|
|
||||||
const announcementsFetch = useAnnouncements(filter)
|
const [filter, setFilter] = useFilters()
|
||||||
|
|
||||||
const stories = fallbackGenerateStories(announcementsFetch)
|
const announcements = useAnnouncements(filter)
|
||||||
|
|
||||||
|
const stories = useMemo(() => fallbackGenerateStories(announcements), [announcements])
|
||||||
|
|
||||||
|
const index = useStoryIndex(announcements.data?.length)
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<Filters filter={filter} setFilter={setFilter} filterShown={filterShown} setFilterShown={setFilterShown} />
|
<Filters filter={filter} setFilter={index.withReset(setFilter)} filterShown={filterShown} setFilterShown={setFilterShown} />
|
||||||
<div style={styles.container}>
|
<div style={styles.container}>
|
||||||
<Stories
|
<Stories
|
||||||
|
currentIndex={index.n}
|
||||||
|
onStoryEnd={index.increment}
|
||||||
|
onNext={index.increment}
|
||||||
|
onPrevious={index.decrement}
|
||||||
stories={stories}
|
stories={stories}
|
||||||
defaultInterval={11000}
|
defaultInterval={11000}
|
||||||
height={height}
|
height={height}
|
||||||
|
@ -1,9 +1,26 @@
|
|||||||
import { Link } from 'react-router-dom'
|
import { Container } from 'react-bootstrap'
|
||||||
|
|
||||||
|
import BackHeader from '../components/BackHeader'
|
||||||
|
import { useUser } from '../hooks/api'
|
||||||
|
import { userCategories } from '../assets/userCategories'
|
||||||
|
import { CategoryPreview } from '../components'
|
||||||
|
import { gotError } from '../hooks/useFetch'
|
||||||
|
|
||||||
function UserPage() {
|
function UserPage() {
|
||||||
/* TODO */
|
const user = useUser()
|
||||||
|
|
||||||
return <h1>For Yet Go <Link to='/'>Home</Link></h1>
|
return (
|
||||||
|
<Container style={{ maxWidth: 'calc(100vh*9/16)' }}>
|
||||||
|
<BackHeader text={
|
||||||
|
gotError(user) ?
|
||||||
|
user.error :
|
||||||
|
`${user.data.name}, с нами с ${new Date(user.data.regDate).toLocaleDateString('ru')}`
|
||||||
|
} />
|
||||||
|
{userCategories.map(cat => (
|
||||||
|
<CategoryPreview key={cat} category={cat} />
|
||||||
|
))}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UserPage
|
export default UserPage
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { Announcement } from '../api/announcement/types'
|
import { Announcement } from '../api/announcement/types'
|
||||||
|
import { isCategory } from '../assets/category'
|
||||||
|
import { fallbackToUndefined, isInt } from './types'
|
||||||
|
|
||||||
const filterNames = ['userId', 'category', 'metro', 'bookedBy'] as const
|
const filterNames = ['userId', 'category', 'metro', 'bookedBy'] as const
|
||||||
type FilterNames = typeof filterNames[number]
|
type FilterNames = typeof filterNames[number]
|
||||||
@ -10,10 +12,31 @@ const defaultFilters: FiltersType = { userId: undefined, category: undefined, me
|
|||||||
const URLEncodeFilters = (filters: FiltersType) => (
|
const URLEncodeFilters = (filters: FiltersType) => (
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
filterNames.map(
|
filterNames.map(
|
||||||
fName => [fName, filters[fName]?.toString()]
|
fName => {
|
||||||
|
const v = filters[fName]
|
||||||
|
if (v) {
|
||||||
|
return [fName, encodeURIComponent(v)]
|
||||||
|
}
|
||||||
|
return [fName, undefined]
|
||||||
|
}
|
||||||
).filter((p): p is [string, string] => typeof p[1] !== 'undefined')
|
).filter((p): p is [string, string] => typeof p[1] !== 'undefined')
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const URLDecoreFilters = (params: URLSearchParams): FiltersType => {
|
||||||
|
const strFilters = Object.fromEntries(
|
||||||
|
filterNames.map(
|
||||||
|
fName => [fName, params.get(fName)]
|
||||||
|
).filter((p): p is [string, string] => p[1] !== null)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
bookedBy: fallbackToUndefined(strFilters['bookedBy'], isInt),
|
||||||
|
category: fallbackToUndefined(strFilters['category'], isCategory),
|
||||||
|
metro: strFilters['metro'],
|
||||||
|
userId: fallbackToUndefined(strFilters['userId'], isInt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type { FilterNames, FiltersType }
|
export type { FilterNames, FiltersType }
|
||||||
export { defaultFilters, filterNames, URLEncodeFilters }
|
export { defaultFilters, filterNames, URLEncodeFilters, URLDecoreFilters }
|
||||||
|
@ -60,8 +60,18 @@ const isString = (obj: unknown): obj is string => (
|
|||||||
typeof obj === 'string'
|
typeof obj === 'string'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const isInt = (obj: unknown): obj is number => (
|
||||||
|
Number.isSafeInteger(obj)
|
||||||
|
)
|
||||||
|
|
||||||
|
function fallbackToUndefined<T>(obj: unknown, isT: ((obj: unknown) => obj is T)) {
|
||||||
|
if (!isT(obj)) return undefined
|
||||||
|
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
type SetState<T> = React.Dispatch<React.SetStateAction<T>>
|
type SetState<T> = React.Dispatch<React.SetStateAction<T>>
|
||||||
|
|
||||||
export type { SetState }
|
export type { SetState }
|
||||||
|
|
||||||
export { isRecord, isObject, isConst, isLiteralUnion, isArrayOf, isString }
|
export { isRecord, isObject, isConst, isLiteralUnion, isArrayOf, isString, isInt, fallbackToUndefined }
|
||||||
|
@ -1 +0,0 @@
|
|||||||
Generic single-database configuration.
|
|
@ -1,85 +0,0 @@
|
|||||||
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()
|
|
@ -1,24 +0,0 @@
|
|||||||
"""${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"}
|
|
@ -1,28 +0,0 @@
|
|||||||
"""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 ###
|
|
@ -1,34 +0,0 @@
|
|||||||
"""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 ###
|
|
@ -1,70 +0,0 @@
|
|||||||
"""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