Compare commits

..

No commits in common. "010500e1243fb5d3d70e3e54851f07949a878ea2" and "9835954647a90bf9850fd181e045c2a6299773f1" have entirely different histories.

37 changed files with 233 additions and 1011 deletions

View File

@ -26,7 +26,6 @@ dist-ssr
.venv .venv
*.db *.db
uploads/
.env .env
__pycache__ __pycache__

1
.gitignore vendored
View File

@ -26,7 +26,6 @@ dist-ssr
.venv .venv
*.db *.db
uploads/
.env .env
__pycache__ __pycache__

View File

@ -6,9 +6,9 @@ COPY front .
RUN npm run build RUN npm run build
FROM python:3-slim FROM python:3-slim
WORKDIR /srv WORKDIR /app
COPY requirements.txt . COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade -r requirements.txt RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY ./back ./back COPY ./app ./app
COPY --from=builder /src/dist ./front/dist COPY --from=builder /src/dist ./front/dist
CMD uvicorn back.main:app --host 0.0.0.0 --port 80 CMD uvicorn app.main:app --host 0.0.0.0 --port 80

View File

@ -1,12 +1,6 @@
# Porridger # Porridger
Food and other stuff sharing platform. The service was developed during Digital Students hackathon by "Полка Billy" team. Food and other stuff sharing platform
Members:
* Dmitry Gantimurov - Backend
* Dmitriy Shishkov - Frontend
* Vladimir Yakovlev - Backend & Design
## Dev build instructions ## Dev build instructions
@ -27,7 +21,7 @@ Backend:
```sh ```sh
pip install -r requirements.txt pip install -r requirements.txt
uvicorn back.main:app --reload uvicorn app.main:app --reload
``` ```
## Deploy instructions ## Deploy instructions
@ -37,5 +31,5 @@ Only docker/podman are required
```sh ```sh
docker build . -t porridger:build docker build . -t porridger:build
docker run --name porridger -p 8080:80 -v ./sql_app.db:/srv/sql_app.db -v uploads:/srv/uploads porridger:build docker run --name porridger -p 8080:80 -v ./sql_app.db:/app/sql_app.db porridger:build
``` ```

View File

