Compare commits
13 Commits
9835954647
...
010500e124
Author | SHA1 | Date | |
---|---|---|---|
010500e124 | |||
929dd8af36 | |||
811cde6f30 | |||
fb7f553915 | |||
8af5bd36d9 | |||
dc6f8fe8f2 | |||
e9b7a9e32a | |||
6742b46db7 | |||
6f80c3c3ba | |||
acff0ba187 | |||
92a8b9f384 | |||
bf043d186c | |||
4b158261db |
@ -26,6 +26,7 @@ dist-ssr
|
||||
.venv
|
||||
|
||||
*.db
|
||||
uploads/
|
||||
.env
|
||||
|
||||
__pycache__
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -26,6 +26,7 @@ dist-ssr
|
||||
.venv
|
||||
|
||||
*.db
|
||||
uploads/
|
||||
.env
|
||||
|
||||
__pycache__
|
@ -6,9 +6,9 @@ COPY front .
|
||||
RUN npm run build
|
||||
|
||||
FROM python:3-slim
|
||||
WORKDIR /app
|
||||
WORKDIR /srv
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
||||
COPY ./app ./app
|
||||
COPY ./back ./back
|
||||
COPY --from=builder /src/dist ./front/dist
|
||||
CMD uvicorn app.main:app --host 0.0.0.0 --port 80
|
||||
CMD uvicorn back.main:app --host 0.0.0.0 --port 80
|
||||
|
12
README.md
12
README.md
@ -1,6 +1,12 @@
|
||||
# Porridger
|
||||
|
||||
Food and other stuff sharing platform
|
||||
Food and other stuff sharing platform. The service was developed during Digital Students hackathon by "Полка Billy" team.
|
||||
|
||||
Members:
|
||||
|
||||
* Dmitry Gantimurov - Backend
|
||||
* Dmitriy Shishkov - Frontend
|
||||
* Vladimir Yakovlev - Backend & Design
|
||||
|
||||
## Dev build instructions
|
||||
|
||||
@ -21,7 +27,7 @@ Backend:
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
|
||||
uvicorn app.main:app --reload
|
||||
uvicorn back.main:app --reload
|
||||
```
|
||||
|
||||
## Deploy instructions
|
||||
@ -31,5 +37,5 @@ Only docker/podman are required
|
||||
```sh
|
||||
docker build . -t porridger:build
|
||||
|
||||
docker run --name porridger -p 8080:80 -v ./sql_app.db:/app/sql_app.db porridger:build
|
||||
docker run --name porridger -p 8080:80 -v ./sql_app.db:/srv/sql_app.db -v uploads:/srv/uploads porridger:build
|
||||
```
|
||||
|
13
back/db.py
Normal file
13
back/db.py
Normal file
@ -0,0 +1,13 @@
|
||||
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()
|
40
back/db_manipultations.py
Normal file
40
back/db_manipultations.py
Normal file
@ -0,0 +1,40 @@
|
||||
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()
|
@ -1,112 +1,51 @@
|
||||
#подключение библиотек
|
||||
from fastapi import FastAPI, Response, Path, Depends, Body, Query, status, HTTPException, APIRouter
|
||||
from fastapi import FastAPI, Response, Path, Depends, Body, Form, Query, status, HTTPException, APIRouter, UploadFile, File
|
||||
from fastapi.responses import HTMLResponse, FileResponse, JSONResponse, RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.requests import Request
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy import Column, Integer, String
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from pydantic import json
|
||||
|
||||
from starlette.staticfiles import StaticFiles
|
||||
from app.utils import *
|
||||
|
||||
import requests
|
||||
from uuid import uuid4
|
||||
|
||||
SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
|
||||
engine = create_engine(
|
||||
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
||||
)
|
||||
import ast
|
||||
import pathlib
|
||||
import shutil
|
||||
import os
|
||||
|
||||
Base = declarative_base()
|
||||
from .utils import *
|
||||
from .models import Announcement, Trashbox, UserDatabase, Base
|
||||
from .db import engine, SessionLocal
|
||||
|
||||
class UserDatabase(Base):#класс пользователя
|
||||
__tablename__ = "users"
|
||||
from . import schema
|
||||
|
||||
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)
|
||||
|
||||
SessionLocal = sessionmaker(autoflush=True, 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()
|
||||
|
||||
# Непосредственно преложение
|
||||
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")
|
||||
|
||||
app.mount("/static", StaticFiles(directory = "./front/dist"))
|
||||
app.mount("/uploads", StaticFiles(directory = "./uploads"))
|
||||
|
||||
@app.get("/api/announcements")#адрес объявлений
|
||||
def annoncements_list(user_id: int = None, metro: str = None, category: str = None, booked_by: int = -1):
|
||||
@ -157,15 +96,29 @@ def single_annoncement(user_id:int):
|
||||
|
||||
# Занести объявление в базу
|
||||
@app.put("/api/announcement")#адрес объявлений
|
||||
def put_in_db(data = Body()):
|
||||
try:
|
||||
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)
|
||||
def put_in_db(name: Annotated[str, Form()], category: Annotated[str, Form()], bestBy: Annotated[int, Form()], address: Annotated[str, Form()], longtitude: Annotated[float, Form()], latitude: Annotated[float, Form()], description: Annotated[str, Form()], src: Annotated[UploadFile | None, File()], metro: Annotated[str, Form()], trashId: Annotated[int | None, Form()] = -1):
|
||||
# try:
|
||||
userId = 1 # temporary
|
||||
|
||||
uploaded_name = ""
|
||||
|
||||
f = src.file
|
||||
f.seek(0, os.SEEK_END)
|
||||
if f.tell() > 0:
|
||||
f.seek(0)
|
||||
destination = pathlib.Path("./uploads/" + str(hash(src.file)) + pathlib.Path(src.filename).suffix.lower())
|
||||
with destination.open('wb') as buffer:
|
||||
shutil.copyfileobj(src.file, buffer)
|
||||
|
||||
uploaded_name = "/uploads/"+destination.name
|
||||
|
||||
temp_ancmt = Announcement(user_id=userId, name=name, category=category, best_by=bestBy, adress=address, longtitude=longtitude, latitude=latitude, description=description, src=uploaded_name, metro=metro, trashId=trashId)
|
||||
db.add(temp_ancmt) # добавляем в бд
|
||||
db.commit() # сохраняем изменения
|
||||
db.refresh(temp_ancmt) # обновляем состояние объекта
|
||||
return {"Answer" : True}
|
||||
except:
|
||||
return {"Answer" : False}
|
||||
# except:
|
||||
# return {"Answer" : False}
|
||||
|
||||
|
||||
# Удалить объявления из базы
|
||||
@ -180,15 +133,19 @@ def delete_from_db(data = Body()):#функция удаления объект
|
||||
|
||||
|
||||
# Забронировать объявление
|
||||
@app.put("/api/book")
|
||||
def change_book_status(data = Body()):
|
||||
# Получаем id пользователя, который бронирует объявление
|
||||
temp_user_id = 1
|
||||
# Находим объявление по данному id
|
||||
announcement_to_change = db.query(Announcement).filter(user_id == temp_user_id).first()
|
||||
# Изменяем поле booked_status на полученный id
|
||||
announcement_to_change.booked_status = temp_user_id
|
||||
return {"Success": True}
|
||||
@app.post("/api/book")
|
||||
def change_book_status(data: schema.Book):
|
||||
try:
|
||||
# Получаем id пользователя, который бронирует объявление
|
||||
temp_user_id = 1
|
||||
# Находим объявление по данному id
|
||||
announcement_to_change = db.query(Announcement).filter(id == data.id).first()
|
||||
# Изменяем поле booked_status на полученный id
|
||||
announcement_to_change.booked_status = temp_user_id
|
||||
return {"Success": True}
|
||||
except:
|
||||
return {"Success": False}
|
||||
|
||||
|
||||
@app.post("/api/signup")
|
||||
def create_user(data = Body()):
|
||||
@ -241,29 +198,30 @@ def get_trashboxes(lat:float, lng:float):#крутая функция для р
|
||||
head = {'Authorization': 'Bearer {}'.format(my_token)}
|
||||
|
||||
my_data={
|
||||
'x' : f"{lat}",
|
||||
'y' : f"{lng}",
|
||||
'x' : f"{lng}",
|
||||
'y' : f"{lat}",
|
||||
'limit' : '1'
|
||||
}
|
||||
|
||||
response = requests.post(f"{BASE_URL}/nearest_recycling/get", headers=head, data=my_data)
|
||||
infos = response.json()
|
||||
|
||||
|
||||
trashboxes = []
|
||||
for trashbox in infos["results"]:
|
||||
temp_dict = {}
|
||||
temp_dict["Category"] = trashbox["Category"]
|
||||
for obj in trashbox["Objects"]:
|
||||
coord_list = obj["geometry"]
|
||||
temp_dict["Lat"] = coord_list["coordinates"][0]
|
||||
temp_dict["Lng"] = coord_list["coordinates"][1]
|
||||
temp_dict["Lat"] = coord_list["coordinates"][1]
|
||||
temp_dict["Lng"] = coord_list["coordinates"][0]
|
||||
|
||||
properties = obj["properties"]
|
||||
temp_dict["Name"] = properties["title"]
|
||||
temp_dict["Adress"] = properties["address"]
|
||||
temp_dict["Address"] = properties["address"]
|
||||
temp_dict["Categories"] = properties["content_text"].split(',')
|
||||
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}")
|
||||
async def react_app(req: Request, rest_of_path: str):
|
45
back/models.py
Normal file
45
back/models.py
Normal file
@ -0,0 +1,45 @@
|
||||
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)#категория продукта из объявления
|
||||
|
5
back/schema.py
Normal file
5
back/schema.py
Normal file
@ -0,0 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
class Book(BaseModel):
|
||||
id: int
|
||||
|
83
front/package-lock.json
generated
83
front/package-lock.json
generated
@ -12,6 +12,7 @@
|
||||
"leaflet": "^1.9.3",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^2.7.4",
|
||||
"react-bootstrap-typeahead": "^6.1.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-insta-stories": "^2.5.9",
|
||||
"react-leaflet": "^4.2.1",
|
||||
@ -1340,6 +1341,11 @@
|
||||
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
|
||||
"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": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@ -1894,8 +1900,7 @@
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||
},
|
||||
"node_modules/fast-json-stable-stringify": {
|
||||
"version": "2.1.0",
|
||||
@ -2635,6 +2640,11 @@
|
||||
"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": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
@ -3049,6 +3059,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||
@ -3061,6 +3094,11 @@
|
||||
"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": {
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/react-insta-stories/-/react-insta-stories-2.5.9.tgz",
|
||||
@ -3092,6 +3130,39 @@
|
||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||
"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": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
|
||||
@ -3280,6 +3351,14 @@
|
||||
"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": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
|
@ -4,7 +4,7 @@
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
"build": "vite build",
|
||||
"lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
@ -14,6 +14,7 @@
|
||||
"leaflet": "^1.9.3",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^2.7.4",
|
||||
"react-bootstrap-typeahead": "^6.1.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-insta-stories": "^2.5.9",
|
||||
"react-leaflet": "^4.2.1",
|
||||
|
@ -5,6 +5,12 @@ body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.modal-content, .form-select {
|
||||
.modal-content, .modal-content .form-select {
|
||||
background-color: rgb(17, 17, 17) !important;
|
||||
}
|
||||
|
||||
/* В связи со сложившейся политической обстановкой */
|
||||
.leaflet-attribution-flag {
|
||||
position: absolute;
|
||||
right: -100px;
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'
|
||||
import { BrowserRouter as Router, Route, Routes, Link } from 'react-router-dom'
|
||||
|
||||
import { HomePage, AddPage, LoginPage, UserPage } from './pages'
|
||||
|
||||
import WithToken from './components/WithToken'
|
||||
|
||||
import 'leaflet/dist/leaflet.css'
|
||||
|
||||
import './App.css'
|
||||
|
||||
function App() {
|
||||
@ -18,7 +20,8 @@ function App() {
|
||||
} />
|
||||
<Route path="/user" element={
|
||||
<WithToken>
|
||||
<UserPage />
|
||||
{/* <UserPage /> */}
|
||||
<h1>For Yet Go <Link to="/">Home</Link></h1>
|
||||
</WithToken>
|
||||
} />
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
|
@ -1,6 +1,95 @@
|
||||
const metros = [
|
||||
"Петроградская",
|
||||
"Горьковская"
|
||||
]
|
||||
const metros = {
|
||||
red: [
|
||||
"Девяткино",
|
||||
"Гражданский проспект",
|
||||
"Академическая",
|
||||
"Политехническая",
|
||||
"Площадь Мужества",
|
||||
"Лесная",
|
||||
"Выборгская",
|
||||
"Площадь Ленина",
|
||||
"Чернышевская",
|
||||
"Площадь Восстания",
|
||||
"Владимирская",
|
||||
"Пушкинская",
|
||||
"Технологический институт",
|
||||
"Балтийская",
|
||||
"Нарвская",
|
||||
"Кировский завод",
|
||||
"Автово",
|
||||
"Ленинский проспект",
|
||||
"Проспект Ветеранов"
|
||||
],
|
||||
blue: [
|
||||
"Парнас",
|
||||
"Проспект Просвещения",
|
||||
"Озерки",
|
||||
"Удельная",
|
||||
"Пионерская",
|
||||
"Чёрная речка",
|
||||
"Петроградская",
|
||||
"Горьковская",
|
||||
"Невский проспект",
|
||||
"Сенная площадь",
|
||||
"Технологический институт",
|
||||
"Фрунзенская",
|
||||
"Московские ворота",
|
||||
"Электросила",
|
||||
"Парк Победы",
|
||||
"Московская",
|
||||
"Звёздная",
|
||||
"Купчино"
|
||||
],
|
||||
green: [
|
||||
"Приморская",
|
||||
"Беговая",
|
||||
"Василеостровская",
|
||||
"Гостиный двор",
|
||||
"Маяковская",
|
||||
"Площадь Александра Невского",
|
||||
"Елизаровская",
|
||||
"Ломоносовская",
|
||||
"Пролетарская",
|
||||
"Обухово",
|
||||
"Рыбацкое"
|
||||
],
|
||||
orange: [
|
||||
"Спасская",
|
||||
"Горный институт",
|
||||
"Театральная",
|
||||
"Достоевская",
|
||||
"Лиговский проспект",
|
||||
"Площадь Александра Невского",
|
||||
"Новочеркасская",
|
||||
"Ладожская",
|
||||
"Проспект Большевиков",
|
||||
"Улица Дыбенко"
|
||||
],
|
||||
violet: [
|
||||
"Комендантский проспект",
|
||||
"Старая Деревня",
|
||||
"Крестовский остров",
|
||||
"Чкаловская",
|
||||
"Спортивная",
|
||||
"Адмиралтейская",
|
||||
"Садовая",
|
||||
"Звенигородская",
|
||||
"Обводный канал",
|
||||
"Волковская",
|
||||
"Бухарестская",
|
||||
"Международная",
|
||||
"Проспект славы",
|
||||
"Дунайскай",
|
||||
"Шушары"
|
||||
],
|
||||
gold: [
|
||||
"Юго западная",
|
||||
"Каретная",
|
||||
"Путиловская",
|
||||
"Броневая",
|
||||
"Заставская",
|
||||
"Боровая"
|
||||
]
|
||||
}
|
||||
|
||||
export { metros }
|
||||
|
13
front/src/assets/puff.svg
Normal file
13
front/src/assets/puff.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<!-- 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>
|
After Width: | Height: | Size: 1.0 KiB |
@ -1,19 +1,18 @@
|
||||
import Modal from 'react-bootstrap/Modal'
|
||||
import { Modal, Button } from 'react-bootstrap'
|
||||
import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet'
|
||||
|
||||
import { categoryNames } from '../assets/category'
|
||||
import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import useBook from '../utils/useBook'
|
||||
import { useBook } from '../hooks/api'
|
||||
|
||||
function AnnouncementDetails({ close, announcement: { id, name, category, bestBy, description, lat, lng, address, metro } }) {
|
||||
const handleBook = useBook(id)
|
||||
|
||||
const {handleBook, status: bookStatus} = useBook(id)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="modal show"
|
||||
style={{ display: 'flex', position: 'initial', alignItems: "center" }}
|
||||
>
|
||||
<Modal.Dialog>
|
||||
<Modal.Dialog style={{minWidth: "50vw"}}>
|
||||
<Modal.Header closeButton onHide={close}>
|
||||
<Modal.Title>
|
||||
Подробнее
|
||||
@ -24,10 +23,10 @@ function AnnouncementDetails({ close, announcement: { id, name, category, bestBy
|
||||
<h1>{name}</h1>
|
||||
|
||||
<span>{categoryNames.get(category)}</span>
|
||||
<span className='m-2'>•</span> {/* dot */}
|
||||
<span className='m-2'>•</span>{/* dot */}
|
||||
<span>Годен до {new Date(bestBy).toLocaleString('ru-RU')}</span>
|
||||
|
||||
<p className='mb-2'>{description}</p>
|
||||
|
||||
<p className='mb-3'>{description}</p>
|
||||
|
||||
<MapContainer style={{ width: "100%", minHeight: 300 }} center={[lat, lng]} zoom={16} >
|
||||
<TileLayer
|
||||
@ -43,7 +42,7 @@ function AnnouncementDetails({ close, announcement: { id, name, category, bestBy
|
||||
|
||||
<Modal.Footer>
|
||||
<Button variant='success' onClick={handleBook}>
|
||||
Забронировать
|
||||
{bookStatus || "Забронировать"}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Dialog>
|
||||
|
@ -27,7 +27,7 @@ function Filters({ filter, setFilter, filterShown, setFilterShown }) {
|
||||
Фильтрация
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
||||
|
||||
<Modal.Body>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form.Group className="mb-3" controlId="categoryFilter">
|
||||
@ -55,9 +55,11 @@ function Filters({ filter, setFilter, filterShown, setFilterShown }) {
|
||||
<option value="">
|
||||
Выберите станцию метро
|
||||
</option>
|
||||
{metros.map(
|
||||
(metro) =>
|
||||
<option key={metro} value={metro}>{metro}</option>
|
||||
{Object.entries(metros).map(
|
||||
([branch, stations]) =>
|
||||
stations.map(metro =>
|
||||
<option key={metro} value={metro}>{metro}</option>
|
||||
)
|
||||
)}
|
||||
</Form.Select>
|
||||
</Form.Group>
|
||||
|
27
front/src/components/LocationMarker.jsx
Normal file
27
front/src/components/LocationMarker.jsx
Normal file
@ -0,0 +1,27 @@
|
||||
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
|
26
front/src/components/TrashboxMarkers.jsx
Normal file
26
front/src/components/TrashboxMarkers.jsx
Normal file
@ -0,0 +1,26 @@
|
||||
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
|
@ -1,3 +1,3 @@
|
||||
const API_URL = "api"
|
||||
const API_URL = "/api"
|
||||
|
||||
export { API_URL }
|
5
front/src/hooks/api/index.js
Normal file
5
front/src/hooks/api/index.js
Normal file
@ -0,0 +1,5 @@
|
||||
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'
|
31
front/src/hooks/api/useAddAnnouncement.js
Normal file
31
front/src/hooks/api/useAddAnnouncement.js
Normal file
@ -0,0 +1,31 @@
|
||||
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
|
61
front/src/hooks/api/useAuth.js
Normal file
61
front/src/hooks/api/useAuth.js
Normal file
@ -0,0 +1,61 @@
|
||||
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
|
42
front/src/hooks/api/useBook.js
Normal file
42
front/src/hooks/api/useBook.js
Normal file
@ -0,0 +1,42 @@
|
||||
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
|
41
front/src/hooks/api/useFetch.js
Normal file
41
front/src/hooks/api/useFetch.js
Normal file
@ -0,0 +1,41 @@
|
||||
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
|
26
front/src/hooks/api/useHomeAnnouncementList.js
Normal file
26
front/src/hooks/api/useHomeAnnouncementList.js
Normal file
@ -0,0 +1,26 @@
|
||||
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
|
12
front/src/hooks/api/useTrashboxes.js
Normal file
12
front/src/hooks/api/useTrashboxes.js
Normal file
@ -0,0 +1,12 @@
|
||||
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
|
@ -1,8 +1,199 @@
|
||||
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() {
|
||||
const [addressPosition, setAddressPosition] = useState(latLng(59.972, 30.3227))
|
||||
const [address, setAddress] = useState('')
|
||||
|
||||
// TODO
|
||||
const { data: trashboxes, trashboxes_loading, trashboxes_error } = useTrashboxes(addressPosition)
|
||||
const [selectedTrashbox, setSelectedTrashbox] = useState({ index: -1, category: '' })
|
||||
|
||||
return <></>
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch(location.protocol + "//nominatim.openstreetmap.org/search?format=json&q=" + address)
|
||||
|
||||
const fetchData = await res.json()
|
||||
|
||||
console.log("f", fetchData)
|
||||
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
})()
|
||||
}, [address])
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch(location.protocol + "//nominatim.openstreetmap.org/reverse?format=json&accept-language=ru&lat=" + addressPosition.lat + "&lon=" + addressPosition.lng)
|
||||
|
||||
const fetchData = await res.json()
|
||||
|
||||
setAddress(fetchData.display_name)
|
||||
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
})()
|
||||
}, [addressPosition])
|
||||
|
||||
const {doAdd, status} = useAddAnnouncement()
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const formData = new FormData(event.target)
|
||||
|
||||
formData.append("latitude", addressPosition.lat)
|
||||
formData.append("longtitude", addressPosition.lng)
|
||||
formData.append("address", address)
|
||||
formData.set("bestBy", new Date(formData.get("bestBy")).getTime())
|
||||
|
||||
doAdd(formData)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="m-4" style={{ height: 'calc(100vh - 3rem)' }}>
|
||||
<Card.Body style={{ overflowY: "auto" }} >
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form.Group className="mb-3" controlId="name">
|
||||
<Form.Label>Заголовок объявления</Form.Label>
|
||||
<Form.Control type="text" required name="name" />
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3" controlId="category">
|
||||
<Form.Label>Категория</Form.Label>
|
||||
<Form.Select required name="category">
|
||||
<option value="" hidden>
|
||||
Выберите категорию
|
||||
</option>
|
||||
{Array.from(categoryNames).map(
|
||||
([category, categoryName]) =>
|
||||
<option key={category} value={category}>{categoryName}</option>
|
||||
)}
|
||||
</Form.Select>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3" controlId="bestBy">
|
||||
<Form.Label>Срок годности</Form.Label>
|
||||
<Form.Control type="date" required name="bestBy" />
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3" controlId="address">
|
||||
<Form.Label>Адрес выдачи</Form.Label>
|
||||
<div className="mb-3">
|
||||
<MapContainer
|
||||
scrollWheelZoom={false}
|
||||
style={{ width: "100%", height: 400 }}
|
||||
center={addressPosition}
|
||||
zoom={13}
|
||||
>
|
||||
<TileLayer
|
||||
attribution='© <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='© <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
|
||||
|
@ -1,18 +1,15 @@
|
||||
import Stories from 'react-insta-stories'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Stories from 'react-insta-stories'
|
||||
|
||||
import BottomNavBar from '../components/BottomNavBar'
|
||||
import useStoryDimensions from '../utils/useStoryDimensions'
|
||||
|
||||
import { API_URL } from '../config'
|
||||
|
||||
import "./leafletStyles.css"
|
||||
import 'leaflet/dist/leaflet.css'
|
||||
|
||||
import useStoryDimensions from '../hooks/useStoryDimensions'
|
||||
import AnnouncementDetails from '../components/AnnouncementDetails'
|
||||
import { categoryGraphics } from '../assets/category'
|
||||
import Filters from '../components/Filters'
|
||||
import { removeNull } from '../utils'
|
||||
|
||||
import { useHomeAnnouncementList } from '../hooks/api'
|
||||
|
||||
import puffSpinner from '../assets/puff.svg'
|
||||
import { categoryGraphics } from '../assets/category'
|
||||
|
||||
function generateStories(announcements) {
|
||||
return announcements.map(announcement => {
|
||||
@ -25,76 +22,68 @@ function generateStories(announcements) {
|
||||
})
|
||||
}
|
||||
|
||||
const mock = [
|
||||
{
|
||||
id: 5,
|
||||
name: "Огурец",
|
||||
category: "fruits_vegatables",
|
||||
src: "https://upload.wikimedia.org/wikipedia/commons/thumb/2/24/Marketvegetables.jpg/800px-Marketvegetables.jpg",
|
||||
bestBy: 10000,
|
||||
description: "Очень вкусный огурец, прям, закачаешься",
|
||||
lat: 59.9724,
|
||||
lng: 30.3227,
|
||||
address: "ул. Профессора Попова, дом 5 литера Ф",
|
||||
metro: "Петроградская"
|
||||
function fallbackGenerateStories(announcementsFetch) {
|
||||
const stories = generateStories(announcementsFetch.data)
|
||||
|
||||
if (announcementsFetch.loading)
|
||||
return fallbackStory()
|
||||
|
||||
if (announcementsFetch.error)
|
||||
return fallbackStory(announcementsFetch.error)
|
||||
|
||||
if (stories.length == 0)
|
||||
return fallbackStory("Здесь пока пусто")
|
||||
|
||||
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>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "Арбуз",
|
||||
category: "soup",
|
||||
src: "/static/watermelon.mp4",
|
||||
bestBy: 20000,
|
||||
description: "Очень вкусный арбуз, прям, закачаешься",
|
||||
lat: 60.9724,
|
||||
lng: 30.3227,
|
||||
address: "ул. Профессора Попова, дом 50 литера Ф",
|
||||
metro: "Горьковская"
|
||||
}
|
||||
]
|
||||
header: { heading: text }
|
||||
}]
|
||||
|
||||
const defaultFilters = { userId: null, category: null, metro: null, bookedBy: null }
|
||||
|
||||
const PROD = true
|
||||
|
||||
function HomePage() {
|
||||
const { height, width } = useStoryDimensions(16 / 10)
|
||||
|
||||
const [announcements, setAnnouncements] = useState([])
|
||||
|
||||
const [filterShown, setFilterShown] = useState(false)
|
||||
|
||||
const [filter, setFilter] = useState(defaultFilters)
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const res = PROD ? await fetch(API_URL + "/announcements?" + new URLSearchParams(removeNull(filter))) : null
|
||||
const announcementsFetch = useHomeAnnouncementList(filter)
|
||||
|
||||
const json = PROD ? (await res.json()).list_of_announcements : mock
|
||||
|
||||
setAnnouncements(json)
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
})()
|
||||
}, [filter])
|
||||
|
||||
const toggleFilters = (toggle) => setFilterShown(toggle)
|
||||
const stories = fallbackGenerateStories(announcementsFetch)
|
||||
|
||||
return (<>
|
||||
<Filters filter={filter} setFilter={setFilter} filterShown={filterShown} setFilterShown={setFilterShown} />
|
||||
<div style={{ display: "flex", justifyContent: "center", backgroundColor: "rgb(17, 17, 17)" }}>
|
||||
{announcements.length && <Stories
|
||||
stories={generateStories(announcements)}
|
||||
defaultInterval={11000}
|
||||
height={height}
|
||||
width={width}
|
||||
loop={true}
|
||||
keyboardNavigation={true}
|
||||
/>}
|
||||
{announcementsFetch.error ?
|
||||
(
|
||||
<div style={{ width, height, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<p className='text-danger'>{announcementsFetch.error}</p>
|
||||
</div>
|
||||
) : (
|
||||
<Stories
|
||||
stories={stories}
|
||||
defaultInterval={11000}
|
||||
height={height}
|
||||
width={width}
|
||||
loop={true}
|
||||
keyboardNavigation={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<BottomNavBar toggleFilters={toggleFilters} width={width} />
|
||||
<BottomNavBar toggleFilters={setFilterShown} width={width} />
|
||||
</>)
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,88 @@
|
||||
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() {
|
||||
return <></>
|
||||
const navigate = useNavigate()
|
||||
|
||||
const doAuth = useAuth()
|
||||
|
||||
const handleAuth = (newAccount) => (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const formData = new FormData(event.currentTarget)
|
||||
|
||||
const data = {
|
||||
email: formData.get('email'),
|
||||
name: newAccount ? formData.get('name') : undefined,
|
||||
password: formData.get('password')
|
||||
}
|
||||
|
||||
const token = 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
|
@ -1,5 +0,0 @@
|
||||
/* В связи со сложившейся политической обстановкой */
|
||||
.leaflet-attribution-flag {
|
||||
position: absolute;
|
||||
right: -100px;
|
||||
}
|
@ -6,4 +6,8 @@ const getToken = () => {
|
||||
return token
|
||||
}
|
||||
|
||||
export { getToken }
|
||||
const setToken = (token) => {
|
||||
localStorage.setItem("Token", token)
|
||||
}
|
||||
|
||||
export { getToken, setToken }
|
@ -1,18 +0,0 @@
|
||||
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
|
@ -2,7 +2,9 @@ import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: "/static",
|
||||
plugins: [react()],
|
||||
})
|
||||
export default defineConfig(
|
||||
({ command }) => ({
|
||||
base: (command === 'serve') ? "/" : "/static",
|
||||
plugins: [react()],
|
||||
})
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user