15 Commits

Author SHA1 Message Date
6c6f96cc94 Trying to change models 2023-07-24 07:07:36 +03:00
45d0b8e52d Alembic installed and activated. Poems table added 2023-07-23 22:52:31 +03:00
1055416640 Still no result 2023-07-20 00:09:30 +03:00
52d9ad3399 Prepare to use another auth code 2023-07-19 23:24:42 +03:00
30140f058f Добавлена response_model=User в get_current_user 2023-07-19 00:14:56 +03:00
d5ba710885 добавили responce_model=User к get_current_user 2023-07-19 00:11:57 +03:00
6127dd8ba4 добавлен параметр response_model к get_user 2023-07-19 00:10:59 +03:00
9e4bb1b99f К схемам из schema.py добавлены доп. поля (соотв. models) 2023-07-19 00:05:42 +03:00
d66b9004e0 fastapi.Responce has been imported 2023-07-18 23:56:07 +03:00
626170964f pass new parameters to sessionmaker 2023-07-18 23:47:01 +03:00
1dd37a72b4 parameters of sessionmaker changed 2023-07-18 23:43:23 +03:00
b39d9ada27 Imported modules corrected 2023-07-18 23:39:04 +03:00
91842dcc51 Merge branch 'main' of https://git.dm1sh.ru/dm1sh/porridger 2023-07-17 23:36:46 +03:00
09ba6a3478 Разделил модели базы данных и модели pydantic. 2023-07-17 23:34:04 +03:00
808edad6b4 Необходимо решить проблему get_user 2023-07-17 03:41:20 +03:00
40 changed files with 537 additions and 946 deletions

102
alembic.ini Normal file
View 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
View File

@ -0,0 +1,2 @@
from .db import Base
from .models import UserDatabase, Announcement, Trashbox

View File

@ -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)
Base = declarative_base() database = SessionLocal()
Base = declarative_base()

View File

@ -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,
@ -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(UserDatabase.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 уже зарегестрирован."}
@ -153,7 +166,7 @@ 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,
@ -167,8 +180,8 @@ async def login_for_access_token(
return access_token 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

View File

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

View File

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

@ -0,0 +1,2 @@
from sqlalchemy.orm import Session

View File

@ -1,52 +1,24 @@
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")
@ -60,17 +32,15 @@ def get_password_hash(password):
# проблема здесь # проблема здесь
def get_user(db, email: str): def get_user(db: Session, email: str):
user = None user_with_required_email = db.query(UserDatabase).filter(UserDatabase.email == email).first()
for person_with_correct_email in db.query(UserDatabase): print(user_with_required_email)
if person_with_correct_email.email == email: if user_with_required_email:
user = person_with_correct_email 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
@ -90,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",
@ -104,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
@ -115,4 +85,8 @@ 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

View File

@ -20,7 +20,6 @@
"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",
@ -818,22 +817,6 @@
"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",

View File

@ -24,7 +24,6 @@
"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",

View File

@ -1,315 +0,0 @@
<!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>

View File

@ -1,9 +1,16 @@
:root { body {
--bs-body-bg: rgb(17, 17, 17) !important; height: 100vh;
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;
} }

View File

@ -1,12 +0,0 @@
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 }

View File

@ -1,6 +1,5 @@
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'
@ -10,8 +9,17 @@ 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[] => {
data.list_of_announcements.map(processAnnouncement) const annList = data.list_of_announcements
)
return annList.map(ann => ({
...ann,
lat: ann.latitude,
lng: ann.longtitude,
bestBy: ann.best_by,
bookedBy: ann.booked_by,
userId: ann.user_id
}))
}
export { initialAnnouncements, composeAnnouncementsURL, processAnnouncements } export { initialAnnouncements, composeAnnouncementsURL, processAnnouncements }

View File

@ -1,25 +0,0 @@
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 }

View File

@ -1,29 +0,0 @@
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 }

View File

@ -1,4 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 329 B

View File

@ -1,4 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 569 B

View File

@ -1,45 +0,0 @@
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 }

View File

@ -1,25 +0,0 @@
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

View File

@ -1,25 +0,0 @@
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

View File

@ -1,143 +0,0 @@
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

View File

@ -7,6 +7,3 @@ 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'