@ -1,51 +1,112 @@
#подключение библиотек #подключение библиотек
from fastapi import FastAPI, Response, Path, Depends, Body, Form, Query, status, HTTPException, APIRouter, UploadFile, File from fastapi import FastAPI, Response, Path, Depends, Body, Query, status, HTTPException, APIRouter
from fastapi.responses import HTMLResponse, FileResponse, JSONResponse, RedirectResponse from fastapi.responses import HTMLResponse, FileResponse, JSONResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from fastapi.requests import Request from fastapi.requests import Request
from fastapi.middleware.cors import CORSMiddleware from sqlalchemy import create_engine
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from pydantic import json from pydantic import json
from starlette.staticfiles import StaticFiles from starlette.staticfiles import StaticFiles
from app.utils import *
import requests import requests
from uuid import uuid4 from uuid import uuid4
import ast SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
import pathlib engine = create_engine(
import shutil SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
import os )
from .utils import * Base = declarative_base()
from .models import Announcement, Trashbox, UserDatabase, Base
from .db import engine, SessionLocal
from . import schema class UserDatabase(Base):#класс пользователя
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)#айди пользователя
phone = Column(Integer, nullable=True)#номер телефона пользователя
email = Column(String)#электронная почта пользователя
password = Column(String) # пароль
hashed_password = Column(String)
name = Column(String, nullable=True)#имя пользователя
surname = Column(String)#фамилия пользователя
class Announcement(Base): #класс объявления
__tablename__ = "announcements"
id = Column(Integer, primary_key=True, index=True)#айди объявления
user_id = Column(Integer)#айди создателя объявления
name = Column(String) # название объявления
category = Column(String)#категория продукта из объявления
best_by = Column(Integer)#срок годности продукта из объявления
adress = Column(String)
longtitude = Column(Integer)
latitude = Column(Integer)
description = Column(String)#описание продукта в объявлении
src = Column(String, nullable=True) #изображение продукта в объявлении
metro = Column(String)#ближайщее метро от адреса нахождения продукта
trashId = Column(Integer, nullable=True)
booked_by = Column(Integer)#статус бронирования (либо -1, либо айди бронирующего)
class Trashbox(Base):#класс мусорных баков
__tablename__ = "trashboxes"
id = Column(Integer, primary_key=True, index=True)#айди
name = Column(String, nullable=True)#имя пользователя
adress = Column(String)
latitude = Column(Integer)
longtitude = Column(Integer)
category = Column(String)#категория продукта из объявления
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
SessionLocal = sessionmaker(autoflush=True, bind=engine)
db = SessionLocal() db = SessionLocal()
# Пробный чувак
tom = UserDatabase(name="Tom", phone="89999999", email="pupka", password="1234", surname="Smith")
# db.add(tom) # добавляем в бд
# db.commit() # сохраняем изменения
# db.refresh(tom) # обновляем состояние объекта
# Пробное объявление 1
a1 = Announcement(user_id=1, category="cat", best_by="201223", adress="abd", longtitude=23, latitude=22,
description="abv", src="111", metro="Lesnaya", booked_by=2)
# Пробное объявление 2
a2 = Announcement(user_id=1, category="dog", best_by="221223", adress="abd", longtitude=50, latitude=12,
description="vvv", src="110", metro="Petrogradskaya", booked_by=2)
a3 = Announcement(user_id=1, category="a", best_by="221223", adress="abd", longtitude=20, latitude=25,
description="vvv", src="101", metro="metro", booked_by=2)
trash1 = Trashbox(name="Tom", adress="abd", longtitude=23, latitude=22, category="indisposable")
# db.add(a1) # добавляем в бд
# db.add(a2) # добавляем в бд
# db.add(a3) # добавляем в бд
# db.add(trash1) # добавляем в бд
# db.commit() # сохраняем изменения
# db.refresh(a1) # обновляем состояние объекта
# db.refresh(a2) # обновляем состояние объекта
# db.refresh(a3) # обновляем состояние объекта
# db.refresh(trash1) # обновляем состояние объекта
# # Удалить все
# db.query(User).delete()
# db.query(Announcement).delete()
# db.commit()
# Непосредственно преложение
app = FastAPI() app = FastAPI()
# CORS fix for development
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173", "http://localhost:8000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
templates = Jinja2Templates(directory="./front/dist") templates = Jinja2Templates(directory="./front/dist")
app.mount("/static", StaticFiles(directory = "./front/dist")) app.mount("/static", StaticFiles(directory = "./front/dist"))
app.mount("/uploads", StaticFiles(directory = "./uploads"))
@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 = -1):
@ -96,29 +157,15 @@ def single_annoncement(user_id:int):
# Занести объявление в базу # Занести объявление в базу
@app.put("/api/announcement")#адрес объявлений @app.put("/api/announcement")#адрес объявлений
def put_in_db(name: Annotated[str, Form()], category: Annotated[str, Form()], bestBy: Annotated[int, Form()], address: Annotated[str, Form()], longtitude: Annotated[float, Form()], latitude: Annotated[float, Form()], description: Annotated[str, Form()], src: Annotated[UploadFile | None, File()], metro: Annotated[str, Form()], trashId: Annotated[int | None, Form()] = -1): def put_in_db(data = Body()):
# try: try:
userId = 1 # temporary temp_ancmt = Announcement(data.id, data.userId, data.name, data.category, data.bestBy, data.adress, data.longtitude, data.latitude, data.description, data.src, data.metro, data.trashId, data.bookedBy)
uploaded_name = ""
f = src.file
f.seek(0, os.SEEK_END)
if f.tell() > 0:
f.seek(0)
destination = pathlib.Path("./uploads/" + str(hash(src.file)) + pathlib.Path(src.filename).suffix.lower())
with destination.open('wb') as buffer:
shutil.copyfileobj(src.file, buffer)
uploaded_name = "/uploads/"+destination.name
temp_ancmt = Announcement(user_id=userId, name=name, category=category, best_by=bestBy, adress=address, longtitude=longtitude, latitude=latitude, description=description, src=uploaded_name, metro=metro, trashId=trashId)
db.add(temp_ancmt) # добавляем в бд db.add(temp_ancmt) # добавляем в бд
db.commit() # сохраняем изменения db.commit() # сохраняем изменения
db.refresh(temp_ancmt) # обновляем состояние объекта db.refresh(temp_ancmt) # обновляем состояние объекта
return {"Answer" : True} return {"Answer" : True}
# except: except:
# return {"Answer" : False} return {"Answer" : False}
# Удалить объявления из базы # Удалить объявления из базы
@ -133,19 +180,15 @@ def delete_from_db(data = Body()):#функция удаления объект
# Забронировать объявление # Забронировать объявление
@app.post("/api/book") @app.put("/api/book")
def change_book_status(data: schema.Book): def change_book_status(data = Body()):
try: # Получаем id пользователя, который бронирует объявление
# Получаем id пользователя, который бронирует объявление temp_user_id = 1
temp_user_id = 1 # Находим объявление по данному id
# Находим объявление по данному id announcement_to_change = db.query(Announcement).filter(user_id == temp_user_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:
return {"Success": False}
@app.post("/api/signup") @app.post("/api/signup")
def create_user(data = Body()): def create_user(data = Body()):
@ -198,30 +241,29 @@ def get_trashboxes(lat:float, lng:float):#крутая функция для р
head = {'Authorization': 'Bearer {}'.format(my_token)} head = {'Authorization': 'Bearer {}'.format(my_token)}
my_data={ my_data={
'x' : f"{lng}", 'x' : f"{lat}",
'y' : f"{lat}", 'y' : f"{lng}",
'limit' : '1' 'limit' : '1'
} }
response = requests.post(f"{BASE_URL}/nearest_recycling/get", headers=head, data=my_data) response = requests.post(f"{BASE_URL}/nearest_recycling/get", headers=head, data=my_data)
infos = response.json() infos = response.json()
trashboxes = [] trashboxes = []
for trashbox in infos["results"]: for trashbox in infos["results"]:
temp_dict = {} temp_dict = {}
temp_dict["Category"] = trashbox["Category"]
for obj in trashbox["Objects"]: for obj in trashbox["Objects"]:
coord_list = obj["geometry"] coord_list = obj["geometry"]
temp_dict["Lat"] = coord_list["coordinates"][1] temp_dict["Lat"] = coord_list["coordinates"][0]
temp_dict["Lng"] = coord_list["coordinates"][0] temp_dict["Lng"] = coord_list["coordinates"][1]
properties = obj["properties"] properties = obj["properties"]
temp_dict["Name"] = properties["title"] temp_dict["Name"] = properties["title"]
temp_dict["Address"] = properties["address"] temp_dict["Adress"] = properties["address"]
temp_dict["Categories"] = properties["content_text"].split(',')
trashboxes.append(temp_dict) trashboxes.append(temp_dict)
return JSONResponse(trashboxes)
uniq_trashboxes = [ast.literal_eval(el1) for el1 in set([str(el2) for el2 in trashboxes])]
return JSONResponse(uniq_trashboxes)
@app.get("/{rest_of_path:path}") @app.get("/{rest_of_path:path}")
async def react_app(req: Request, rest_of_path: str): async def react_app(req: Request, rest_of_path: str):

View File

@ -1,13 +0,0 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autoflush=True, bind=engine)
Base = declarative_base()

View File

@ -1,40 +0,0 @@
from .db import engine
from .models import Announcement, UserDatabase, Trashbox, Base
Base.metadata.create_all(bind=engine)
db = SessionLocal()
# Пробный чувак
tom = UserDatabase(name="Tom", phone="89999999", email="pupka", password="1234", surname="Smith")
# db.add(tom) # добавляем в бд
# db.commit() # сохраняем изменения
# db.refresh(tom) # обновляем состояние объекта
# Пробное объявление 1
a1 = Announcement(user_id=1, category="cat", best_by="201223", adress="abd", longtitude=23, latitude=22,
description="abv", src="111", metro="Lesnaya", booked_by=2)
# Пробное объявление 2
a2 = Announcement(user_id=1, category="dog", best_by="221223", adress="abd", longtitude=50, latitude=12,
description="vvv", src="110", metro="Petrogradskaya", booked_by=2)
a3 = Announcement(user_id=1, category="a", best_by="221223", adress="abd", longtitude=20, latitude=25,
description="vvv", src="101", metro="metro", booked_by=2)
trash1 = Trashbox(name="Tom", adress="abd", longtitude=23, latitude=22, category="indisposable")
# db.add(a1) # добавляем в бд
# db.add(a2) # добавляем в бд
# db.add(a3) # добавляем в бд
# db.add(trash1) # добавляем в бд
# db.commit() # сохраняем изменения
# db.refresh(a1) # обновляем состояние объекта
# db.refresh(a2) # обновляем состояние объекта
# db.refresh(a3) # обновляем состояние объекта
# db.refresh(trash1) # обновляем состояние объекта
# # Удалить все
# db.query(User).delete()
# db.query(Announcement).delete()
# db.commit()

View File

@ -1,45 +0,0 @@
from sqlalchemy import Column, Integer, String
from .db import Base
class UserDatabase(Base):#класс пользователя
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)#айди пользователя
phone = Column(Integer, nullable=True)#номер телефона пользователя
email = Column(String)#электронная почта пользователя
password = Column(String) # пароль
hashed_password = Column(String)
name = Column(String, nullable=True)#имя пользователя
surname = Column(String)#фамилия пользователя
class Announcement(Base): #класс объявления
__tablename__ = "announcements"
id = Column(Integer, primary_key=True, index=True)#айди объявления
user_id = Column(Integer)#айди создателя объявления
name = Column(String) # название объявления
category = Column(String)#категория продукта из объявления
best_by = Column(Integer)#срок годности продукта из объявления
adress = Column(String)
longtitude = Column(Integer)
latitude = Column(Integer)
description = Column(String)#описание продукта в объявлении
src = Column(String, nullable=True) #изображение продукта в объявлении
metro = Column(String)#ближайщее метро от адреса нахождения продукта
trashId = Column(Integer, nullable=True)
booked_by = Column(Integer)#статус бронирования (либо -1, либо айди бронирующего)
class Trashbox(Base):#класс мусорных баков
__tablename__ = "trashboxes"
id = Column(Integer, primary_key=True, index=True)#айди
name = Column(String, nullable=True)#имя пользователя
adress = Column(String)
latitude = Column(Integer)
longtitude = Column(Integer)
category = Column(String)#категория продукта из объявления

View File

@ -1,5 +0,0 @@
from pydantic import BaseModel
class Book(BaseModel):
id: int

View File