View File

@ -4,4 +4,3 @@ 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'

View File

@ -1,23 +0,0 @@
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

View File

@ -1,5 +1,3 @@
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'

View File

@ -3,31 +3,40 @@ 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<T> = { type UseFetchShared = {
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<T> } & UseFetchShared
type UseFetchErrored<T> = { type UseFetchErrored = {
error: string, error: string,
data: undefined data: undefined
} & UseFetchShared<T> } & UseFetchShared
const gotError = <T>(res: UseFetchErrored<T> | UseFetchSucced<T>): res is UseFetchErrored<T> => ( const gotError = <T>(res: UseFetchErrored | UseFetchSucced<T>): res is UseFetchErrored => (
typeof res.error === 'string' typeof res.error === 'string'
) )
const fallbackError = <T>(res: UseFetchSucced<T> | UseFetchErrored<T>) => ( const fallbackError = <T>(res: UseFetchSucced<T> | UseFetchErrored) => (
gotError(res) ? res.error : res.data gotError(res) ? res.error : res.data
) )
type UseFetchReturn<T> = UseFetchSucced<T> | UseFetchErrored<T> type UseFetchReturn<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,
@ -61,8 +70,6 @@ function useFetch<R, T>(
} }
} }
export type { UseFetchReturn }
export default useFetch export default useFetch
export { gotError, fallbackError } export { gotError, fallbackError }

View File

@ -1,47 +0,0 @@
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

View File

@ -14,14 +14,12 @@ 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 bottomBarHeight = 56 const height = windowDimensions.height - 56
const height = windowDimensions.height - bottomBarHeight
const ratio = Math.max(maxRatio, height / windowDimensions.width) const ratio = Math.max(maxRatio, height / windowDimensions.width)

View File

@ -1,47 +0,0 @@
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

View File

@ -1,16 +1,16 @@
import { CSSProperties, useEffect, useMemo, 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'
import { BottomNavBar, AnnouncementDetails, Filters } from '../components' import { BottomNavBar, AnnouncementDetails, Filters } from '../components'
import { useFilters, useStoryDimensions } from '../hooks' import { 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(announcements: UseFetchReturn<Announcement[]>) { function fallbackGenerateStories(announcementsFetch: ReturnType<typeof useAnnouncements>) {
if (announcements.loading) if (announcementsFetch.loading)
return fallbackStory() return fallbackStory()
if (gotError(announcements)) if (gotError(announcementsFetch))
return fallbackStory(announcements.error, true) return fallbackStory(announcementsFetch.error, true)
const stories = generateStories(announcements.data) const stories = generateStories(announcementsFetch.data)
if (stories.length === 0) if (stories.length === 0)
return fallbackStory('Здесь пока пусто') return fallbackStory('Здесь пока пусто')
@ -66,23 +66,16 @@ 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 [filter, setFilter] = useFilters() const announcementsFetch = useAnnouncements(filter)
const announcements = useAnnouncements(filter) const stories = fallbackGenerateStories(announcementsFetch)
const stories = useMemo(() => fallbackGenerateStories(announcements), [announcements])
const index = useStoryIndex(announcements.data?.length)
return (<> return (<>
<Filters filter={filter} setFilter={index.withReset(setFilter)} filterShown={filterShown} setFilterShown={setFilterShown} /> <Filters filter={filter} setFilter={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}

View File

@ -1,26 +1,9 @@
import { Container } from 'react-bootstrap' import { Link } from 'react-router-dom'
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() {
const user = useUser() /* TODO */
return ( return <h1>For Yet Go <Link to='/'>Home</Link></h1>
<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

View File

@ -1,6 +1,4 @@
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]
@ -12,31 +10,10 @@ 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 => [fName, filters[fName]?.toString()]
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, URLDecoreFilters } export { defaultFilters, filterNames, URLEncodeFilters }

View File

@ -60,18 +60,8 @@ 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, isInt, fallbackToUndefined } export { isRecord, isObject, isConst, isLiteralUnion, isArrayOf, isString }

1
migrations/README Normal file
View File

@ -0,0 +1 @@
Generic single-database configuration.

85
migrations/env.py Normal file
View 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
View 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"}

View 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 ###

View 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 ###

View File

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