@ -12,7 +12,6 @@
"leaflet": "^1.9.3", "leaflet": "^1.9.3",
"react": "^18.2.0", "react": "^18.2.0",
"react-bootstrap": "^2.7.4", "react-bootstrap": "^2.7.4",
"react-bootstrap-typeahead": "^6.1.2",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-insta-stories": "^2.5.9", "react-insta-stories": "^2.5.9",
"react-leaflet": "^4.2.1", "react-leaflet": "^4.2.1",
@ -1341,11 +1340,6 @@
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true "dev": true
}, },
"node_modules/compute-scroll-into-view": {
"version": "1.0.20",
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
"integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg=="
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -1900,7 +1894,8 @@
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
}, },
"node_modules/fast-json-stable-stringify": { "node_modules/fast-json-stable-stringify": {
"version": "2.1.0", "version": "2.1.0",
@ -2640,11 +2635,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
},
"node_modules/lodash.merge": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -3059,29 +3049,6 @@
} }
} }
}, },
"node_modules/react-bootstrap-typeahead": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/react-bootstrap-typeahead/-/react-bootstrap-typeahead-6.1.2.tgz",
"integrity": "sha512-waIWRQ4CUZld69iL+EFiuL/2B+N4LecaAKcRTMQey0NDOM7Sxmtl+iELFzGltt2/DK6yvrxEUCbZI8pTztPFXA==",
"dependencies": {
"@babel/runtime": "^7.14.6",
"@popperjs/core": "^2.10.2",
"@restart/hooks": "^0.4.0",
"classnames": "^2.2.0",
"fast-deep-equal": "^3.1.1",
"invariant": "^2.2.1",
"lodash.debounce": "^4.0.8",
"prop-types": "^15.5.8",
"react-overlays": "^5.2.0",
"react-popper": "^2.2.5",
"scroll-into-view-if-needed": "^2.2.20",
"warning": "^4.0.1"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "18.2.0", "version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
@ -3094,11 +3061,6 @@
"react": "^18.2.0" "react": "^18.2.0"
} }
}, },
"node_modules/react-fast-compare": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.1.tgz",
"integrity": "sha512-xTYf9zFim2pEif/Fw16dBiXpe0hoy5PxcD8+OwBnTtNLfIm3g6WxhKNurY+6OmdH1u6Ta/W/Vl6vjbYP1MFnDg=="
},
"node_modules/react-insta-stories": { "node_modules/react-insta-stories": {
"version": "2.5.9", "version": "2.5.9",
"resolved": "https://registry.npmjs.org/react-insta-stories/-/react-insta-stories-2.5.9.tgz", "resolved": "https://registry.npmjs.org/react-insta-stories/-/react-insta-stories-2.5.9.tgz",
@ -3130,39 +3092,6 @@
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
}, },
"node_modules/react-overlays": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.2.1.tgz",
"integrity": "sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==",
"dependencies": {
"@babel/runtime": "^7.13.8",
"@popperjs/core": "^2.11.6",
"@restart/hooks": "^0.4.7",
"@types/warning": "^3.0.0",
"dom-helpers": "^5.2.0",
"prop-types": "^15.7.2",
"uncontrollable": "^7.2.1",
"warning": "^4.0.3"
},
"peerDependencies": {
"react": ">=16.3.0",
"react-dom": ">=16.3.0"
}
},
"node_modules/react-popper": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz",
"integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==",
"dependencies": {
"react-fast-compare": "^3.0.1",
"warning": "^4.0.2"
},
"peerDependencies": {
"@popperjs/core": "^2.0.0",
"react": "^16.8.0 || ^17 || ^18",
"react-dom": "^16.8.0 || ^17 || ^18"
}
},
"node_modules/react-refresh": { "node_modules/react-refresh": {
"version": "0.14.0", "version": "0.14.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
@ -3351,14 +3280,6 @@
"loose-envify": "^1.1.0" "loose-envify": "^1.1.0"
} }
}, },
"node_modules/scroll-into-view-if-needed": {
"version": "2.2.31",
"resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
"integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==",
"dependencies": {
"compute-scroll-into-view": "^1.0.20"
}
},
"node_modules/semver": { "node_modules/semver": {
"version": "6.3.0", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",

View File

@ -4,7 +4,7 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host 0.0.0.0", "dev": "vite",
"build": "vite build", "build": "vite build",
"lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview" "preview": "vite preview"
@ -14,7 +14,6 @@
"leaflet": "^1.9.3", "leaflet": "^1.9.3",
"react": "^18.2.0", "react": "^18.2.0",
"react-bootstrap": "^2.7.4", "react-bootstrap": "^2.7.4",
"react-bootstrap-typeahead": "^6.1.2",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-insta-stories": "^2.5.9", "react-insta-stories": "^2.5.9",
"react-leaflet": "^4.2.1", "react-leaflet": "^4.2.1",

View File

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

View File

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

View File

@ -1,95 +1,6 @@
const metros = { const metros = [
red: [ "Петроградская",
"Девяткино", "Горьковская"
"Гражданский проспект", ]
"Академическая",
"Политехническая",
"Площадь Мужества",
"Лесная",
"Выборгская",
"Площадь Ленина",
"Чернышевская",
"Площадь Восстания",
"Владимирская",
"Пушкинская",
"Технологический институт",
"Балтийская",
"Нарвская",
"Кировский завод",
"Автово",
"Ленинский проспект",
"Проспект Ветеранов"
],
blue: [
"Парнас",
"Проспект Просвещения",
"Озерки",
"Удельная",
"Пионерская",
"Чёрная речка",
"Петроградская",
"Горьковская",
"Невский проспект",
"Сенная площадь",
"Технологический институт",
"Фрунзенская",
"Московские ворота",
"Электросила",
"Парк Победы",
"Московская",
"Звёздная",
"Купчино"
],
green: [
"Приморская",
"Беговая",
"Василеостровская",
"Гостиный двор",
"Маяковская",
"Площадь Александра Невского",
"Елизаровская",
"Ломоносовская",
"Пролетарская",
"Обухово",
"Рыбацкое"
],
orange: [
"Спасская",
"Горный институт",
"Театральная",
"Достоевская",
"Лиговский проспект",
"Площадь Александра Невского",
"Новочеркасская",
"Ладожская",
"Проспект Большевиков",
"Улица Дыбенко"
],
violet: [
"Комендантский проспект",
"Старая Деревня",
"Крестовский остров",
"Чкаловская",
"Спортивная",
"Адмиралтейская",
"Садовая",
"Звенигородская",
"Обводный канал",
"Волковская",
"Бухарестская",
"Международная",
"Проспект славы",
"Дунайскай",
"Шушары"
],
gold: [
"Юго западная",
"Каретная",
"Путиловская",
"Броневая",
"Заставская",
"Боровая"
]
}
export { metros } export { metros }

View File

@ -1,13 +0,0 @@
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
<svg width="44" height="44" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg" stroke="#fff">
<g fill="none" fill-rule="evenodd" stroke-width="2">
<circle cx="22" cy="22" r="1">
<animate attributeName="r" begin="0s" dur="1.8s" values="1; 20" calcMode="spline" keyTimes="0; 1" keySplines="0.165, 0.84, 0.44, 1" repeatCount="indefinite"/>
<animate attributeName="stroke-opacity" begin="0s" dur="1.8s" values="1; 0" calcMode="spline" keyTimes="0; 1" keySplines="0.3, 0.61, 0.355, 1" repeatCount="indefinite"/>
</circle>
<circle cx="22" cy="22" r="1">
<animate attributeName="r" begin="-0.9s" dur="1.8s" values="1; 20" calcMode="spline" keyTimes="0; 1" keySplines="0.165, 0.84, 0.44, 1" repeatCount="indefinite"/>
<animate attributeName="stroke-opacity" begin="-0.9s" dur="1.8s" values="1; 0" calcMode="spline" keyTimes="0; 1" keySplines="0.3, 0.61, 0.355, 1" repeatCount="indefinite"/>
</circle>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,18 +1,19 @@
import { Modal, Button } from 'react-bootstrap' import Modal from 'react-bootstrap/Modal'
import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet'
import { categoryNames } from '../assets/category' import { categoryNames } from '../assets/category'
import { useBook } from '../hooks/api' import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet'
import { Button } from 'react-bootstrap'
import useBook from '../utils/useBook'
function AnnouncementDetails({ close, announcement: { id, name, category, bestBy, description, lat, lng, address, metro } }) { function AnnouncementDetails({ close, announcement: { id, name, category, bestBy, description, lat, lng, address, metro } }) {
const {handleBook, status: bookStatus} = useBook(id) const handleBook = useBook(id)
return ( return (
<div <div
className="modal show" className="modal show"
style={{ display: 'flex', position: 'initial', alignItems: "center" }} style={{ display: 'flex', position: 'initial', alignItems: "center" }}
> >
<Modal.Dialog style={{minWidth: "50vw"}}> <Modal.Dialog>
<Modal.Header closeButton onHide={close}> <Modal.Header closeButton onHide={close}>
<Modal.Title> <Modal.Title>
Подробнее Подробнее
@ -23,10 +24,10 @@ function AnnouncementDetails({ close, announcement: { id, name, category, bestBy
<h1>{name}</h1> <h1>{name}</h1>
<span>{categoryNames.get(category)}</span> <span>{categoryNames.get(category)}</span>
<span className='m-2'>&#x2022;</span>{/* dot */} <span className='m-2'>&#x2022;</span> {/* dot */}
<span>Годен до {new Date(bestBy).toLocaleString('ru-RU')}</span> <span>Годен до {new Date(bestBy).toLocaleString('ru-RU')}</span>
<p className='mb-3'>{description}</p> <p className='mb-2'>{description}</p>
<MapContainer style={{ width: "100%", minHeight: 300 }} center={[lat, lng]} zoom={16} > <MapContainer style={{ width: "100%", minHeight: 300 }} center={[lat, lng]} zoom={16} >
<TileLayer <TileLayer
@ -42,7 +43,7 @@ function AnnouncementDetails({ close, announcement: { id, name, category, bestBy
<Modal.Footer> <Modal.Footer>
<Button variant='success' onClick={handleBook}> <Button variant='success' onClick={handleBook}>
{bookStatus || "Забронировать"} Забронировать
</Button> </Button>
</Modal.Footer> </Modal.Footer>
</Modal.Dialog> </Modal.Dialog>

View File

@ -27,7 +27,7 @@ function Filters({ filter, setFilter, filterShown, setFilterShown }) {
Фильтрация Фильтрация
</Modal.Title> </Modal.Title>
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
<Form.Group className="mb-3" controlId="categoryFilter"> <Form.Group className="mb-3" controlId="categoryFilter">
@ -55,11 +55,9 @@ function Filters({ filter, setFilter, filterShown, setFilterShown }) {
<option value=""> <option value="">
Выберите станцию метро Выберите станцию метро
</option> </option>
{Object.entries(metros).map( {metros.map(
([branch, stations]) => (metro) =>
stations.map(metro => <option key={metro} value={metro}>{metro}</option>
<option key={metro} value={metro}>{metro}</option>
)
)} )}
</Form.Select> </Form.Select>
</Form.Group> </Form.Group>

View File

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

View File

@ -1,26 +0,0 @@
import { Marker, Popup } from "react-leaflet"
const TrashboxMarkers = ({ trashboxes, selectTrashbox }) => {
return (
<>{trashboxes.map((trashbox, index) => (
<Marker key={trashbox.Lat + "" + trashbox.Lng} position={[trashbox.Lat, trashbox.Lng]}>
<Popup>
<p>{trashbox.Address}</p>
<p>Тип мусора: <>
{trashbox.Categories.map((category, j) =>
<span key={trashbox.Address + category}>
<a href="#" onClick={() => selectTrashbox({ index, category })}>
{category}
</a>
{(j < trashbox.Categories.length - 1) ? ', ' : ''}
</span>
)}
</></p>
<p>{trashbox.Lat.toFixed(4)}, {trashbox.Lng.toFixed(4)}</p>
</Popup>
</Marker>
))}</>
)
}
export default TrashboxMarkers

View File

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

View File

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

View File

@ -1,31 +0,0 @@
import { useState } from "react"
import { API_URL } from "../../config"
const useAddAnnouncement = () => {
const [status, setStatus] = useState("Опубликовать")
const doAdd = async (formData) => {
setStatus(true)
try {
const res = await fetch(API_URL + "/announcement", {
method: 'PUT',
body: formData,
})
const data = await res.json()
if (!data.Answer) {
throw new Error("Не удалось опубликовать объявление")
}
setStatus("Опубликовано")
} catch (err) {
setStatus(err.message ?? err)
setTimeout(() => setStatus("Опубликовать"), 10000)
}
}
return {doAdd, status}
}
export default useAddAnnouncement

View File

@ -1,61 +0,0 @@
import { useState } from "react"
import { API_URL } from "../../config"
function useAuth() {
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const doAuth = async (data, newAccount) => {
setLoading(true)
if (newAccount) {
try {
const res = await fetch(API_URL + "/signup", {
method: "POST",
body: data,
headers: {
'Content-Type': 'application/json'
}
})
const signupData = await res.json()
if (signupData.Success === false) {
throw new Error(signupData.Message)
}
} catch (err) {
setError(err.message)
setLoading(false)
return null
}
}
try {
const res = fetch(API_URL + '/auth/token' + new URLSearchParams({
username: data.email,
password: data.password
}), {
method: "POST",
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
const loginData = await res.json()
const token = loginData.access_token
setError('')
setLoading(false)
return token
} catch (err) {
setError(err.message)
setLoading(false)
return null
}
}
return { doAuth, loading, error }
}
export default useAuth

View File

@ -1,42 +0,0 @@
import { useState } from "react"
import { useNavigate } from "react-router-dom"
import { getToken } from "../../utils/auth"
import { API_URL } from "../../config"
function useBook(id) {
const navigate = useNavigate()
const [status, setStatus] = useState('')
const handleBook = () => {
const token = getToken() || "Test"
if (token) {
setStatus("Загрузка")
fetch(API_URL + '/book', {
method: 'POST',
body: JSON.stringify({
id: id
}),
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
}
}).then(res => res.json()).then(data => {
if (data.Success === true) {
setStatus('Забронировано')
} else {
setStatus("Ошибка бронирования")
}
})
} else {
return navigate("/login")
}
}
return { handleBook, status }
}
export default useBook

View File

@ -1,41 +0,0 @@
import { useEffect, useState } from "react"
const useFetch = (url, params, initialData) => {
const [data, setData] = useState(initialData)
const [loading, setLoading] = useState(true)
const [error, setError] = useState("")
useEffect(() => {
fetch(url, params)
.then(res => {
if (!res.ok) {
switch (res.status) {
case 401: {
throw new Error("Ошибка авторизации")
}
case 404: {
new Error("Объект не найден")
}
break
default: {
throw new Error("Ошибка ответа от сервера")
}
}
}
return res.json()
})
.then(data => {
setData(data)
setLoading(false)
})
.catch(err => {
setError("Ошибка сети")
setLoading(false)
})
}, [url, params])
return { data, loading, error }
}
export default useFetch

View File

@ -1,26 +0,0 @@
import useFetch from './useFetch'
import { API_URL } from '../../config'
import { removeNull } from '../../utils'
const initialAnnouncements = { list_of_announcements: [], Success: true }
const useHomeAnnouncementList = (filters) => {
const { data, loading, error } = useFetch(
API_URL + '/announcements?' + new URLSearchParams(removeNull(filters)),
null,
initialAnnouncements
)
const annList = data.list_of_announcements
const res = annList.map(ann => ({
...ann,
lat: ann.latitude,
lng: ann.longtitude,
bestBy: ann.best_by
}))
return { data: error ? [] : res, loading, error }
}
export default useHomeAnnouncementList

View File

@ -1,12 +0,0 @@
import { API_URL } from "../../config"
import useFetch from "./useFetch"
const useTrashboxes = (position) => {
return useFetch(
API_URL + "/trashbox?" + new URLSearchParams({ lat: position.lat, lng: position.lng }),
undefined,
[]
)
}
export default useTrashboxes

View File

@ -1,199 +1,8 @@
import { useEffect, useState } from "react"
import { Form, Button, Card } from "react-bootstrap"
import { MapContainer, TileLayer } from 'react-leaflet'
import { categoryNames } from "../assets/category"
import { latLng } from "leaflet"
import { metros } from "../assets/metro"
import LocationMarker from "../components/LocationMarker"
import TrashboxMarkers from "../components/TrashboxMarkers"
import { useAddAnnouncement, useTrashboxes } from "../hooks/api"
function AddPage() { function AddPage() {
const [addressPosition, setAddressPosition] = useState(latLng(59.972, 30.3227))
const [address, setAddress] = useState('')
const { data: trashboxes, trashboxes_loading, trashboxes_error } = useTrashboxes(addressPosition) // TODO
const [selectedTrashbox, setSelectedTrashbox] = useState({ index: -1, category: '' })
useEffect(() => { return <></>
(async () => {
try {
const res = await fetch(location.protocol + "//nominatim.openstreetmap.org/search?format=json&q=" + address)
const fetchData = await res.json()
console.log("f", fetchData)
} catch (err) {
console.error(err)
}
})()
}, [address])
useEffect(() => {
(async () => {
try {
const res = await fetch(location.protocol + "//nominatim.openstreetmap.org/reverse?format=json&accept-language=ru&lat=" + addressPosition.lat + "&lon=" + addressPosition.lng)
const fetchData = await res.json()
setAddress(fetchData.display_name)
} catch (err) {
console.error(err)
}
})()
}, [addressPosition])
const {doAdd, status} = useAddAnnouncement()
const handleSubmit = (event) => {
event.preventDefault()
event.stopPropagation()
const formData = new FormData(event.target)
formData.append("latitude", addressPosition.lat)
formData.append("longtitude", addressPosition.lng)
formData.append("address", address)
formData.set("bestBy", new Date(formData.get("bestBy")).getTime())
doAdd(formData)
}
return (
<Card className="m-4" style={{ height: 'calc(100vh - 3rem)' }}>
<Card.Body style={{ overflowY: "auto" }} >
<Form onSubmit={handleSubmit}>
<Form.Group className="mb-3" controlId="name">
<Form.Label>Заголовок объявления</Form.Label>
<Form.Control type="text" required name="name" />
</Form.Group>
<Form.Group className="mb-3" controlId="category">
<Form.Label>Категория</Form.Label>
<Form.Select required name="category">
<option value="" hidden>
Выберите категорию
</option>
{Array.from(categoryNames).map(
([category, categoryName]) =>
<option key={category} value={category}>{categoryName}</option>
)}
</Form.Select>
</Form.Group>
<Form.Group className="mb-3" controlId="bestBy">
<Form.Label>Срок годности</Form.Label>
<Form.Control type="date" required name="bestBy" />
</Form.Group>
<Form.Group className="mb-3" controlId="address">
<Form.Label>Адрес выдачи</Form.Label>
<div className="mb-3">
<MapContainer
scrollWheelZoom={false}
style={{ width: "100%", height: 400 }}
center={addressPosition}
zoom={13}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<LocationMarker
address={address}
position={addressPosition}
setPosition={setAddressPosition}
/>
</MapContainer>
</div>
<p>Адрес: {address}</p>
</Form.Group>
<Form.Group className="mb-3" controlId="description">
<Form.Label>Описание</Form.Label>
<Form.Control as="textarea" name="description" rows={3} placeholder="Укажите свои контакты, а так же, когда вам будет удобно передать продукт" />
</Form.Group>
<Form.Group className="mb-3" controlId="src">
<Form.Label>Иллюстрация (фото или видео)</Form.Label>
<Form.Control
type="file"
name="src"
accept="video/mp4,video/mkv, video/x-m4v,video/*, image/*"
capture="environment"
/>
</Form.Group>
<Form.Group className="mb-3" controlId="metro">
<Form.Label>
Станция метро
</Form.Label>
<Form.Select name="metro">
<option value="">
Укажите ближайщую станцию метро
</option>
{Object.entries(metros).map(
([branch, stations]) =>
stations.map(metro =>
<option key={metro} value={metro}>{metro}</option>
)
)}
</Form.Select>
</Form.Group>
<Form.Group className="mb-3" controlId="password">
<Form.Label>Пункт сбора мусора</Form.Label>
<div className="mb-3">
{trashboxes_loading
? (
<div style={{ height: 400 }}>
<p>Загрузка</p>
</div>
) : (
trashboxes_error ? (
<p
style={{ height: 400 }}
className="text-danger"
>{trashboxes_error}</p>
) : (
<MapContainer
scrollWheelZoom={false}
style={{ width: "100%", height: 400 }}
center={addressPosition}
zoom={13}
className=""
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<TrashboxMarkers
trashboxes={trashboxes}
selectTrashbox={setSelectedTrashbox}
/>
</MapContainer>
)
)
}
</div>
{selectedTrashbox.index > -1 ? (
<p>Выбран пункт сбора мусора на {
trashboxes[selectedTrashbox.index].Address
} с категорией {selectedTrashbox.category}</p>
) : (
<p>Выберите пунк сбора мусора и категорию</p>
)}
</Form.Group>
<Button variant="success" type="submit">
{status}
</Button>
</Form>
</Card.Body>
</Card>
)
} }
export default AddPage export default AddPage

View File

@ -1,15 +1,18 @@
import { useEffect, useState } from 'react'
import Stories from 'react-insta-stories' import Stories from 'react-insta-stories'
import { useEffect, useState } from 'react'
import BottomNavBar from '../components/BottomNavBar' import BottomNavBar from '../components/BottomNavBar'
import useStoryDimensions from '../hooks/useStoryDimensions' import useStoryDimensions from '../utils/useStoryDimensions'
import { API_URL } from '../config'
import "./leafletStyles.css"
import 'leaflet/dist/leaflet.css'
import AnnouncementDetails from '../components/AnnouncementDetails' import AnnouncementDetails from '../components/AnnouncementDetails'
import Filters from '../components/Filters'
import { useHomeAnnouncementList } from '../hooks/api'
import puffSpinner from '../assets/puff.svg'
import { categoryGraphics } from '../assets/category' import { categoryGraphics } from '../assets/category'
import Filters from '../components/Filters'
import { removeNull } from '../utils'
function generateStories(announcements) { function generateStories(announcements) {
return announcements.map(announcement => { return announcements.map(announcement => {
@ -22,68 +25,76 @@ function generateStories(announcements) {
}) })
} }
function fallbackGenerateStories(announcementsFetch) { const mock = [
const stories = generateStories(announcementsFetch.data) {
id: 5,
if (announcementsFetch.loading) name: "Огурец",
return fallbackStory() category: "fruits_vegatables",
src: "https://upload.wikimedia.org/wikipedia/commons/thumb/2/24/Marketvegetables.jpg/800px-Marketvegetables.jpg",
if (announcementsFetch.error) bestBy: 10000,
return fallbackStory(announcementsFetch.error) description: "Очень вкусный огурец, прям, закачаешься",
lat: 59.9724,
if (stories.length == 0) lng: 30.3227,
return fallbackStory("Здесь пока пусто") address: "ул. Профессора Попова, дом 5 литера Ф",
metro: "Петроградская"
return stories
}
const fallbackStory = (text) => [{
content: ({ action }) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => { action('pause') }, [action])
return (
<div style={{ margin: 'auto' }}>
{text || <img src={puffSpinner} />}
</div>
)
}, },
header: { heading: text } {
}] id: 9,
name: "Арбуз",
category: "soup",
src: "/static/watermelon.mp4",
bestBy: 20000,
description: "Очень вкусный арбуз, прям, закачаешься",
lat: 60.9724,
lng: 30.3227,
address: "ул. Профессора Попова, дом 50 литера Ф",
metro: "Горьковская"
}
]
const defaultFilters = { userId: null, category: null, metro: null, bookedBy: null } const defaultFilters = { userId: null, category: null, metro: null, bookedBy: null }
const PROD = true
function HomePage() { function HomePage() {
const { height, width } = useStoryDimensions(16 / 10) const { height, width } = useStoryDimensions(16 / 10)
const [announcements, setAnnouncements] = useState([])
const [filterShown, setFilterShown] = useState(false) const [filterShown, setFilterShown] = useState(false)
const [filter, setFilter] = useState(defaultFilters) const [filter, setFilter] = useState(defaultFilters)
const announcementsFetch = useHomeAnnouncementList(filter) useEffect(() => {
(async () => {
try {
const res = PROD ? await fetch(API_URL + "/announcements?" + new URLSearchParams(removeNull(filter))) : null
const stories = fallbackGenerateStories(announcementsFetch) const json = PROD ? (await res.json()).list_of_announcements : mock
setAnnouncements(json)
}
catch (err) {
console.error(err)
}
})()
}, [filter])
const toggleFilters = (toggle) => setFilterShown(toggle)
return (<> return (<>
<Filters filter={filter} setFilter={setFilter} filterShown={filterShown} setFilterShown={setFilterShown} /> <Filters filter={filter} setFilter={setFilter} filterShown={filterShown} setFilterShown={setFilterShown} />
<div style={{ display: "flex", justifyContent: "center", backgroundColor: "rgb(17, 17, 17)" }}> <div style={{ display: "flex", justifyContent: "center", backgroundColor: "rgb(17, 17, 17)" }}>
{announcementsFetch.error ? {announcements.length && <Stories
( stories={generateStories(announcements)}
<div style={{ width, height, display: 'flex', alignItems: 'center', justifyContent: 'center' }}> defaultInterval={11000}
<p className='text-danger'>{announcementsFetch.error}</p> height={height}
</div> width={width}
) : ( loop={true}
<Stories keyboardNavigation={true}
stories={stories} />}
defaultInterval={11000}
height={height}
width={width}
loop={true}
keyboardNavigation={true}
/>
)
}
</div> </div>
<BottomNavBar toggleFilters={setFilterShown} width={width} /> <BottomNavBar toggleFilters={toggleFilters} width={width} />
</>) </>)
} }

View File

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

View File

@ -0,0 +1,5 @@
/* В связи со сложившейся политической обстановкой */
.leaflet-attribution-flag {
position: absolute;
right: -100px;
}

View File

@ -6,8 +6,4 @@ const getToken = () => {
return token return token
} }
const setToken = (token) => { export { getToken }
localStorage.setItem("Token", token)
}
export { getToken, setToken }

View File

@ -0,0 +1,18 @@
import { useNavigate } from "react-router-dom"
import { getToken } from "./auth"
function useBook(id) {
const navigate = useNavigate()
const handleBook = () => {
/* TODO */
if (!getToken()) {
return navigate("/login")
}
}
return handleBook
}
export default useBook

View File

@ -2,9 +2,7 @@ import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig( export default defineConfig({
({ command }) => ({ base: "/static",
base: (command === 'serve') ? "/" : "/static", plugins: [react()],
plugins: [react()], })
})
)