From 65b90f9636c89b7d70326e0598d1dc0fcc09da31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0=20=D0=A1=D0=B5=D1=80?= =?UTF-8?q?=D0=B3=D0=B5=D0=B5=D0=B2?= Date: Tue, 5 Nov 2024 19:48:50 +0400 Subject: [PATCH 01/10] base entity --- server/src/data/models.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/server/src/data/models.py b/server/src/data/models.py index 3d1aa26..e7ef36e 100644 --- a/server/src/data/models.py +++ b/server/src/data/models.py @@ -1,6 +1,7 @@ from datetime import datetime -from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase +from sqlalchemy import Integer, ForeignKey, String +from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase, relationship from sqlalchemy.dialects.mysql import TIMESTAMP, TIME, VARCHAR @@ -34,3 +35,32 @@ class Weather(Base): WindSpeed: Mapped[float] WindSpeed10Min: Mapped[float] +class WindTurbineType(Base): + __tablename__ = 'wind_turbine_type' + Id: Mapped[int] = mapped_column(primary_key=True) + Name: Mapped[str] = mapped_column(VARCHAR(255)) + Height: Mapped[float] + BladeLength: Mapped[float] + + +class WindPark(Base): + __tablename__ = 'wind_park' + Id: Mapped[int] = mapped_column(primary_key=True) + Name: Mapped[str] = mapped_column(VARCHAR(255)) + CenterLatitude: Mapped[float] + CenterLongitude: Mapped[float] + + +class WindParkTurbine(Base): + __tablename__ = 'wind_park_turbine' + + wind_park_id: Mapped[int] = mapped_column(Integer, ForeignKey('wind_park.id'), primary_key=True) + turbine_id: Mapped[int] = mapped_column(Integer, ForeignKey('wind_turbine_type.id'), primary_key=True) + x_offset: Mapped[int] = mapped_column(Integer, nullable=False) + y_offset: Mapped[int] = mapped_column(Integer, nullable=False) + angle: Mapped[int] = mapped_column(Integer, nullable=True) + comment: Mapped[str] = mapped_column(String(2000), nullable=True) + + # Связи с другими таблицами + wind_park: Mapped["WindPark"] = relationship("WindPark", back_populates="wind_park_turbines") + turbine: Mapped["WindTurbineType"] = relationship("WindTurbineType", back_populates="turbine_parks") From 484f1f205eaf206b16a9ca140fe1157ad1b49faf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0=20=D0=A1=D0=B5=D1=80?= =?UTF-8?q?=D0=B3=D0=B5=D0=B5=D0=B2?= Date: Tue, 5 Nov 2024 20:13:37 +0400 Subject: [PATCH 02/10] repository.py schemas.py wind_park_router.py --- server/src/data/repository.py | 108 ++++++++++++++++++++++++- server/src/data/schemas.py | 49 ++++++++++- server/src/routers/wind_park_router.py | 87 ++++++++++++++++++++ 3 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 server/src/routers/wind_park_router.py diff --git a/server/src/data/repository.py b/server/src/data/repository.py index b9fbc39..84fb1db 100644 --- a/server/src/data/repository.py +++ b/server/src/data/repository.py @@ -1,5 +1,8 @@ from sqlalchemy import select +from sqlalchemy.orm import Session +from .models import WindTurbineType, WindPark, WindParkTurbine +from .schemas import WindTurbineTypeCreate, WindParkCreate, WindParkTurbineCreate from data.database import session_maker from data.models import Weather from data.schemas import SWeatherInfo @@ -24,4 +27,107 @@ class WeatherRepository: ) res = session.execute(query) weather_model = res.scalars().first() - return SWeatherInfo.model_validate(weather_model, from_attributes=True) \ No newline at end of file + return SWeatherInfo.model_validate(weather_model, from_attributes=True) + + + +class WindTurbineTypeRepository: + @staticmethod + def create(db: Session, turbine_type: WindTurbineTypeCreate): + db_turbine_type = WindTurbineType(**turbine_type.dict()) + db.add(db_turbine_type) + db.commit() + db.refresh(db_turbine_type) + return db_turbine_type + + @staticmethod + def get(db: Session, turbine_type_id: int): + return db.query(WindTurbineType).filter(WindTurbineType.Id == turbine_type_id).first() + + @staticmethod + def update(db: Session, turbine_type_id: int, turbine_type: WindTurbineTypeCreate): + db_turbine_type = db.query(WindTurbineType).filter(WindTurbineType.Id == turbine_type_id).first() + if db_turbine_type: + for key, value in turbine_type.dict().items(): + setattr(db_turbine_type, key, value) + db.commit() + db.refresh(db_turbine_type) + return db_turbine_type + return None + + @staticmethod + def delete(db: Session, turbine_type_id: int): + db_turbine_type = db.query(WindTurbineType).filter(WindTurbineType.Id == turbine_type_id).first() + if db_turbine_type: + db.delete(db_turbine_type) + db.commit() + return True + return False + + +class WindParkRepository: + @staticmethod + def create(db: Session, park: WindParkCreate): + db_park = WindPark(**park.dict()) + db.add(db_park) + db.commit() + db.refresh(db_park) + return db_park + + @staticmethod + def get(db: Session, park_id: int): + return db.query(WindPark).filter(WindPark.Id == park_id).first() + + @staticmethod + def update(db: Session, park_id: int, park: WindParkCreate): + db_park = db.query(WindPark).filter(WindPark.Id == park_id).first() + if db_park: + for key, value in park.dict().items(): + setattr(db_park, key, value) + db.commit() + db.refresh(db_park) + return db_park + return None + + @staticmethod + def delete(db: Session, park_id: int): + db_park = db.query(WindPark).filter(WindPark.Id == park_id).first() + if db_park: + db.delete(db_park) + db.commit() + return True + return False + + +class WindParkTurbineRepository: + @staticmethod + def create(db: Session, park_turbine: WindParkTurbineCreate): + db_park_turbine = WindParkTurbine(**park_turbine.dict()) + db.add(db_park_turbine) + db.commit() + db.refresh(db_park_turbine) + return db_park_turbine + + @staticmethod + def get(db: Session, park_turbine_id: int): + return db.query(WindParkTurbine).filter(WindParkTurbine.turbine_id == park_turbine_id).first() + + @staticmethod + def update(db: Session, park_turbine_id: int, park_turbine: WindParkTurbineCreate): + db_park_turbine = db.query(WindParkTurbine).filter(WindParkTurbine.turbine_id == park_turbine_id).first() + if db_park_turbine: + for key, value in park_turbine.dict().items(): + setattr(db_park_turbine, key, value) + db.commit() + db.refresh(db_park_turbine) + return db_park_turbine + return None + + @staticmethod + def delete(db: Session, park_turbine_id: int): + db_park_turbine = db.query(WindParkTurbine).filter(WindParkTurbine.turbine_id == park_turbine_id).first() + if db_park_turbine: + db.delete(db_park_turbine) + db.commit() + return True + return False \ No newline at end of file diff --git a/server/src/data/schemas.py b/server/src/data/schemas.py index 6558a9d..87c0f09 100644 --- a/server/src/data/schemas.py +++ b/server/src/data/schemas.py @@ -1,4 +1,5 @@ from datetime import date +from typing import Optional from pydantic import BaseModel, Field from fastapi import Query @@ -19,4 +20,50 @@ class SFlorisInputParams(BaseModel): class SFlorisOutputData(BaseModel): file_name: str - data: list[float] \ No newline at end of file + data: list[float] + + +class WindTurbineTypeBase(BaseModel): + Name: str + Height: float + BladeLength: float + +class WindTurbineTypeCreate(WindTurbineTypeBase): + pass + +class WindTurbineTypeResponse(WindTurbineTypeBase): + Id: int + + class Config: + orm_mode = True + + +class WindParkBase(BaseModel): + Name: str + CenterLatitude: float + CenterLongitude: float + +class WindParkCreate(WindParkBase): + pass + +class WindParkResponse(WindParkBase): + Id: int + + class Config: + orm_mode = True + + +class WindParkTurbineBase(BaseModel): + wind_park_id: int + turbine_id: int + x_offset: int + y_offset: int + angle: Optional[int] = None + comment: Optional[str] = None + +class WindParkTurbineCreate(WindParkTurbineBase): + pass + +class WindParkTurbineResponse(WindParkTurbineBase): + class Config: + orm_mode = True \ No newline at end of file diff --git a/server/src/routers/wind_park_router.py b/server/src/routers/wind_park_router.py new file mode 100644 index 0000000..cac4098 --- /dev/null +++ b/server/src/routers/wind_park_router.py @@ -0,0 +1,87 @@ +from fastapi import APIRouter, HTTPException, Depends +from sqlalchemy.orm import Session +from typing import List + +from data.schemas import WindTurbineTypeCreate, WindTurbineTypeResponse, WindParkCreate, WindParkResponse, WindParkTurbineCreate, WindParkTurbineResponse +from data.database import get_db +from data.repository import WindTurbineTypeRepository, WindParkRepository, WindParkTurbineRepository + +router = APIRouter( + prefix="/api/wind", + tags=["Wind API"], +) + +# Wind Turbine Type CRUD +@router.post("/turbine_type/", response_model=WindTurbineTypeResponse) +async def create_turbine_type(turbine_type: WindTurbineTypeCreate, db: Session = Depends(get_db)): + return WindTurbineTypeRepository.create(db=db, turbine_type=turbine_type) + +@router.get("/turbine_type/{turbine_type_id}", response_model=WindTurbineTypeResponse) +async def read_turbine_type(turbine_type_id: int, db: Session = Depends(get_db)): + turbine_type = WindTurbineTypeRepository.get(db=db, turbine_type_id=turbine_type_id) + if turbine_type is None: + raise HTTPException(status_code=404, detail="Turbine Type not found") + return turbine_type + +@router.put("/turbine_type/{turbine_type_id}", response_model=WindTurbineTypeResponse) +async def update_turbine_type(turbine_type_id: int, turbine_type: WindTurbineTypeCreate, db: Session = Depends(get_db)): + updated_turbine_type = WindTurbineTypeRepository.update(db=db, turbine_type_id=turbine_type_id, turbine_type=turbine_type) + if updated_turbine_type is None: + raise HTTPException(status_code=404, detail="Turbine Type not found") + return updated_turbine_type + +@router.delete("/turbine_type/{turbine_type_id}", status_code=204) +async def delete_turbine_type(turbine_type_id: int, db: Session = Depends(get_db)): + result = WindTurbineTypeRepository.delete(db=db, turbine_type_id=turbine_type_id) + if not result: + raise HTTPException(status_code=404, detail="Turbine Type not found") + +# Wind Park CRUD +@router.post("/park/", response_model=WindParkResponse) +async def create_park(park: WindParkCreate, db: Session = Depends(get_db)): + return WindParkRepository.create(db=db, park=park) + +@router.get("/park/{park_id}", response_model=WindParkResponse) +async def read_park(park_id: int, db: Session = Depends(get_db)): + park = WindParkRepository.get(db=db, park_id=park_id) + if park is None: + raise HTTPException(status_code=404, detail="Park not found") + return park + +@router.put("/park/{park_id}", response_model=WindParkResponse) +async def update_park(park_id: int, park: WindParkCreate, db: Session = Depends(get_db)): + updated_park = WindParkRepository.update(db=db, park_id=park_id, park=park) + if updated_park is None: + raise HTTPException(status_code=404, detail="Park not found") + return updated_park + +@router.delete("/park/{park_id}", status_code=204) +async def delete_park(park_id: int, db: Session = Depends(get_db)): + result = WindParkRepository.delete(db=db, park_id=park_id) + if not result: + raise HTTPException(status_code=404, detail="Park not found") + +# Wind Park Turbine CRUD +@router.post("/park_turbine/", response_model=WindParkTurbineResponse) +async def create_park_turbine(park_turbine: WindParkTurbineCreate, db: Session = Depends(get_db)): + return WindParkTurbineRepository.create(db=db, park_turbine=park_turbine) + +@router.get("/park_turbine/{park_turbine_id}", response_model=WindParkTurbineResponse) +async def read_park_turbine(park_turbine_id: int, db: Session = Depends(get_db)): + park_turbine = WindParkTurbineRepository.get(db=db, park_turbine_id=park_turbine_id) + if park_turbine is None: + raise HTTPException(status_code=404, detail="Park Turbine not found") + return park_turbine + +@router.put("/park_turbine/{park_turbine_id}", response_model=WindParkTurbineResponse) +async def update_park_turbine(park_turbine_id: int, park_turbine: WindParkTurbineCreate, db: Session = Depends(get_db)): + updated_park_turbine = WindParkTurbineRepository.update(db=db, park_turbine_id=park_turbine_id, park_turbine=park_turbine) + if updated_park_turbine is None: + raise HTTPException(status_code=404, detail="Park Turbine not found") + return updated_park_turbine + +@router.delete("/park_turbine/{park_turbine_id}", status_code=204) +async def delete_park_turbine(park_turbine_id: int, db: Session = Depends(get_db)): + result = WindParkTurbineRepository.delete(db=db, park_turbine_id=park_turbine_id) + if not result: + raise HTTPException(status_code=404, detail="Park Turbine not found") From 1fd0f946e67947561687401a446393b0e91ef7fe Mon Sep 17 00:00:00 2001 From: Danil_Malin Date: Tue, 5 Nov 2024 20:53:55 +0400 Subject: [PATCH 03/10] Rest api, CRUD for wind_parks --- server/src/data/database.py | 15 +++++++-- server/src/data/models.py | 27 ++++++++++------ server/src/data/repository.py | 36 ++++++++++++++++++--- server/src/data/schemas.py | 43 ++++++++++++++++++++---- server/src/main.py | 2 ++ server/src/routers/wind_park_router.py | 45 +++++++++++++++++++++++--- 6 files changed, 139 insertions(+), 29 deletions(-) diff --git a/server/src/data/database.py b/server/src/data/database.py index 01711ae..789eb48 100644 --- a/server/src/data/database.py +++ b/server/src/data/database.py @@ -1,11 +1,20 @@ from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import sessionmaker, declarative_base, Session # TODO Maybe create env file DATABASE_URL = "mysql+pymysql://wind:wind@193.124.203.110:3306/wind_towers" engine = create_engine(DATABASE_URL) -session_maker = sessionmaker(autocommit=False, autoflush=False, bind=engine) - +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# Базовый класс для декларативных моделей +Base = declarative_base() +# Функция для получения сессии +def get_db() -> Session: + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/server/src/data/models.py b/server/src/data/models.py index e7ef36e..e85f7eb 100644 --- a/server/src/data/models.py +++ b/server/src/data/models.py @@ -1,6 +1,6 @@ from datetime import datetime -from sqlalchemy import Integer, ForeignKey, String +from sqlalchemy import Integer, ForeignKey, String, Float from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase, relationship from sqlalchemy.dialects.mysql import TIMESTAMP, TIME, VARCHAR @@ -28,39 +28,46 @@ class Weather(Base): RainStorm: Mapped[float] RainYear: Mapped[float] SunRise: Mapped[datetime] = mapped_column(TIME) - SunSet:Mapped[datetime] = mapped_column(TIME) + SunSet: Mapped[datetime] = mapped_column(TIME) TempIn: Mapped[float] TempOut: Mapped[float] WindDir: Mapped[str] = mapped_column(VARCHAR(50)) WindSpeed: Mapped[float] WindSpeed10Min: Mapped[float] + class WindTurbineType(Base): __tablename__ = 'wind_turbine_type' Id: Mapped[int] = mapped_column(primary_key=True) Name: Mapped[str] = mapped_column(VARCHAR(255)) - Height: Mapped[float] - BladeLength: Mapped[float] + Height: Mapped[float] = mapped_column(Float) + BladeLength: Mapped[float] = mapped_column(Float) + + # Связь с WindParkTurbine + turbine_parks: Mapped["list[WindParkTurbine]"] = relationship("WindParkTurbine", back_populates="turbine") class WindPark(Base): __tablename__ = 'wind_park' Id: Mapped[int] = mapped_column(primary_key=True) Name: Mapped[str] = mapped_column(VARCHAR(255)) - CenterLatitude: Mapped[float] - CenterLongitude: Mapped[float] + CenterLatitude: Mapped[float] = mapped_column(Float) + CenterLongitude: Mapped[float] = mapped_column(Float) + + # Связь с WindParkTurbine + wind_park_turbines: Mapped["list[WindParkTurbine]"] = relationship("WindParkTurbine", back_populates="wind_park") class WindParkTurbine(Base): __tablename__ = 'wind_park_turbine' - wind_park_id: Mapped[int] = mapped_column(Integer, ForeignKey('wind_park.id'), primary_key=True) - turbine_id: Mapped[int] = mapped_column(Integer, ForeignKey('wind_turbine_type.id'), primary_key=True) + wind_park_id: Mapped[int] = mapped_column(Integer, ForeignKey('wind_park.Id'), primary_key=True) + turbine_id: Mapped[int] = mapped_column(Integer, ForeignKey('wind_turbine_type.Id'), primary_key=True) x_offset: Mapped[int] = mapped_column(Integer, nullable=False) y_offset: Mapped[int] = mapped_column(Integer, nullable=False) angle: Mapped[int] = mapped_column(Integer, nullable=True) comment: Mapped[str] = mapped_column(String(2000), nullable=True) # Связи с другими таблицами - wind_park: Mapped["WindPark"] = relationship("WindPark", back_populates="wind_park_turbines") - turbine: Mapped["WindTurbineType"] = relationship("WindTurbineType", back_populates="turbine_parks") + wind_park: Mapped[WindPark] = relationship("WindPark", back_populates="wind_park_turbines") + turbine: Mapped[WindTurbineType] = relationship("WindTurbineType", back_populates="turbine_parks") diff --git a/server/src/data/repository.py b/server/src/data/repository.py index 84fb1db..760446a 100644 --- a/server/src/data/repository.py +++ b/server/src/data/repository.py @@ -1,9 +1,11 @@ +from typing import List, Type + from sqlalchemy import select from sqlalchemy.orm import Session from .models import WindTurbineType, WindPark, WindParkTurbine -from .schemas import WindTurbineTypeCreate, WindParkCreate, WindParkTurbineCreate -from data.database import session_maker +from .schemas import WindTurbineTypeCreate, WindParkCreate, WindParkTurbineCreate, WindParkResponse, WindTurbineResponse +from data.database import SessionLocal from data.models import Weather from data.schemas import SWeatherInfo @@ -11,7 +13,7 @@ from data.schemas import SWeatherInfo class WeatherRepository: @classmethod def get_all(cls): - with session_maker() as session: + with SessionLocal() as session: query = ( select(Weather) ) @@ -21,7 +23,7 @@ class WeatherRepository: @classmethod def get_by_id(cls, id): - with session_maker() as session: + with SessionLocal() as session: query = ( select(Weather).where(Weather.id == id) ) @@ -64,6 +66,10 @@ class WindTurbineTypeRepository: return True return False + @staticmethod + def get_all_turbines(db: Session) -> list[Type[WindTurbineType]]: + return db.query(WindTurbineType).all() + class WindParkRepository: @staticmethod @@ -98,6 +104,26 @@ class WindParkRepository: return True return False + @staticmethod + def get_all_parks(db: Session) -> list[WindParkResponse]: + return db.query(WindPark).all() + + @staticmethod + def get_turbines_by_park_id(park_id: int, db: Session) -> list[WindTurbineResponse]: + turbines = ( + db.query(WindParkTurbine) + .filter(WindParkTurbine.wind_park_id == park_id) + .all() + ) + + if not turbines: + return [] # Возвращаем пустой список, если не найдено + + turbine_ids = [turbine.turbine_id for turbine in turbines] + turbine_details = db.query(WindTurbineType).filter(WindTurbineType.Id.in_(turbine_ids)).all() + + return turbine_details + class WindParkTurbineRepository: @staticmethod @@ -130,4 +156,4 @@ class WindParkTurbineRepository: db.delete(db_park_turbine) db.commit() return True - return False \ No newline at end of file + return False diff --git a/server/src/data/schemas.py b/server/src/data/schemas.py index 87c0f09..5c72545 100644 --- a/server/src/data/schemas.py +++ b/server/src/data/schemas.py @@ -28,9 +28,11 @@ class WindTurbineTypeBase(BaseModel): Height: float BladeLength: float + class WindTurbineTypeCreate(WindTurbineTypeBase): pass + class WindTurbineTypeResponse(WindTurbineTypeBase): Id: int @@ -43,15 +45,10 @@ class WindParkBase(BaseModel): CenterLatitude: float CenterLongitude: float + class WindParkCreate(WindParkBase): pass -class WindParkResponse(WindParkBase): - Id: int - - class Config: - orm_mode = True - class WindParkTurbineBase(BaseModel): wind_park_id: int @@ -61,9 +58,41 @@ class WindParkTurbineBase(BaseModel): angle: Optional[int] = None comment: Optional[str] = None + class WindParkTurbineCreate(WindParkTurbineBase): pass + class WindParkTurbineResponse(WindParkTurbineBase): class Config: - orm_mode = True \ No newline at end of file + orm_mode = True + + +class WindTurbineType(BaseModel): + Id: int + Name: str + Height: float + BladeLength: float + + class Config: + orm_mode = True + + +class WindParkResponse(BaseModel): + Id: int + Name: str + CenterLatitude: float + CenterLongitude: float + + class Config: + orm_mode = True + + +class WindTurbineResponse(BaseModel): + Id: int + Name: str + Height: float + BladeLength: float + + class Config: + orm_mode = True diff --git a/server/src/main.py b/server/src/main.py index 78939e1..6dbeee5 100644 --- a/server/src/main.py +++ b/server/src/main.py @@ -6,6 +6,7 @@ from fastapi import FastAPI from fastapi.staticfiles import StaticFiles +from routers.wind_park_router import router as wind_park_router from routers.floris_router import router as floris_router from routers.floris_template_router import router as floris_template_router from routers.weather_router import router as weather_router @@ -33,3 +34,4 @@ app.mount("/public", StaticFiles(directory=Path("../public")), name="public") app.include_router(floris_router) app.include_router(floris_template_router) app.include_router(weather_router) +app.include_router(wind_park_router) diff --git a/server/src/routers/wind_park_router.py b/server/src/routers/wind_park_router.py index cac4098..200ca21 100644 --- a/server/src/routers/wind_park_router.py +++ b/server/src/routers/wind_park_router.py @@ -1,8 +1,8 @@ from fastapi import APIRouter, HTTPException, Depends from sqlalchemy.orm import Session -from typing import List -from data.schemas import WindTurbineTypeCreate, WindTurbineTypeResponse, WindParkCreate, WindParkResponse, WindParkTurbineCreate, WindParkTurbineResponse +from data.schemas import WindTurbineTypeCreate, WindTurbineTypeResponse, WindParkCreate, WindParkResponse, \ + WindParkTurbineCreate, WindParkTurbineResponse, WindTurbineResponse from data.database import get_db from data.repository import WindTurbineTypeRepository, WindParkRepository, WindParkTurbineRepository @@ -11,11 +11,13 @@ router = APIRouter( tags=["Wind API"], ) + # Wind Turbine Type CRUD @router.post("/turbine_type/", response_model=WindTurbineTypeResponse) async def create_turbine_type(turbine_type: WindTurbineTypeCreate, db: Session = Depends(get_db)): return WindTurbineTypeRepository.create(db=db, turbine_type=turbine_type) + @router.get("/turbine_type/{turbine_type_id}", response_model=WindTurbineTypeResponse) async def read_turbine_type(turbine_type_id: int, db: Session = Depends(get_db)): turbine_type = WindTurbineTypeRepository.get(db=db, turbine_type_id=turbine_type_id) @@ -23,24 +25,29 @@ async def read_turbine_type(turbine_type_id: int, db: Session = Depends(get_db)) raise HTTPException(status_code=404, detail="Turbine Type not found") return turbine_type + @router.put("/turbine_type/{turbine_type_id}", response_model=WindTurbineTypeResponse) async def update_turbine_type(turbine_type_id: int, turbine_type: WindTurbineTypeCreate, db: Session = Depends(get_db)): - updated_turbine_type = WindTurbineTypeRepository.update(db=db, turbine_type_id=turbine_type_id, turbine_type=turbine_type) + updated_turbine_type = WindTurbineTypeRepository.update(db=db, turbine_type_id=turbine_type_id, + turbine_type=turbine_type) if updated_turbine_type is None: raise HTTPException(status_code=404, detail="Turbine Type not found") return updated_turbine_type + @router.delete("/turbine_type/{turbine_type_id}", status_code=204) async def delete_turbine_type(turbine_type_id: int, db: Session = Depends(get_db)): result = WindTurbineTypeRepository.delete(db=db, turbine_type_id=turbine_type_id) if not result: raise HTTPException(status_code=404, detail="Turbine Type not found") + # Wind Park CRUD @router.post("/park/", response_model=WindParkResponse) async def create_park(park: WindParkCreate, db: Session = Depends(get_db)): return WindParkRepository.create(db=db, park=park) + @router.get("/park/{park_id}", response_model=WindParkResponse) async def read_park(park_id: int, db: Session = Depends(get_db)): park = WindParkRepository.get(db=db, park_id=park_id) @@ -48,6 +55,7 @@ async def read_park(park_id: int, db: Session = Depends(get_db)): raise HTTPException(status_code=404, detail="Park not found") return park + @router.put("/park/{park_id}", response_model=WindParkResponse) async def update_park(park_id: int, park: WindParkCreate, db: Session = Depends(get_db)): updated_park = WindParkRepository.update(db=db, park_id=park_id, park=park) @@ -55,17 +63,20 @@ async def update_park(park_id: int, park: WindParkCreate, db: Session = Depends( raise HTTPException(status_code=404, detail="Park not found") return updated_park + @router.delete("/park/{park_id}", status_code=204) async def delete_park(park_id: int, db: Session = Depends(get_db)): result = WindParkRepository.delete(db=db, park_id=park_id) if not result: raise HTTPException(status_code=404, detail="Park not found") + # Wind Park Turbine CRUD @router.post("/park_turbine/", response_model=WindParkTurbineResponse) async def create_park_turbine(park_turbine: WindParkTurbineCreate, db: Session = Depends(get_db)): return WindParkTurbineRepository.create(db=db, park_turbine=park_turbine) + @router.get("/park_turbine/{park_turbine_id}", response_model=WindParkTurbineResponse) async def read_park_turbine(park_turbine_id: int, db: Session = Depends(get_db)): park_turbine = WindParkTurbineRepository.get(db=db, park_turbine_id=park_turbine_id) @@ -73,15 +84,41 @@ async def read_park_turbine(park_turbine_id: int, db: Session = Depends(get_db)) raise HTTPException(status_code=404, detail="Park Turbine not found") return park_turbine + @router.put("/park_turbine/{park_turbine_id}", response_model=WindParkTurbineResponse) async def update_park_turbine(park_turbine_id: int, park_turbine: WindParkTurbineCreate, db: Session = Depends(get_db)): - updated_park_turbine = WindParkTurbineRepository.update(db=db, park_turbine_id=park_turbine_id, park_turbine=park_turbine) + updated_park_turbine = WindParkTurbineRepository.update(db=db, park_turbine_id=park_turbine_id, + park_turbine=park_turbine) if updated_park_turbine is None: raise HTTPException(status_code=404, detail="Park Turbine not found") return updated_park_turbine + @router.delete("/park_turbine/{park_turbine_id}", status_code=204) async def delete_park_turbine(park_turbine_id: int, db: Session = Depends(get_db)): result = WindParkTurbineRepository.delete(db=db, park_turbine_id=park_turbine_id) if not result: raise HTTPException(status_code=404, detail="Park Turbine not found") + + +@router.get("/parks/", response_model=list[WindParkResponse]) +async def get_all_parks(db: Session = Depends(get_db)): + """Получить все ветропарки""" + return WindParkRepository.get_all_parks(db) + + +@router.get("/parks/{park_id}/turbines/", response_model=list[WindTurbineResponse]) +async def get_turbines_by_park_id(park_id: int, db: Session = Depends(get_db)): + """Получить все турбины в ветропарке по ID""" + turbines = WindParkRepository.get_turbines_by_park_id(park_id, db) + + if not turbines: + raise HTTPException(status_code=404, detail="Турбины не найдены для данного ветропарка") + + return turbines + + +@router.get("/turbines/", response_model=list[WindTurbineResponse]) +async def get_all_turbines(db: Session = Depends(get_db)): + """Получить все турбины""" + return WindTurbineTypeRepository.get_all_turbines(db) From d387e5c5bf667fe923067bd2e26727136bcdef92 Mon Sep 17 00:00:00 2001 From: it-is-not-alright Date: Mon, 11 Nov 2024 00:55:10 +0400 Subject: [PATCH 04/10] [test-entity]: front refresh --- front/public/images/svg/plus.svg | 7 -- front/public/images/svg/upload.svg | 15 +++ front/src/api/floris/constants.ts | 2 - front/src/api/floris/index.ts | 1 - front/src/api/floris/service.ts | 26 ---- front/src/api/floris/types.ts | 4 - front/src/api/floris/utils.ts | 9 -- front/src/api/index.tsx | 1 - front/src/components/_func.scss | 19 +++ front/src/components/_mixins.scss | 0 front/src/components/app/_theme.scss | 8 +- front/src/components/app/index.tsx | 3 +- .../components/layouts/main-layout/index.tsx | 4 +- .../layouts/main-layout/styles.module.scss | 5 +- .../components/pages/form-page/component.tsx | 12 ++ front/src/components/pages/form-page/index.ts | 1 + .../pages/form-page/styles.module.scss | 11 ++ .../components/pages/home-page/component.tsx | 59 ++++----- .../pages/home-page/styles.module.scss | 41 ++---- front/src/components/pages/index.tsx | 1 + .../ui/animation/ripple/component.tsx | 89 +++++++------ .../ripple/parts/ripple-wave/component.tsx | 39 ------ .../ripple/parts/ripple-wave/index.ts | 1 - .../parts/ripple-wave/style.module.scss | 33 ----- .../ripple/parts/ripple-wave/types.ts | 6 - .../ui/animation/ripple/styles.module.scss | 34 +++++ .../components/ui/animation/ripple/types.ts | 1 - .../components/ui/animation/ripple/utils.ts | 7 +- .../components/ui/autocomplete/component.tsx | 119 ++++++++++++++++++ front/src/components/ui/autocomplete/index.ts | 3 + .../components/ui/autocomplete/preview.tsx | 44 +++++++ .../ui/autocomplete/styles.module.scss | 62 +++++++++ front/src/components/ui/autocomplete/types.ts | 14 +++ front/src/components/ui/button/component.tsx | 2 +- .../components/ui/button/styles.module.scss | 28 +++-- .../src/components/ui/calendar/component.tsx | 2 +- .../calendar-days/component.tsx | 2 +- .../calendar-days/constants.ts | 0 .../calendar-days/index.ts | 0 .../calendar-days/styles.module.scss | 13 +- .../calendar-days/types.ts | 2 +- .../calendar-days/utils.ts | 11 +- .../calendar/{parts => components}/index.ts | 0 front/src/components/ui/calendar/types.ts | 2 +- .../ui/checkbox-group/styles.module.scss | 10 +- .../src/components/ui/checkbox/component.tsx | 2 +- .../components/ui/checkbox/styles.module.scss | 40 +++--- .../components/ui/comet/styles.module.scss | 36 ++++-- .../src/components/ui/data-grid/component.tsx | 50 ++++++++ .../components/DataGridHeader/component.tsx | 62 +++++++++ .../components/DataGridHeader/index.ts} | 0 .../DataGridHeader/styles.module.scss | 58 +++++++++ .../components/DataGridHeader/types.ts | 7 ++ .../components/DataGridRow/component.tsx | 32 +++++ .../data-grid/components/DataGridRow/index.ts | 1 + .../components/DataGridRow/styles.module.scss | 18 +++ .../data-grid/components/DataGridRow/types.ts | 8 ++ .../ui/data-grid/components/index.ts | 2 + front/src/components/ui/data-grid/index.ts | 2 + front/src/components/ui/data-grid/preview.tsx | 28 +++++ front/src/components/ui/data-grid/types.ts | 23 ++++ .../components/ui/date-input/component.tsx | 50 ++++---- front/src/components/ui/date-input/types.ts | 2 +- .../components/ui/file-uploader/component.tsx | 78 ++++++++++++ .../src/components/ui/file-uploader/index.ts | 1 + .../components/ui/file-uploader/preview.tsx | 14 +++ .../ui/file-uploader/style.module.scss | 68 ++++++++++ .../src/components/ui/file-uploader/types.ts | 11 ++ .../ui/icon-button/styles.module.scss | 23 ++-- .../ui/image-file-manager/component.tsx | 40 ++++++ .../components/ui/image-file-manager/index.ts | 1 + .../ui/image-file-manager/preview.tsx | 31 +++++ .../ui/image-file-manager/styles.module.scss | 20 +++ .../components/ui/image-file-manager/types.ts | 9 ++ .../components/ui/image-viewer/component.tsx | 32 +++++ front/src/components/ui/image-viewer/index.ts | 1 + .../ui/image-viewer/styles.module.scss | 56 +++++++++ front/src/components/ui/image-viewer/types.ts | 7 ++ front/src/components/ui/index.tsx | 5 + .../components/ui/input/styles.module.scss | 24 ++-- .../components/ui/number-input/component.tsx | 38 ++++++ .../src/components/ui/number-input/index.tsx | 1 + .../components/ui/number-input/preview.tsx | 39 ++++++ front/src/components/ui/number-input/types.ts | 8 ++ .../ui/paragraph/styles.module.scss | 10 +- front/src/components/ui/popover/component.tsx | 32 ++++- front/src/components/ui/popover/index.ts | 2 - front/src/components/ui/popover/index.tsx | 1 + .../components/ui/popover/styles.module.scss | 3 +- front/src/components/ui/popover/types.ts | 15 ++- front/src/components/ui/popover/utils.ts | 92 ++++++++++++++ .../ui/radio-group/styles.module.scss | 10 +- .../components/ui/radio/styles.module.scss | 32 +++-- front/src/components/ui/select/component.tsx | 25 +--- .../components/ui/select/styles.module.scss | 24 ++-- front/src/components/ui/select/types.ts | 2 + .../src/components/ui/span/styles.module.scss | 10 +- .../src/components/ui/text-area/component.tsx | 20 +++ front/src/components/ui/text-area/index.ts | 1 + front/src/components/ui/text-area/preview.tsx | 14 +++ .../ui/text-area/styles.module.scss | 44 +++++++ front/src/components/ui/text-area/types.ts | 7 ++ .../src/components/ui/text-input/preview.tsx | 6 +- front/src/components/ui/text-input/types.ts | 2 +- front/src/components/ux/header/component.tsx | 5 + front/src/components/ux/index.tsx | 8 +- front/src/components/ux/sidebar/component.tsx | 23 ++++ front/src/components/ux/sidebar/constants.ts | 6 + front/src/components/ux/sidebar/index.ts | 1 + .../components/ux/sidebar/styles.module.scss | 19 +++ front/src/components/ux/sidebar/types.ts | 4 + .../components/ux/sign-in-form/component.tsx | 33 +++++ .../components/ux/sign-in-form/constants.ts | 7 ++ .../src/components/ux/sign-in-form/index.tsx | 1 + .../styles.module.scss | 16 ++- front/src/components/ux/sign-in-form/types.ts | 6 + .../ux/theme-select/styles.module.scss | 4 + .../components/ux/windmill-form/component.tsx | 98 --------------- .../components/ux/windmill-form/constants.ts | 5 - .../src/components/ux/windmill-form/index.tsx | 6 - .../src/components/ux/windmill-form/types.ts | 21 ---- .../ux/windmill-table/component.tsx | 59 --------- .../parts/windmill-table-row/component.tsx | 68 ---------- .../parts/windmill-table-row/index.tsx | 1 - .../windmill-table-row/style.module.scss | 16 --- .../parts/windmill-table-row/types.ts | 8 -- .../ux/windmill-table/styles.module.scss | 32 ----- .../src/components/ux/windmill-table/types.ts | 6 - front/src/utils/date/index.ts | 1 - front/src/utils/date/input.ts | 8 -- front/src/utils/file/general.ts | 14 +++ front/src/utils/file/index.ts | 1 + 132 files changed, 1735 insertions(+), 775 deletions(-) delete mode 100644 front/public/images/svg/plus.svg create mode 100644 front/public/images/svg/upload.svg delete mode 100644 front/src/api/floris/constants.ts delete mode 100644 front/src/api/floris/index.ts delete mode 100644 front/src/api/floris/service.ts delete mode 100644 front/src/api/floris/types.ts delete mode 100644 front/src/api/floris/utils.ts delete mode 100644 front/src/api/index.tsx create mode 100644 front/src/components/_func.scss delete mode 100644 front/src/components/_mixins.scss create mode 100644 front/src/components/pages/form-page/component.tsx create mode 100644 front/src/components/pages/form-page/index.ts create mode 100644 front/src/components/pages/form-page/styles.module.scss delete mode 100644 front/src/components/ui/animation/ripple/parts/ripple-wave/component.tsx delete mode 100644 front/src/components/ui/animation/ripple/parts/ripple-wave/index.ts delete mode 100644 front/src/components/ui/animation/ripple/parts/ripple-wave/style.module.scss delete mode 100644 front/src/components/ui/animation/ripple/parts/ripple-wave/types.ts delete mode 100644 front/src/components/ui/animation/ripple/types.ts create mode 100644 front/src/components/ui/autocomplete/component.tsx create mode 100644 front/src/components/ui/autocomplete/index.ts create mode 100644 front/src/components/ui/autocomplete/preview.tsx create mode 100644 front/src/components/ui/autocomplete/styles.module.scss create mode 100644 front/src/components/ui/autocomplete/types.ts rename front/src/components/ui/calendar/{parts => components}/calendar-days/component.tsx (98%) rename front/src/components/ui/calendar/{parts => components}/calendar-days/constants.ts (100%) rename front/src/components/ui/calendar/{parts => components}/calendar-days/index.ts (100%) rename front/src/components/ui/calendar/{parts => components}/calendar-days/styles.module.scss (87%) rename front/src/components/ui/calendar/{parts => components}/calendar-days/types.ts (92%) rename front/src/components/ui/calendar/{parts => components}/calendar-days/utils.ts (77%) rename front/src/components/ui/calendar/{parts => components}/index.ts (100%) create mode 100644 front/src/components/ui/data-grid/component.tsx create mode 100644 front/src/components/ui/data-grid/components/DataGridHeader/component.tsx rename front/src/components/{ux/windmill-table/index.tsx => ui/data-grid/components/DataGridHeader/index.ts} (100%) create mode 100644 front/src/components/ui/data-grid/components/DataGridHeader/styles.module.scss create mode 100644 front/src/components/ui/data-grid/components/DataGridHeader/types.ts create mode 100644 front/src/components/ui/data-grid/components/DataGridRow/component.tsx create mode 100644 front/src/components/ui/data-grid/components/DataGridRow/index.ts create mode 100644 front/src/components/ui/data-grid/components/DataGridRow/styles.module.scss create mode 100644 front/src/components/ui/data-grid/components/DataGridRow/types.ts create mode 100644 front/src/components/ui/data-grid/components/index.ts create mode 100644 front/src/components/ui/data-grid/index.ts create mode 100644 front/src/components/ui/data-grid/preview.tsx create mode 100644 front/src/components/ui/data-grid/types.ts create mode 100644 front/src/components/ui/file-uploader/component.tsx create mode 100644 front/src/components/ui/file-uploader/index.ts create mode 100644 front/src/components/ui/file-uploader/preview.tsx create mode 100644 front/src/components/ui/file-uploader/style.module.scss create mode 100644 front/src/components/ui/file-uploader/types.ts create mode 100644 front/src/components/ui/image-file-manager/component.tsx create mode 100644 front/src/components/ui/image-file-manager/index.ts create mode 100644 front/src/components/ui/image-file-manager/preview.tsx create mode 100644 front/src/components/ui/image-file-manager/styles.module.scss create mode 100644 front/src/components/ui/image-file-manager/types.ts create mode 100644 front/src/components/ui/image-viewer/component.tsx create mode 100644 front/src/components/ui/image-viewer/index.ts create mode 100644 front/src/components/ui/image-viewer/styles.module.scss create mode 100644 front/src/components/ui/image-viewer/types.ts create mode 100644 front/src/components/ui/number-input/component.tsx create mode 100644 front/src/components/ui/number-input/index.tsx create mode 100644 front/src/components/ui/number-input/preview.tsx create mode 100644 front/src/components/ui/number-input/types.ts delete mode 100644 front/src/components/ui/popover/index.ts create mode 100644 front/src/components/ui/popover/index.tsx create mode 100644 front/src/components/ui/popover/utils.ts create mode 100644 front/src/components/ui/text-area/component.tsx create mode 100644 front/src/components/ui/text-area/index.ts create mode 100644 front/src/components/ui/text-area/preview.tsx create mode 100644 front/src/components/ui/text-area/styles.module.scss create mode 100644 front/src/components/ui/text-area/types.ts create mode 100644 front/src/components/ux/sidebar/component.tsx create mode 100644 front/src/components/ux/sidebar/constants.ts create mode 100644 front/src/components/ux/sidebar/index.ts create mode 100644 front/src/components/ux/sidebar/styles.module.scss create mode 100644 front/src/components/ux/sidebar/types.ts create mode 100644 front/src/components/ux/sign-in-form/component.tsx create mode 100644 front/src/components/ux/sign-in-form/constants.ts create mode 100644 front/src/components/ux/sign-in-form/index.tsx rename front/src/components/ux/{windmill-form => sign-in-form}/styles.module.scss (59%) create mode 100644 front/src/components/ux/sign-in-form/types.ts create mode 100644 front/src/components/ux/theme-select/styles.module.scss delete mode 100644 front/src/components/ux/windmill-form/component.tsx delete mode 100644 front/src/components/ux/windmill-form/constants.ts delete mode 100644 front/src/components/ux/windmill-form/index.tsx delete mode 100644 front/src/components/ux/windmill-form/types.ts delete mode 100644 front/src/components/ux/windmill-table/component.tsx delete mode 100644 front/src/components/ux/windmill-table/parts/windmill-table-row/component.tsx delete mode 100644 front/src/components/ux/windmill-table/parts/windmill-table-row/index.tsx delete mode 100644 front/src/components/ux/windmill-table/parts/windmill-table-row/style.module.scss delete mode 100644 front/src/components/ux/windmill-table/parts/windmill-table-row/types.ts delete mode 100644 front/src/components/ux/windmill-table/styles.module.scss delete mode 100644 front/src/components/ux/windmill-table/types.ts delete mode 100644 front/src/utils/date/index.ts delete mode 100644 front/src/utils/date/input.ts create mode 100644 front/src/utils/file/general.ts create mode 100644 front/src/utils/file/index.ts diff --git a/front/public/images/svg/plus.svg b/front/public/images/svg/plus.svg deleted file mode 100644 index 9940452..0000000 --- a/front/public/images/svg/plus.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/front/public/images/svg/upload.svg b/front/public/images/svg/upload.svg new file mode 100644 index 0000000..c712776 --- /dev/null +++ b/front/public/images/svg/upload.svg @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/front/src/api/floris/constants.ts b/front/src/api/floris/constants.ts deleted file mode 100644 index fd812c1..0000000 --- a/front/src/api/floris/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -// export const BASE_URL = 'http://localhost:8000/api'; -export const BASE_URL = 'http://192.168.1.110:8000/api'; diff --git a/front/src/api/floris/index.ts b/front/src/api/floris/index.ts deleted file mode 100644 index e34a0bf..0000000 --- a/front/src/api/floris/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { downloadImage, getWindmillData } from './service'; diff --git a/front/src/api/floris/service.ts b/front/src/api/floris/service.ts deleted file mode 100644 index 6e18fa6..0000000 --- a/front/src/api/floris/service.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { WindmillFormStore } from '@components/ux/windmill-form'; - -import { BASE_URL } from './constants'; -import { GetWindmillDataRes } from './types'; -import { getWindmillDataParams } from './utils'; - -export const getWindmillData = async (store: Partial) => { - const params = getWindmillDataParams(store); - const url = `${BASE_URL}/floris/get_windmill_data?${params}`; - const init: RequestInit = { - method: 'GET', - }; - const res: Response = await fetch(url, init); - const data: GetWindmillDataRes = await res.json(); - return data; -}; - -export const downloadImage = async (imageName: string) => { - const url = `${BASE_URL}/floris/download_image/${imageName}`; - const init: RequestInit = { - method: 'GET', - }; - const res: Response = await fetch(url, init); - const data = await res.blob(); - return data; -}; diff --git a/front/src/api/floris/types.ts b/front/src/api/floris/types.ts deleted file mode 100644 index ff20ef1..0000000 --- a/front/src/api/floris/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type GetWindmillDataRes = { - file_name: string; - data: number[]; -}; diff --git a/front/src/api/floris/utils.ts b/front/src/api/floris/utils.ts deleted file mode 100644 index 7e7650c..0000000 --- a/front/src/api/floris/utils.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { WindmillFormStore } from '@components/ux/windmill-form'; - -export const getWindmillDataParams = (store: Partial) => { - const layoutX = store.windmills?.map((row) => `layout_x=${row.x}`).join('&'); - const layoutY = store.windmills?.map((row) => `layout_y=${row.y}`).join('&'); - const dateStart = `date_start=${store.dateFrom?.substring(0, 10)}`; - const dateEnd = `date_end=${store.dateTo?.substring(0, 10)}`; - return `${layoutX}&${layoutY}&${dateStart}&${dateEnd}`; -}; diff --git a/front/src/api/index.tsx b/front/src/api/index.tsx deleted file mode 100644 index 4b6c453..0000000 --- a/front/src/api/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './floris'; diff --git a/front/src/components/_func.scss b/front/src/components/_func.scss new file mode 100644 index 0000000..e1215e9 --- /dev/null +++ b/front/src/components/_func.scss @@ -0,0 +1,19 @@ +@function scale($values, $factor) { + @if type-of($values) == 'list' { + $m-values: (); + @each $value in $values { + $m-values: append($m-values, $value * $factor); + } + @return $m-values; + } @else { + @return nth($values, 1) * $factor; + } +} + +@function m($values) { + @return scale($values, 1.25); +} + +@function l($values) { + @return scale($values, 1.5); +} diff --git a/front/src/components/_mixins.scss b/front/src/components/_mixins.scss deleted file mode 100644 index e69de29..0000000 diff --git a/front/src/components/app/_theme.scss b/front/src/components/app/_theme.scss index 4c375d7..61fbc9a 100644 --- a/front/src/components/app/_theme.scss +++ b/front/src/components/app/_theme.scss @@ -4,12 +4,12 @@ --clr-primary: #4176FF; --clr-primary-o50: #3865DA80; --clr-primary-hover: #638FFF; - --clr-primary-active: #3D68D7; + --clr-primary-disabled: #3D68D7; --clr-on-primary: #FFFFFF; --clr-secondary: #EAEAEA; --clr-secondary-hover: #EFEFEF; - --clr-secondary-active: #E1E1E1; + --clr-secondary-disabled: #E1E1E1; --clr-on-secondary: #0D0D0D; --clr-layer-100: #EBEEF0; @@ -36,12 +36,12 @@ --clr-primary: #3865DA; --clr-primary-o50: #3865DA80; --clr-primary-hover: #4073F7; - --clr-primary-active: #2A4DA7; + --clr-primary-disabled: #2A4DA7; --clr-on-primary: #FFFFFF; --clr-secondary: #3F3F3F; --clr-secondary-hover: #4D4D4D; - --clr-secondary-active: #323232; + --clr-secondary-disabled: #323232; --clr-on-secondary: #FFFFFF; --clr-layer-100: #1B1B1B; diff --git a/front/src/components/app/index.tsx b/front/src/components/app/index.tsx index fec961b..75b826d 100644 --- a/front/src/components/app/index.tsx +++ b/front/src/components/app/index.tsx @@ -2,7 +2,7 @@ import './styles.scss'; import '@public/fonts/styles.css'; import { MainLayout } from '@components/layouts'; -import { HomePage } from '@components/pages'; +import { FormPage, HomePage } from '@components/pages'; import React from 'react'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; @@ -12,6 +12,7 @@ function App() { }> } /> + } /> diff --git a/front/src/components/layouts/main-layout/index.tsx b/front/src/components/layouts/main-layout/index.tsx index e6f2193..c438cde 100644 --- a/front/src/components/layouts/main-layout/index.tsx +++ b/front/src/components/layouts/main-layout/index.tsx @@ -1,4 +1,4 @@ -import { Header } from '@components/ux'; +import { Sidebar } from '@components/ux'; import React from 'react'; import { Outlet } from 'react-router-dom'; @@ -7,7 +7,7 @@ import styles from './styles.module.scss'; function MainLayout() { return (
-
+
diff --git a/front/src/components/layouts/main-layout/styles.module.scss b/front/src/components/layouts/main-layout/styles.module.scss index 64e65a6..1b778a6 100644 --- a/front/src/components/layouts/main-layout/styles.module.scss +++ b/front/src/components/layouts/main-layout/styles.module.scss @@ -2,9 +2,8 @@ display: grid; height: 100%; grid-template: - 'header' auto - 'main' minmax(0, 1fr) - / minmax(0, 1fr); + 'sidebar main' minmax(0, 1fr) + / auto minmax(0, 1fr); } .main { diff --git a/front/src/components/pages/form-page/component.tsx b/front/src/components/pages/form-page/component.tsx new file mode 100644 index 0000000..ed95963 --- /dev/null +++ b/front/src/components/pages/form-page/component.tsx @@ -0,0 +1,12 @@ +import { SignInForm } from '@components/ux'; +import React from 'react'; + +import styles from './styles.module.scss'; + +export function FormPage() { + return ( +
+ +
+ ); +} diff --git a/front/src/components/pages/form-page/index.ts b/front/src/components/pages/form-page/index.ts new file mode 100644 index 0000000..e136094 --- /dev/null +++ b/front/src/components/pages/form-page/index.ts @@ -0,0 +1 @@ +export { FormPage } from './component'; diff --git a/front/src/components/pages/form-page/styles.module.scss b/front/src/components/pages/form-page/styles.module.scss new file mode 100644 index 0000000..00762eb --- /dev/null +++ b/front/src/components/pages/form-page/styles.module.scss @@ -0,0 +1,11 @@ +.about { + display: grid; + padding: 20px; + grid-template: + '. form .' auto + / auto minmax(0, 380px) auto; +} + +.form { + grid-area: form; +} diff --git a/front/src/components/pages/home-page/component.tsx b/front/src/components/pages/home-page/component.tsx index d1911b7..2527f5b 100644 --- a/front/src/components/pages/home-page/component.tsx +++ b/front/src/components/pages/home-page/component.tsx @@ -1,40 +1,35 @@ -import { Heading, Paragraph } from '@components/ui'; -import { WindmillForm } from '@components/ux'; -import { WindmillFormResponse } from '@components/ux/windmill-form'; -import React, { useState } from 'react'; +import { AutocompletePreview } from '@components/ui/autocomplete'; +import { ButtonPreview } from '@components/ui/button'; +import { CheckboxGroupPreview } from '@components/ui/checkbox-group'; +import { DataGridPreview } from '@components/ui/data-grid'; +import { DateInputPreview } from '@components/ui/date-input'; +import { ImageFileManagerPreview } from '@components/ui/image-file-manager/preview'; +import { NumberInputPreview } from '@components/ui/number-input/preview'; +import { PasswordInputPreview } from '@components/ui/password-input'; +import { RadioGroupPreview } from '@components/ui/radio-group'; +import { SelectPreview } from '@components/ui/select'; +import { TextAreaPreview } from '@components/ui/text-area/preview'; +import { TextInputPreview } from '@components/ui/text-input'; +import React from 'react'; import styles from './styles.module.scss'; export function HomePage() { - const [result, setResult] = useState(null); - const [error, setError] = useState(null); - - const handleFormSuccess = (response: WindmillFormResponse) => { - setResult(response); - setError(null); - }; - - const handleFormFail = (message: string) => { - setError(message); - setResult(null); - }; - return ( -
-
- -
-
- Result - {result && ( - <> -
{result.power.join(' ')}
-
- {result.image && Image} -
- - )} - {error && {error}} +
+
+ + + + + + + + + + + +
); diff --git a/front/src/components/pages/home-page/styles.module.scss b/front/src/components/pages/home-page/styles.module.scss index 16eb3b3..5a8812c 100644 --- a/front/src/components/pages/home-page/styles.module.scss +++ b/front/src/components/pages/home-page/styles.module.scss @@ -1,41 +1,14 @@ -.page { +.home { display: grid; - padding: 20px; - gap: 20px; grid-template: - '. form result .' auto - / auto minmax(0, 380px) minmax(0, 700px) auto; + '. content .' auto + / auto minmax(0, 1000px) auto; } -.wrapperForm { - grid-area: form; -} - -.result { +.content { display: flex; flex-direction: column; - padding: 20px; - border-radius: 10px; - background-color: var(--clr-layer-200); - box-shadow: 0px 1px 2px var(--clr-shadow-100); - gap: 20px; - grid-area: result; -} - -.image { - width: 100%; - - img { - max-width: 100%; - border-radius: 10px; - } -} - -@media (width <= 1000px) { - .page { - grid-template: - 'form' auto - 'result' auto - / 1fr; - } + padding: 20px 20px 60px 20px; + gap: 30px; + grid-area: content; } diff --git a/front/src/components/pages/index.tsx b/front/src/components/pages/index.tsx index 1ae4201..449826b 100644 --- a/front/src/components/pages/index.tsx +++ b/front/src/components/pages/index.tsx @@ -1 +1,2 @@ +export { FormPage } from './form-page'; export { HomePage } from './home-page'; diff --git a/front/src/components/ui/animation/ripple/component.tsx b/front/src/components/ui/animation/ripple/component.tsx index 7abd6f4..0faee3a 100644 --- a/front/src/components/ui/animation/ripple/component.tsx +++ b/front/src/components/ui/animation/ripple/component.tsx @@ -1,68 +1,65 @@ -import React, { - ForwardedRef, - forwardRef, - useImperativeHandle, - useRef, - useState, -} from 'react'; +import clsx from 'clsx'; +import React, { useRef } from 'react'; -import { RippleWave } from './parts/ripple-wave'; import styles from './styles.module.scss'; -import { RippleProps } from './types'; import { calcRippleWaveStyle } from './utils'; -export function RippleInner( - props: RippleProps, - ref: ForwardedRef, -) { +export function Ripple() { const rippleRef = useRef(null); - const [waves, setWaves] = useState([]); - const [isTouch, setIsTouch] = useState(false); - useImperativeHandle(ref, () => rippleRef.current, []); + const clean = () => { + document.removeEventListener('touchend', clean); + document.removeEventListener('mouseup', clean); + if (!rippleRef.current) { + return; + } + const { lastChild: wave } = rippleRef.current; + if (!wave || !(wave instanceof HTMLElement)) { + return; + } + wave.dataset.isMouseReleased = 'true'; + if (wave.dataset.isAnimationComplete) { + wave.classList.replace(styles.visible, styles.invisible); + } + }; - const handleWaveOnDone = () => { - setWaves((prev) => prev.slice(1)); + const handleAnimationEnd = (event: AnimationEvent) => { + const { target: wave, animationName } = event; + if (!(wave instanceof HTMLElement)) { + return; + } + if (animationName === styles.fadein) { + wave.dataset.isAnimationComplete = 'true'; + if (wave.dataset.isMouseReleased) { + wave.classList.replace(styles.visible, styles.invisible); + } + } else { + wave.remove(); + } }; const addWave = (x: number, y: number) => { + const wave = document.createElement('div'); const style = calcRippleWaveStyle(x, y, rippleRef.current); - const wave = ( - - ); - setWaves([...waves, wave]); + Object.assign(wave.style, style); + wave.className = clsx(styles.wave, styles.visible); + wave.addEventListener('animationend', handleAnimationEnd); + rippleRef.current.appendChild(wave); + document.addEventListener('touchend', clean); + document.addEventListener('mouseup', clean); }; - const handleMouseDown = (event: React.MouseEvent) => { - if (isTouch) { - return; - } + const handlePointerDown = (event: React.MouseEvent) => { + event.stopPropagation(); const { pageX, pageY } = event; addWave(pageX, pageY); }; - const handleTouchStart = (event: React.TouchEvent) => { - setIsTouch(true); - const { touches, changedTouches } = event; - const { pageX, pageY } = touches[0] ?? changedTouches[0]; - addWave(pageX, pageY); - }; - return (
- {waves} -
+ ref={rippleRef} + onPointerDown={handlePointerDown} + /> ); } - -export const Ripple = forwardRef(RippleInner); diff --git a/front/src/components/ui/animation/ripple/parts/ripple-wave/component.tsx b/front/src/components/ui/animation/ripple/parts/ripple-wave/component.tsx deleted file mode 100644 index 1227520..0000000 --- a/front/src/components/ui/animation/ripple/parts/ripple-wave/component.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import clsx from 'clsx'; -import React, { useEffect, useState } from 'react'; - -import styles from './style.module.scss'; -import { RippleWaveProps } from './types'; - -export function RippleWave({ style, onDone }: RippleWaveProps) { - const [isMouseUp, setIsMouseUp] = useState(false); - const [isAnimationEnd, setIsAnimationEnd] = useState(false); - - useEffect(() => { - const mouseUpListener = () => setIsMouseUp(true); - document.addEventListener('mouseup', mouseUpListener, { once: true }); - document.addEventListener('touchend', mouseUpListener, { once: true }); - }, []); - - const visible = !isMouseUp || !isAnimationEnd; - - const className = clsx( - styles.wave, - visible ? styles.visible : styles.invisible, - ); - - const handleAnimationEnd = (event: React.AnimationEvent) => { - if (event.animationName === styles.fadein) { - setIsAnimationEnd(true); - } else { - onDone(); - } - }; - - return ( -
- ); -} diff --git a/front/src/components/ui/animation/ripple/parts/ripple-wave/index.ts b/front/src/components/ui/animation/ripple/parts/ripple-wave/index.ts deleted file mode 100644 index 74e0c72..0000000 --- a/front/src/components/ui/animation/ripple/parts/ripple-wave/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { RippleWave } from './component'; diff --git a/front/src/components/ui/animation/ripple/parts/ripple-wave/style.module.scss b/front/src/components/ui/animation/ripple/parts/ripple-wave/style.module.scss deleted file mode 100644 index a9319f5..0000000 --- a/front/src/components/ui/animation/ripple/parts/ripple-wave/style.module.scss +++ /dev/null @@ -1,33 +0,0 @@ -.wave { - position: absolute; - border-radius: 100%; - background-color: var(--clr-ripple); -} - -.visible { - animation: fadein 0.3s linear; -} - -.invisible { - animation: fadeout 0.3s linear forwards; -} - -@keyframes fadein { - from { - opacity: 0; - scale: 0; - } - to { - opacity: 1; - scale: 1; - } -} - -@keyframes fadeout { - from { - opacity: 1; - } - to { - opacity: 0; - } -} diff --git a/front/src/components/ui/animation/ripple/parts/ripple-wave/types.ts b/front/src/components/ui/animation/ripple/parts/ripple-wave/types.ts deleted file mode 100644 index 3588e4d..0000000 --- a/front/src/components/ui/animation/ripple/parts/ripple-wave/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { CSSProperties } from 'react'; - -export type RippleWaveProps = { - style: CSSProperties; - onDone: () => void; -}; diff --git a/front/src/components/ui/animation/ripple/styles.module.scss b/front/src/components/ui/animation/ripple/styles.module.scss index 98abe03..800e368 100644 --- a/front/src/components/ui/animation/ripple/styles.module.scss +++ b/front/src/components/ui/animation/ripple/styles.module.scss @@ -5,3 +5,37 @@ width: 200%; height: 200%; } + +.wave { + position: absolute; + border-radius: 100%; + background-color: var(--clr-ripple); +} + +.visible { + animation: fadein 0.3s linear; +} + +.invisible { + animation: fadeout 0.3s linear forwards; +} + +@keyframes fadein { + from { + opacity: 0; + scale: 0; + } + to { + opacity: 1; + scale: 1; + } +} + +@keyframes fadeout { + from { + opacity: 1; + } + to { + opacity: 0; + } +} diff --git a/front/src/components/ui/animation/ripple/types.ts b/front/src/components/ui/animation/ripple/types.ts deleted file mode 100644 index af8def9..0000000 --- a/front/src/components/ui/animation/ripple/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type RippleProps = {} & React.ComponentProps<'div'>; diff --git a/front/src/components/ui/animation/ripple/utils.ts b/front/src/components/ui/animation/ripple/utils.ts index 55e9188..5dfa7a6 100644 --- a/front/src/components/ui/animation/ripple/utils.ts +++ b/front/src/components/ui/animation/ripple/utils.ts @@ -1,3 +1,4 @@ +import { px } from '@utils/css'; import { CSSProperties } from 'react'; export const calcRippleWaveStyle = ( @@ -8,7 +9,7 @@ export const calcRippleWaveStyle = ( const wrapperRect = ripple.getBoundingClientRect(); const diameter = Math.max(wrapperRect.width, wrapperRect.height); const radius = diameter / 2; - const left = x - wrapperRect.left - radius; - const top = y - wrapperRect.top - radius; - return { left, top, width: diameter, height: diameter }; + const left = px(x - wrapperRect.left - radius); + const top = px(y - wrapperRect.top - radius); + return { left, top, width: px(diameter), height: px(diameter) }; }; diff --git a/front/src/components/ui/autocomplete/component.tsx b/front/src/components/ui/autocomplete/component.tsx new file mode 100644 index 0000000..4d28843 --- /dev/null +++ b/front/src/components/ui/autocomplete/component.tsx @@ -0,0 +1,119 @@ +import ArrowDownIcon from '@public/images/svg/arrow-down.svg'; +import { useMissClick } from '@utils/miss-click'; +import clsx from 'clsx'; +import React, { + ForwardedRef, + forwardRef, + useImperativeHandle, + useRef, + useState, +} from 'react'; + +import { Menu } from '../menu'; +import { Popover } from '../popover'; +import { TextInput } from '../text-input'; +import styles from './styles.module.scss'; +import { AutocompleteProps } from './types'; + +function AutocompleteInner( + { + options, + value, + getOptionKey, + getOptionLabel, + onChange, + scale = 'm', + label = {}, + name, + id, + }: Omit, 'ref'>, + ref: ForwardedRef, +) { + const autocompleteRef = useRef(null); + const menuRef = useRef(null); + const inputWrapperRef = useRef(null); + const [menuVisible, setMenuVisible] = useState(false); + const [text, setText] = useState(''); + + useImperativeHandle(ref, () => autocompleteRef.current, []); + + useMissClick( + [autocompleteRef, menuRef], + () => setMenuVisible(false), + menuVisible, + ); + + const autocompleteClassName = clsx(styles.autocomplete, styles[scale], { + [styles.menuVisible]: menuVisible, + }); + + const filteredOptions = options.filter((option) => { + const label = getOptionLabel(option).toLocaleLowerCase(); + const raw = text.trim().toLocaleLowerCase(); + return label.includes(raw); + }); + + const handleInputClick = () => { + setMenuVisible(!menuVisible); + }; + + const handleMenuSelect = (option: T) => { + setMenuVisible(false); + onChange?.(option); + setText(''); + }; + + const handleInputChange = (event: React.ChangeEvent) => { + const { value } = event.target; + setText(value); + const option = options.find((option) => { + const label = getOptionLabel(option).toLocaleLowerCase(); + const raw = value.toLocaleLowerCase(); + return label === raw; + }); + onChange?.(option ?? null); + }; + + return ( +
+ + +
+ } + /> + + +
+ } + /> +
+ ); +} + +export const Autocomplete = forwardRef(AutocompleteInner) as ( + props: AutocompleteProps, +) => ReturnType; diff --git a/front/src/components/ui/autocomplete/index.ts b/front/src/components/ui/autocomplete/index.ts new file mode 100644 index 0000000..5b99bce --- /dev/null +++ b/front/src/components/ui/autocomplete/index.ts @@ -0,0 +1,3 @@ +export { Autocomplete } from './component'; +export { AutocompletePreview } from './preview'; +export { type AutocompleteProps } from './types'; diff --git a/front/src/components/ui/autocomplete/preview.tsx b/front/src/components/ui/autocomplete/preview.tsx new file mode 100644 index 0000000..192a8ef --- /dev/null +++ b/front/src/components/ui/autocomplete/preview.tsx @@ -0,0 +1,44 @@ +import { PreviewArticle } from '@components/ui/preview'; +import React, { useState } from 'react'; + +import { Autocomplete } from './component'; + +export function AutocompletePreview() { + const [selectValue, setSelectValue] = useState(); + const options = ['Orange', 'Banana', 'Apple', 'Avocado']; + + return ( + + o} + getOptionLabel={(o) => o} + label={{ text: 'Select your favorite fruit' }} + scale="s" + value={selectValue} + onChange={(o) => setSelectValue(o)} + name="fruit" + /> + o} + getOptionLabel={(o) => o} + label={{ text: 'Select your favorite fruit' }} + scale="m" + value={selectValue} + onChange={(o) => setSelectValue(o)} + name="fruit" + /> + o} + getOptionLabel={(o) => o} + label={{ text: 'Select your favorite fruit' }} + scale="l" + value={selectValue} + onChange={(o) => setSelectValue(o)} + name="fruit" + /> + + ); +} diff --git a/front/src/components/ui/autocomplete/styles.module.scss b/front/src/components/ui/autocomplete/styles.module.scss new file mode 100644 index 0000000..f0a648d --- /dev/null +++ b/front/src/components/ui/autocomplete/styles.module.scss @@ -0,0 +1,62 @@ +@use '@components/func.scss' as f; + +.autocomplete { + position: relative; + width: fit-content; +} + +.icon { + fill: var(--clr-text-100); + transition: all var(--td-100) ease-in-out; +} + +.fade { + position: absolute; + z-index: 1; +} + +.menuVisible { + .icon { + rotate: 180deg; + } +} + +.menuWrapper { + padding: 5px 0; +} + +$padding-right: 7px; +$size: 10px; + +.s { + .iconBox { + padding-right: $padding-right; + } + + .icon { + width: $size; + height: $size; + } +} + +.m { + .iconBox { + padding-right: f.m($padding-right); + } + + .icon { + width: f.m($size); + height: f.m($size); + } +} + +.l { + .iconBox { + padding-right: f.l($padding-right); + } + + .icon { + width: f.l($size); + height: f.l($size); + } +} diff --git a/front/src/components/ui/autocomplete/types.ts b/front/src/components/ui/autocomplete/types.ts new file mode 100644 index 0000000..74c5982 --- /dev/null +++ b/front/src/components/ui/autocomplete/types.ts @@ -0,0 +1,14 @@ +import { LabelProps } from '../label'; +import { Scale } from '../types'; + +export type AutocompleteProps = { + options: T[]; + value?: T; + getOptionKey: (option: T) => React.Key; + getOptionLabel: (option: T) => string; + onChange?: (option: T) => void; + scale?: Scale; + label?: LabelProps; + name?: string; + id?: string; +} & Omit, 'onChange'>; diff --git a/front/src/components/ui/button/component.tsx b/front/src/components/ui/button/component.tsx index 5344602..c67973c 100644 --- a/front/src/components/ui/button/component.tsx +++ b/front/src/components/ui/button/component.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx'; import React from 'react'; -import { Ripple } from '../animation'; +import { Ripple } from '../animation/ripple/component'; import { Comet } from '../comet'; import { RawButton } from '../raw'; import { COMET_VARIANT_MAP } from './constants'; diff --git a/front/src/components/ui/button/styles.module.scss b/front/src/components/ui/button/styles.module.scss index 4069c35..373a99b 100644 --- a/front/src/components/ui/button/styles.module.scss +++ b/front/src/components/ui/button/styles.module.scss @@ -1,3 +1,5 @@ +@use '@components/func.scss' as f; + .button { position: relative; overflow: hidden; @@ -40,7 +42,7 @@ } &.pending { - background-color: var(--clr-primary-active); + background-color: var(--clr-primary-disabled); } } @@ -53,24 +55,28 @@ } &.pending { - background-color: var(--clr-secondary-active); + background-color: var(--clr-secondary-disabled); } } +$padding: 10px 16px; +$border-radius: 8px; +$font-size: 12px; + .s { - padding: 10px 16px; - border-radius: 8px; - font-size: 12px; + padding: $padding; + border-radius: $border-radius; + font-size: $font-size; } .m { - padding: 14px 20px; - border-radius: 10px; - font-size: 16px; + padding: f.m($padding); + border-radius: f.m($border-radius); + font-size: f.m($font-size); } .l { - padding: 18px 24px; - border-radius: 12px; - font-size: 20px; + padding: f.l($padding); + border-radius: f.l($border-radius); + font-size: f.l($font-size); } diff --git a/front/src/components/ui/calendar/component.tsx b/front/src/components/ui/calendar/component.tsx index 33e5451..a44ef64 100644 --- a/front/src/components/ui/calendar/component.tsx +++ b/front/src/components/ui/calendar/component.tsx @@ -1,6 +1,6 @@ import React, { ForwardedRef, forwardRef, useEffect, useState } from 'react'; -import { CalendarDays } from './parts'; +import { CalendarDays } from './components'; import { CalendarProps } from './types'; function CalendarInner( diff --git a/front/src/components/ui/calendar/parts/calendar-days/component.tsx b/front/src/components/ui/calendar/components/calendar-days/component.tsx similarity index 98% rename from front/src/components/ui/calendar/parts/calendar-days/component.tsx rename to front/src/components/ui/calendar/components/calendar-days/component.tsx index da2bc54..7c3b559 100644 --- a/front/src/components/ui/calendar/parts/calendar-days/component.tsx +++ b/front/src/components/ui/calendar/components/calendar-days/component.tsx @@ -33,7 +33,7 @@ export function CalendarDays({ }, [date, min, max]); const handleChange = (newValue: string) => { - onChange?.(newValue); + onChange(newValue); }; return ( diff --git a/front/src/components/ui/calendar/parts/calendar-days/constants.ts b/front/src/components/ui/calendar/components/calendar-days/constants.ts similarity index 100% rename from front/src/components/ui/calendar/parts/calendar-days/constants.ts rename to front/src/components/ui/calendar/components/calendar-days/constants.ts diff --git a/front/src/components/ui/calendar/parts/calendar-days/index.ts b/front/src/components/ui/calendar/components/calendar-days/index.ts similarity index 100% rename from front/src/components/ui/calendar/parts/calendar-days/index.ts rename to front/src/components/ui/calendar/components/calendar-days/index.ts diff --git a/front/src/components/ui/calendar/parts/calendar-days/styles.module.scss b/front/src/components/ui/calendar/components/calendar-days/styles.module.scss similarity index 87% rename from front/src/components/ui/calendar/parts/calendar-days/styles.module.scss rename to front/src/components/ui/calendar/components/calendar-days/styles.module.scss index 46dd70c..652a3b6 100644 --- a/front/src/components/ui/calendar/parts/calendar-days/styles.module.scss +++ b/front/src/components/ui/calendar/components/calendar-days/styles.module.scss @@ -40,18 +40,11 @@ justify-content: center; border-radius: 10px; color: var(--clr-text-100); + cursor: pointer; transition: all var(--td-100) ease-in-out; - &:not(:disabled) { - cursor: pointer; - - &:hover { - background-color: var(--clr-layer-300-hover); - } - } - - &:disabled { - color: var(--clr-text-100); + &:hover { + background-color: var(--clr-layer-300-hover); } } diff --git a/front/src/components/ui/calendar/parts/calendar-days/types.ts b/front/src/components/ui/calendar/components/calendar-days/types.ts similarity index 92% rename from front/src/components/ui/calendar/parts/calendar-days/types.ts rename to front/src/components/ui/calendar/components/calendar-days/types.ts index ccd3598..7f6d27f 100644 --- a/front/src/components/ui/calendar/parts/calendar-days/types.ts +++ b/front/src/components/ui/calendar/components/calendar-days/types.ts @@ -9,7 +9,7 @@ export type CalendarDay = { export type CalendarDaysProps = { value?: string; - onChange?: (value: string) => void; + onChange: (value: string) => void; min: Date | null; max: Date | null; date: Date; diff --git a/front/src/components/ui/calendar/parts/calendar-days/utils.ts b/front/src/components/ui/calendar/components/calendar-days/utils.ts similarity index 77% rename from front/src/components/ui/calendar/parts/calendar-days/utils.ts rename to front/src/components/ui/calendar/components/calendar-days/utils.ts index 7acda21..1eb333e 100644 --- a/front/src/components/ui/calendar/parts/calendar-days/utils.ts +++ b/front/src/components/ui/calendar/components/calendar-days/utils.ts @@ -1,11 +1,18 @@ -import { dateToInputString } from '@utils/date'; - import { CalendarDay, GetCalendarDaysParams } from './types'; const addDays = (date: Date, days: number) => { date.setDate(date.getDate() + days); }; +function dateToInputString(date: Date) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}`; +} + const daysAreEqual = (date1: Date, date2: Date) => { return ( date1.getDate() === date2.getDate() && diff --git a/front/src/components/ui/calendar/parts/index.ts b/front/src/components/ui/calendar/components/index.ts similarity index 100% rename from front/src/components/ui/calendar/parts/index.ts rename to front/src/components/ui/calendar/components/index.ts diff --git a/front/src/components/ui/calendar/types.ts b/front/src/components/ui/calendar/types.ts index 107f219..ea2b469 100644 --- a/front/src/components/ui/calendar/types.ts +++ b/front/src/components/ui/calendar/types.ts @@ -1,6 +1,6 @@ export type CalendarProps = { value?: string; - onChange?: (value: string) => void; + onChange: (value: string) => void; min: Date | null; max: Date | null; } & Omit, 'onChange'>; diff --git a/front/src/components/ui/checkbox-group/styles.module.scss b/front/src/components/ui/checkbox-group/styles.module.scss index b1533e6..2393402 100644 --- a/front/src/components/ui/checkbox-group/styles.module.scss +++ b/front/src/components/ui/checkbox-group/styles.module.scss @@ -1,22 +1,26 @@ +@use '@components/func.scss' as f; + .checkBoxGroup { display: flex; flex-direction: column; } +$margin-bottom: 4px; + .s { .label { - margin-bottom: 3px; + margin-bottom: $margin-bottom; } } .m { .label { - margin-bottom: 5px; + margin-bottom: f.m($margin-bottom); } } .l { .label { - margin-bottom: 7px; + margin-bottom: f.l($margin-bottom); } } diff --git a/front/src/components/ui/checkbox/component.tsx b/front/src/components/ui/checkbox/component.tsx index fc287ed..074113c 100644 --- a/front/src/components/ui/checkbox/component.tsx +++ b/front/src/components/ui/checkbox/component.tsx @@ -2,7 +2,7 @@ import CheckIcon from '@public/images/svg/check.svg'; import clsx from 'clsx'; import React, { ForwardedRef, forwardRef } from 'react'; -import { Ripple } from '../animation'; +import { Ripple } from '../animation/ripple/component'; import { Label, LabelProps } from '../label'; import { RawInput } from '../raw'; import styles from './styles.module.scss'; diff --git a/front/src/components/ui/checkbox/styles.module.scss b/front/src/components/ui/checkbox/styles.module.scss index 560f3c8..eb6bbf9 100644 --- a/front/src/components/ui/checkbox/styles.module.scss +++ b/front/src/components/ui/checkbox/styles.module.scss @@ -1,8 +1,11 @@ +@use '@components/func.scss' as f; + .wrapper { position: relative; overflow: hidden; border-radius: 100%; cursor: pointer; + user-select: none; &:hover { .checkbox { @@ -42,7 +45,7 @@ .checkbox { display: flex; justify-content: center; - border: 1px solid var(--clr-border-200); + border: 2px solid var(--clr-border-200); background-color: var(--clr-layer-300); box-shadow: 0px 2px 2px var(--clr-shadow-200); transition: all var(--td-100) ease-in-out; @@ -54,35 +57,40 @@ transition: all var(--td-100) ease-in-out; } +$padding-outer: 4px; +$size: 16px; +$padding-inner: 2px; +$border-radius: 5px; + .s { - padding: 3px; + padding: $padding-outer; .checkbox { - width: 16px; - height: 16px; - padding: 2px; - border-radius: 5px; + width: $size; + height: $size; + padding: $padding-inner; + border-radius: $border-radius; } } .m { - padding: 5px; + padding: f.m($padding-outer); .checkbox { - width: 20px; - height: 20px; - padding: 3px; - border-radius: 6px; + width: f.m($size); + height: f.m($size); + padding: f.m($padding-inner); + border-radius: f.m($border-radius); } } .l { - padding: 7px; + padding: f.l($padding-outer); .checkbox { - width: 24px; - height: 24px; - padding: 4px; - border-radius: 7px; + width: f.l($size); + height: f.l($size); + padding: f.l($padding-inner); + border-radius: f.l($border-radius); } } diff --git a/front/src/components/ui/comet/styles.module.scss b/front/src/components/ui/comet/styles.module.scss index 1df98da..13fef98 100644 --- a/front/src/components/ui/comet/styles.module.scss +++ b/front/src/components/ui/comet/styles.module.scss @@ -1,3 +1,5 @@ +@use '@components/func.scss' as f; + .comet { border-radius: 50%; animation: spinner-comet 1s infinite linear; @@ -9,23 +11,37 @@ } } +$size: 12px; +$offset: 1.75px; + .s { - width: 12px; - height: 12px; - mask: radial-gradient(farthest-side, #0000 calc(100% - 2px), #000 0); + width: $size; + height: $size; + mask: radial-gradient( + farthest-side, + #0000 calc(100% - $offset), + #000 0 + ); } .m { - width: 16px; - height: 16px; - mask: radial-gradient(farthest-side, #0000 calc(100% - 2.5px), #000 0); + width: f.m($size); + height: f.m($size); + mask: radial-gradient( + farthest-side, + #0000 calc(100% - f.m($offset)), + #000 0 + ); } .l { - width: 20px; - height: 20px; - mask: radial-gradient(farthest-side, #0000 calc(100% - 3px), #000 0); -} + width: f.l($size); + height: f.l($size); + mask: radial-gradient( + farthest-side, + #0000 calc(100% - f.l($offset)), + #000 0 + );} .onPrimary { background: conic-gradient(#0000 10%, var(--clr-on-primary)); diff --git a/front/src/components/ui/data-grid/component.tsx b/front/src/components/ui/data-grid/component.tsx new file mode 100644 index 0000000..ed01e2e --- /dev/null +++ b/front/src/components/ui/data-grid/component.tsx @@ -0,0 +1,50 @@ +import React, { useState } from 'react'; + +import { DataGridHeader, DataGridRow } from './components'; +import { DataGridProps } from './types'; + +export function DataGrid({ + items, + columns, + className, + ...props +}: DataGridProps) { + const [selectedRows, setSelectedRows] = useState>({}); + const [allRowsSelected, setAllRowsSelected] = useState(false); + + const handleSelectAllRows = () => { + const newSelectedRows: Record = {}; + items.forEach((_, index) => { + newSelectedRows[index] = !allRowsSelected; + }); + setSelectedRows(newSelectedRows); + setAllRowsSelected(!allRowsSelected); + }; + + const handleRowSelect = (rowIndex: number) => { + setSelectedRows({ + ...selectedRows, + [rowIndex]: selectedRows[rowIndex] ? !selectedRows[rowIndex] : true, + }); + setAllRowsSelected(false); + }; + + return ( +
+ + {items.map((item, index) => ( + handleRowSelect(index)} + key={index} + /> + ))} +
+ ); +} diff --git a/front/src/components/ui/data-grid/components/DataGridHeader/component.tsx b/front/src/components/ui/data-grid/components/DataGridHeader/component.tsx new file mode 100644 index 0000000..c4df8d7 --- /dev/null +++ b/front/src/components/ui/data-grid/components/DataGridHeader/component.tsx @@ -0,0 +1,62 @@ +import { Ripple } from '@components/ui/animation'; +import { Checkbox } from '@components/ui/checkbox'; +import { RawButton } from '@components/ui/raw'; +import { Span } from '@components/ui/span'; +import ArrowUpIcon from '@public/images/svg/arrow-up.svg'; +import clsx from 'clsx'; +import React, { useState } from 'react'; + +import { DataGridSort } from '../../types'; +import styles from './styles.module.scss'; +import { DataGridHeaderProps } from './types'; + +export function DataGridHeader({ + columns, + allRowsSelected, + onSelectAllRows, +}: DataGridHeaderProps) { + const [sort, setSort] = useState({ order: 'asc', column: '' }); + + const handleSortButtonClick = (column: string) => { + if (column === sort.column) { + if (sort.order === 'asc') { + setSort({ order: 'desc', column }); + } else { + setSort({ order: 'desc', column: '' }); + } + } else { + setSort({ order: 'asc', column }); + } + }; + + return ( +
+ + {columns.map((column) => { + const isActive = sort.column === column.name; + const cellClassName = clsx(styles.cell, { + [styles.activeCell]: isActive, + [styles.desc]: isActive && sort.order === 'desc', + }); + return ( + handleSortButtonClick(column.name)} + > + + {column.name} + + + + + ); + })} +
+ ); +} diff --git a/front/src/components/ux/windmill-table/index.tsx b/front/src/components/ui/data-grid/components/DataGridHeader/index.ts similarity index 100% rename from front/src/components/ux/windmill-table/index.tsx rename to front/src/components/ui/data-grid/components/DataGridHeader/index.ts diff --git a/front/src/components/ui/data-grid/components/DataGridHeader/styles.module.scss b/front/src/components/ui/data-grid/components/DataGridHeader/styles.module.scss new file mode 100644 index 0000000..4888bd1 --- /dev/null +++ b/front/src/components/ui/data-grid/components/DataGridHeader/styles.module.scss @@ -0,0 +1,58 @@ +.header { + display: flex; +} + +.checkboxLabel { + padding: 10px; + border: solid 1px var(--clr-border-100); + background-color: var(--clr-layer-300); + border-top-left-radius: 10px; +} + +.cell { + position: relative; + display: flex; + overflow: hidden; + flex: 1; + align-items: center; + padding: 10px; + border: solid 1px var(--clr-border-100); + background-color: var(--clr-layer-300); + cursor: pointer; + gap: 10px; + transition: all var(--td-100) ease-in-out; + + &:hover { + background-color: var(--clr-layer-300-hover); + } + + &:last-of-type { + border-top-right-radius: 10px; + } +} + +.name { + overflow: hidden; + flex-shrink: 1; + text-overflow: ellipsis; +} + +.icon { + width: 12px; + height: 12px; + flex-shrink: 0; + fill: transparent; + transition: all var(--td-100) ease-in-out; +} + +.activeCell { + .icon { + fill: var(--clr-text-200); + } +} + +.desc { + .icon { + rotate: 180deg; + } +} diff --git a/front/src/components/ui/data-grid/components/DataGridHeader/types.ts b/front/src/components/ui/data-grid/components/DataGridHeader/types.ts new file mode 100644 index 0000000..dec95a3 --- /dev/null +++ b/front/src/components/ui/data-grid/components/DataGridHeader/types.ts @@ -0,0 +1,7 @@ +import { DataGridColumnConfig } from '../../types'; + +export type DataGridHeaderProps = { + columns: DataGridColumnConfig[]; + allRowsSelected: boolean; + onSelectAllRows: () => void; +}; diff --git a/front/src/components/ui/data-grid/components/DataGridRow/component.tsx b/front/src/components/ui/data-grid/components/DataGridRow/component.tsx new file mode 100644 index 0000000..c55621e --- /dev/null +++ b/front/src/components/ui/data-grid/components/DataGridRow/component.tsx @@ -0,0 +1,32 @@ +import { Checkbox } from '@components/ui/checkbox'; +import { Span } from '@components/ui/span'; +import React from 'react'; + +import styles from './styles.module.scss'; +import { DataGridRowProps } from './types'; + +export function DataGridRow({ + object, + columns, + selected, + onSelect, +}: DataGridRowProps) { + return ( +
+ + {columns.map((column) => ( +
+ {column.getText(object)} +
+ ))} +
+ ); +} diff --git a/front/src/components/ui/data-grid/components/DataGridRow/index.ts b/front/src/components/ui/data-grid/components/DataGridRow/index.ts new file mode 100644 index 0000000..bb82484 --- /dev/null +++ b/front/src/components/ui/data-grid/components/DataGridRow/index.ts @@ -0,0 +1 @@ +export * from './component'; diff --git a/front/src/components/ui/data-grid/components/DataGridRow/styles.module.scss b/front/src/components/ui/data-grid/components/DataGridRow/styles.module.scss new file mode 100644 index 0000000..6cc471e --- /dev/null +++ b/front/src/components/ui/data-grid/components/DataGridRow/styles.module.scss @@ -0,0 +1,18 @@ +.row { + display: flex; +} + +.checkboxLabel { + padding: 10px; + border: solid 1px var(--clr-border-100); +} + +.cell { + display: flex; + overflow: hidden; + flex: 1 0 0; + align-items: center; + padding: 10px; + border: solid 1px var(--clr-border-100); + overflow-wrap: anywhere; +} diff --git a/front/src/components/ui/data-grid/components/DataGridRow/types.ts b/front/src/components/ui/data-grid/components/DataGridRow/types.ts new file mode 100644 index 0000000..f88f239 --- /dev/null +++ b/front/src/components/ui/data-grid/components/DataGridRow/types.ts @@ -0,0 +1,8 @@ +import { DataGridColumnConfig } from '../../types'; + +export type DataGridRowProps = { + object: T; + columns: DataGridColumnConfig[]; + selected: boolean; + onSelect: () => void; +}; diff --git a/front/src/components/ui/data-grid/components/index.ts b/front/src/components/ui/data-grid/components/index.ts new file mode 100644 index 0000000..f703f00 --- /dev/null +++ b/front/src/components/ui/data-grid/components/index.ts @@ -0,0 +1,2 @@ +export * from './DataGridHeader'; +export * from './DataGridRow'; diff --git a/front/src/components/ui/data-grid/index.ts b/front/src/components/ui/data-grid/index.ts new file mode 100644 index 0000000..fb2697a --- /dev/null +++ b/front/src/components/ui/data-grid/index.ts @@ -0,0 +1,2 @@ +export * from './component'; +export * from './preview'; diff --git a/front/src/components/ui/data-grid/preview.tsx b/front/src/components/ui/data-grid/preview.tsx new file mode 100644 index 0000000..4695146 --- /dev/null +++ b/front/src/components/ui/data-grid/preview.tsx @@ -0,0 +1,28 @@ +import { PreviewArticle } from '@components/ui/preview'; +import React from 'react'; + +import { DataGrid } from './component'; +import { Cat, DataGridColumnConfig } from './types'; + +export function DataGridPreview() { + const items: Cat[] = [ + { name: 'Luna', breed: 'British Shorthair', color: 'Gray', age: '2' }, + { name: 'Simba', breed: 'Siamese', color: 'Cream', age: '1' }, + { name: 'Bella', breed: 'Maine Coon', color: 'Brown Tabby', age: '3' }, + { name: 'Oliver', breed: 'Persian', color: 'White', age: '4' }, + { name: 'Milo', breed: 'Sphynx', color: 'Pink', age: '2' }, + ]; + + const columns: DataGridColumnConfig[] = [ + { name: 'Name', getText: (cat) => cat.name, flex: '2' }, + { name: 'Breed', getText: (cat) => cat.breed }, + { name: 'Age', getText: (cat) => cat.age }, + { name: 'Color', getText: (cat) => cat.color }, + ]; + + return ( + + + + ); +} diff --git a/front/src/components/ui/data-grid/types.ts b/front/src/components/ui/data-grid/types.ts new file mode 100644 index 0000000..7f49f4e --- /dev/null +++ b/front/src/components/ui/data-grid/types.ts @@ -0,0 +1,23 @@ +export type DataGridColumnConfig = { + name: string; + getText: (object: T) => string; + sortable?: boolean; + flex?: string; +}; + +export type DataGridSort = { + order: 'asc' | 'desc'; + column: string; +}; + +export type DataGridProps = { + items: T[]; + columns: DataGridColumnConfig[]; +} & React.ComponentPropsWithoutRef<'div'>; + +export type Cat = { + name: string; + breed: string; + age: string; + color: string; +}; diff --git a/front/src/components/ui/date-input/component.tsx b/front/src/components/ui/date-input/component.tsx index 5eaf3c6..c60ca7c 100644 --- a/front/src/components/ui/date-input/component.tsx +++ b/front/src/components/ui/date-input/component.tsx @@ -1,5 +1,4 @@ import CalendarIcon from '@public/images/svg/calendar.svg'; -import { px } from '@utils/css'; import { useMissClick } from '@utils/miss-click'; import React, { useEffect, useMemo, useRef, useState } from 'react'; @@ -48,6 +47,14 @@ export function DateInput({ setCalendarVisible(!calendarVisible); }; + const handleCalendarButtonMouseDown = (event: React.MouseEvent) => { + event.preventDefault(); + }; + + const handleCalendarButtonMouseUp = (event: React.MouseEvent) => { + event.preventDefault(); + }; + const handleInputChange = (event: React.ChangeEvent) => { const newDirtyDate = inputToDirtyDate(event.target.value); if (newDirtyDate.length === 10) { @@ -58,42 +65,19 @@ export function DateInput({ (!minDate || date >= minDate) && (!maxDate || date <= maxDate) ) { - onChange?.(newValue); + onChange(newValue); } else { - onChange?.(''); + onChange(''); } } setDirtyDate(newDirtyDate); }; const handleCalendarChange = (newValue: string) => { - onChange?.(newValue); + onChange(newValue); setCalendarVisible(false); }; - const calcPopoverStyles = (calendarRect: DOMRect) => { - if (calendarRect === null) { - return {}; - } - - const inputWrapperRect = inputWrapperRef.current.getBoundingClientRect(); - const { left, bottom, top } = inputWrapperRect; - - const rightSpace = window.innerWidth - left; - const rightOverflow = calendarRect.width - rightSpace; - const bottomSpace = window.innerHeight - bottom; - - const popoverLeft = rightOverflow <= 0 ? left : left - rightOverflow; - - const popoverTop = - bottomSpace >= calendarRect.height ? bottom : top - calendarRect.height; - - return { - left: px(popoverLeft), - top: px(popoverTop), - }; - }; - return (
- + {rightNode} @@ -113,7 +102,10 @@ export function DateInput({ /> void; + onChange: (value: string) => void; max?: string; min?: string; } & Omit; diff --git a/front/src/components/ui/file-uploader/component.tsx b/front/src/components/ui/file-uploader/component.tsx new file mode 100644 index 0000000..d797c96 --- /dev/null +++ b/front/src/components/ui/file-uploader/component.tsx @@ -0,0 +1,78 @@ +import UploadIcon from '@public/images/svg/upload.svg'; +import { getFileExtension } from '@utils/file'; +import clsx from 'clsx'; +import React, { useRef } from 'react'; + +import { Ripple } from '../animation'; +import { Label } from '../label'; +import { RawButton, RawInput } from '../raw'; +import { Span } from '../span'; +import styles from './style.module.scss'; +import { FileUploaderProps } from './types'; + +export function FileUploader({ + extensions, + onChange, + scale = 'm', + label = {}, + input = {}, + ...props +}: FileUploaderProps) { + const inputRef = useRef(null); + const uploaderClassName = clsx(styles.uploader, styles[scale]); + + const handleChange = (files: FileList) => { + if (!files || !onChange) { + return; + } + const array = Array.from(files); + const filtered = extensions + ? array.filter((file) => extensions.includes(getFileExtension(file))) + : array; + onChange(filtered); + }; + + const handleButtonClick = () => { + inputRef.current.click(); + }; + + const handleInputChange = (event: React.ChangeEvent) => { + handleChange(event.target.files); + }; + + const handleDragOver = (event: React.DragEvent) => { + event.preventDefault(); + event.dataTransfer.dropEffect = 'copy'; + }; + + const handleDrop = (event: React.DragEvent) => { + event.preventDefault(); + handleChange(event.dataTransfer.files); + }; + + return ( + + ); +} diff --git a/front/src/components/ui/file-uploader/index.ts b/front/src/components/ui/file-uploader/index.ts new file mode 100644 index 0000000..1d94b2f --- /dev/null +++ b/front/src/components/ui/file-uploader/index.ts @@ -0,0 +1 @@ +export { FileUploader } from './component'; diff --git a/front/src/components/ui/file-uploader/preview.tsx b/front/src/components/ui/file-uploader/preview.tsx new file mode 100644 index 0000000..8108d07 --- /dev/null +++ b/front/src/components/ui/file-uploader/preview.tsx @@ -0,0 +1,14 @@ +import { PreviewArticle } from '@components/ui/preview'; +import React from 'react'; + +import { FileUploader } from './component'; + +export function FileUploaderPreview() { + return ( + + + + + + ); +} diff --git a/front/src/components/ui/file-uploader/style.module.scss b/front/src/components/ui/file-uploader/style.module.scss new file mode 100644 index 0000000..c90402e --- /dev/null +++ b/front/src/components/ui/file-uploader/style.module.scss @@ -0,0 +1,68 @@ +@use '@components/func.scss' as f; + +.uploader { + position: relative; + display: flex; + overflow: hidden; + flex-direction: column; + align-items: center; + border: 1px dashed var(--clr-border-200); + background-color: var(--clr-layer-300); + box-shadow: 0px 2px 2px var(--clr-shadow-100); + cursor: pointer; + transition: all var(--td-100) ease-in-out; + + &:not(.wrapperFocus):hover { + background-color: var(--clr-layer-300-hover); + } +} + +.input { + display: none; +} + +.icon { + fill: var(--clr-text-100); +} + +$padding: 10px 16px; +$border-radius: 8px; +$font-size: 12px; +$icon-size: 24px; +$gap: 5px; + +.s { + padding: $padding; + border-radius: $border-radius; + font-size: $font-size; + gap: $gap; + + .icon { + width: $icon-size; + height: $icon-size; + } +} + +.m { + padding: f.m($padding); + border-radius: f.m($border-radius); + font-size: f.m($font-size); + gap: f.m($gap); + + .icon { + width: f.m($icon-size); + height: f.m($icon-size); + } +} + +.l { + padding: f.l($padding); + border-radius: f.l($border-radius); + font-size: f.l($font-size); + gap: f.l($gap); + + .icon { + width: f.l($icon-size); + height: f.l($icon-size); + } +} diff --git a/front/src/components/ui/file-uploader/types.ts b/front/src/components/ui/file-uploader/types.ts new file mode 100644 index 0000000..95b5365 --- /dev/null +++ b/front/src/components/ui/file-uploader/types.ts @@ -0,0 +1,11 @@ +import { LabelProps } from '../label'; +import { RawInputProps } from '../raw'; +import { Scale } from '../types'; + +export type FileUploaderProps = { + extensions?: string[]; + onChange?: (value: File[]) => void; + scale?: Scale; + label?: LabelProps; + input?: Omit; +} & Omit, 'onChange'>; diff --git a/front/src/components/ui/icon-button/styles.module.scss b/front/src/components/ui/icon-button/styles.module.scss index f971457..2044968 100644 --- a/front/src/components/ui/icon-button/styles.module.scss +++ b/front/src/components/ui/icon-button/styles.module.scss @@ -1,3 +1,5 @@ +@use '@components/func.scss' as f; + .button { position: relative; overflow: hidden; @@ -22,20 +24,23 @@ } } +$size: 26px; +$padding: 4px; + .s { - width: 27px; - height: 27px; - padding: 4px; + width: $size; + height: $size; + padding: $padding; } .m { - width: 35px; - height: 35px; - padding: 6px; + width: f.m($size); + height: f.m($size); + padding: f.m($padding); } .l { - width: 43px; - height: 43px; - padding: 8px; + width: f.l($size); + height: f.l($size); + padding: f.l($padding); } diff --git a/front/src/components/ui/image-file-manager/component.tsx b/front/src/components/ui/image-file-manager/component.tsx new file mode 100644 index 0000000..e652e7a --- /dev/null +++ b/front/src/components/ui/image-file-manager/component.tsx @@ -0,0 +1,40 @@ +import clsx from 'clsx'; +import React from 'react'; + +import { FileUploader } from '../file-uploader'; +import { ImageViewer } from '../image-viewer'; +import styles from './styles.module.scss'; +import { ImageFileManagerProps } from './types'; + +export function ImageFileManager({ + value, + onChange, + scale = 'm', + label = {}, +}: ImageFileManagerProps) { + const managerClassName = clsx(styles.manager, styles[scale]); + + const handleFileUploaderChange = (files: File[]) => { + const file = files[0]; + if (!file) { + return; + } + onChange?.(file); + }; + + const handleClear = () => { + onChange?.(null); + }; + + return ( +
+ + +
+ ); +} diff --git a/front/src/components/ui/image-file-manager/index.ts b/front/src/components/ui/image-file-manager/index.ts new file mode 100644 index 0000000..0fff9fa --- /dev/null +++ b/front/src/components/ui/image-file-manager/index.ts @@ -0,0 +1 @@ +export { ImageFileManager } from './component'; diff --git a/front/src/components/ui/image-file-manager/preview.tsx b/front/src/components/ui/image-file-manager/preview.tsx new file mode 100644 index 0000000..4ecbe2f --- /dev/null +++ b/front/src/components/ui/image-file-manager/preview.tsx @@ -0,0 +1,31 @@ +import { PreviewArticle } from '@components/ui/preview'; +import React, { useState } from 'react'; + +import { ImageFileManager } from './component'; + +export function ImageFileManagerPreview() { + const [value, setValue] = useState(null); + + return ( + + + + + + ); +} diff --git a/front/src/components/ui/image-file-manager/styles.module.scss b/front/src/components/ui/image-file-manager/styles.module.scss new file mode 100644 index 0000000..80eff0a --- /dev/null +++ b/front/src/components/ui/image-file-manager/styles.module.scss @@ -0,0 +1,20 @@ +@use '@components/func.scss' as f; + +.manager { + display: grid; + grid-template-columns: minmax(0, 1fr); +} + +$gap: 12px; + +.s { + gap: $gap; +} + +.m { + gap: f.m($gap); +} + +.l { + gap: f.l($gap); +} diff --git a/front/src/components/ui/image-file-manager/types.ts b/front/src/components/ui/image-file-manager/types.ts new file mode 100644 index 0000000..9f59dab --- /dev/null +++ b/front/src/components/ui/image-file-manager/types.ts @@ -0,0 +1,9 @@ +import { LabelProps } from '../label'; +import { Scale } from '../types'; + +export type ImageFileManagerProps = { + value?: File | null; + onChange?: (value: File | null) => void; + scale?: Scale; + label?: LabelProps; +} & Omit, 'onChange'>; diff --git a/front/src/components/ui/image-viewer/component.tsx b/front/src/components/ui/image-viewer/component.tsx new file mode 100644 index 0000000..f7860ec --- /dev/null +++ b/front/src/components/ui/image-viewer/component.tsx @@ -0,0 +1,32 @@ +import DeleteIcon from '@public/images/svg/delete.svg'; +import { formatFileSize } from '@utils/file'; +import clsx from 'clsx'; +import React from 'react'; + +import { IconButton } from '../icon-button'; +import { Span } from '../span'; +import styles from './styles.module.scss'; +import { ImageViewerProps } from './types'; + +export function ImageViewer({ file, scale = 'm', onClear }: ImageViewerProps) { + const viewerClassName = clsx(styles.viewer, styles[scale]); + return ( +
+ {file ? ( + <> + +
+ {formatFileSize(file.size)} + + + +
+ + ) : ( +
+ File not uploaded +
+ )} +
+ ); +} diff --git a/front/src/components/ui/image-viewer/index.ts b/front/src/components/ui/image-viewer/index.ts new file mode 100644 index 0000000..52d66d1 --- /dev/null +++ b/front/src/components/ui/image-viewer/index.ts @@ -0,0 +1 @@ +export { ImageViewer } from './component'; diff --git a/front/src/components/ui/image-viewer/styles.module.scss b/front/src/components/ui/image-viewer/styles.module.scss new file mode 100644 index 0000000..87d1617 --- /dev/null +++ b/front/src/components/ui/image-viewer/styles.module.scss @@ -0,0 +1,56 @@ +@use '@components/func.scss' as f; + +.viewer { + display: grid; + overflow: hidden; + box-shadow: 0px 2px 2px var(--clr-shadow-100); + grid-template-columns: 1fr; +} + +.placeholder { + display: flex; + justify-content: center; + background-color: var(--clr-layer-300); +} + +.image { + max-width: 100%; + max-height: 100%; +} + +.footer { + display: flex; + align-items: center; + justify-content: space-between; + background-color: var(--clr-layer-300); +} + +$padding: 10px 16px; +$border-radius: 8px; + +.s { + border-radius: $border-radius; + + .placeholder, + .footer { + padding: $padding; + } +} + +.m { + border-radius: f.m($border-radius); + + .placeholder, + .footer { + padding: f.m($padding); + } +} + +.l { + border-radius: f.l($border-radius); + + .placeholder, + .footer { + padding: f.l($padding); + } +} diff --git a/front/src/components/ui/image-viewer/types.ts b/front/src/components/ui/image-viewer/types.ts new file mode 100644 index 0000000..2b44ce3 --- /dev/null +++ b/front/src/components/ui/image-viewer/types.ts @@ -0,0 +1,7 @@ +import { Scale } from '../types'; + +export type ImageViewerProps = { + file?: File; + scale?: Scale; + onClear?: () => void; +}; diff --git a/front/src/components/ui/index.tsx b/front/src/components/ui/index.tsx index e485f62..9ce5c78 100644 --- a/front/src/components/ui/index.tsx +++ b/front/src/components/ui/index.tsx @@ -1,13 +1,18 @@ +export { Autocomplete } from './autocomplete'; export { Button } from './button'; export { Checkbox } from './checkbox'; export { CheckboxGroup } from './checkbox-group'; export { DateInput } from './date-input'; +export { FileUploader } from './file-uploader'; export { Heading } from './heading'; export { IconButton } from './icon-button'; +export { ImageFileManager } from './image-file-manager'; export { Menu } from './menu'; +export { NumberInput } from './number-input'; export { Paragraph } from './paragraph'; export { PasswordInput } from './password-input'; export { RadioGroup } from './radio-group'; export { Select } from './select'; export { Span } from './span'; +export { TextArea } from './text-area'; export { TextInput } from './text-input'; diff --git a/front/src/components/ui/input/styles.module.scss b/front/src/components/ui/input/styles.module.scss index c4d2fc2..1a68299 100644 --- a/front/src/components/ui/input/styles.module.scss +++ b/front/src/components/ui/input/styles.module.scss @@ -1,3 +1,5 @@ +@use '@components/func.scss' as f; + .wrapper { display: flex; align-items: center; @@ -26,29 +28,33 @@ outline: none; } +$border-radius: 8px; +$padding: 9px; +$font-size: 12px; + .s { - border-radius: 8px; + border-radius: $border-radius; .input { - padding: 9px; - font-size: 12px; + padding: $padding; + font-size: $font-size; } } .m { - border-radius: 10px; + border-radius: f.m($border-radius); .input { - padding: 13px; - font-size: 16px; + padding: f.m($padding); + font-size: f.m($font-size); } } .l { - border-radius: 12px; + border-radius: f.l($border-radius); .input { - padding: 17px; - font-size: 20px; + padding: f.l($padding); + font-size: f.l($font-size); } } diff --git a/front/src/components/ui/number-input/component.tsx b/front/src/components/ui/number-input/component.tsx new file mode 100644 index 0000000..eef6896 --- /dev/null +++ b/front/src/components/ui/number-input/component.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +import { TextInput } from '../text-input'; +import { NumberInputProps } from './types'; + +export function NumberInput({ + value, + onChange, + float = false, + negative = false, + ...props +}: NumberInputProps) { + const extractNumber = (event: React.ChangeEvent) => { + const { value } = event.target; + if (!value) { + return ''; + } + let pattern = ''; + if (negative) { + pattern += '-?'; + } + pattern += '\\d*'; + if (float) { + pattern += '\\.?\\d*'; + } + return value.match(pattern)?.[0] ?? null; + }; + + const handleChange = (event: React.ChangeEvent) => { + const num = extractNumber(event); + if (num === null) { + return; + } + onChange?.(num); + }; + + return ; +} diff --git a/front/src/components/ui/number-input/index.tsx b/front/src/components/ui/number-input/index.tsx new file mode 100644 index 0000000..101f010 --- /dev/null +++ b/front/src/components/ui/number-input/index.tsx @@ -0,0 +1 @@ +export { NumberInput } from './component'; diff --git a/front/src/components/ui/number-input/preview.tsx b/front/src/components/ui/number-input/preview.tsx new file mode 100644 index 0000000..185c1ab --- /dev/null +++ b/front/src/components/ui/number-input/preview.tsx @@ -0,0 +1,39 @@ +import { PreviewArticle } from '@components/ui/preview'; +import React, { useState } from 'react'; + +import { NumberInput } from './component'; + +export function NumberInputPreview() { + const [value1, setValue1] = useState(''); + const [value2, setValue2] = useState(''); + const [value3, setValue3] = useState(''); + + return ( + + + + + + ); +} diff --git a/front/src/components/ui/number-input/types.ts b/front/src/components/ui/number-input/types.ts new file mode 100644 index 0000000..ea020c7 --- /dev/null +++ b/front/src/components/ui/number-input/types.ts @@ -0,0 +1,8 @@ +import { TextInputProps } from '../text-input'; + +export type NumberInputProps = { + float?: boolean; + negative?: boolean; + value?: string; + onChange?: (value: string) => void; +} & Omit; diff --git a/front/src/components/ui/paragraph/styles.module.scss b/front/src/components/ui/paragraph/styles.module.scss index 03d0c00..466d030 100644 --- a/front/src/components/ui/paragraph/styles.module.scss +++ b/front/src/components/ui/paragraph/styles.module.scss @@ -1,17 +1,21 @@ +@use '@components/func.scss' as f; + .paragraph { margin: 0; } +$font-size: 12px; + .s { - font-size: 12px; + font-size: $font-size; } .m { - font-size: 16px; + font-size: f.m($font-size); } .l { - font-size: 20px; + font-size: f.l($font-size); } .t100 { diff --git a/front/src/components/ui/popover/component.tsx b/front/src/components/ui/popover/component.tsx index 254b47f..351dfd6 100644 --- a/front/src/components/ui/popover/component.tsx +++ b/front/src/components/ui/popover/component.tsx @@ -10,8 +10,16 @@ import { createPortal } from 'react-dom'; import { Fade } from '../animation'; import styles from './styles.module.scss'; import { PopoverProps } from './types'; +import { calcFadeStyles } from './utils'; -export function Popover({ element, visible, calcStyles }: PopoverProps) { +export function Popover({ + visible, + anchorRef, + position, + horizontalAlign, + element, + flip = false, +}: PopoverProps) { const elementRef = useRef(null); const fadeRef = useRef(null); const [elementRect, setElementRect] = useState(null); @@ -25,7 +33,14 @@ export function Popover({ element, visible, calcStyles }: PopoverProps) { return; } const updateStyles = () => { - Object.assign(fadeRef.current.style, calcStyles(elementRect)); + const style = calcFadeStyles( + elementRect, + anchorRef, + position, + horizontalAlign, + flip, + ); + Object.assign(fadeRef.current.style, style); }; window.addEventListener('scroll', updateStyles, true); window.addEventListener('resize', updateStyles); @@ -36,7 +51,10 @@ export function Popover({ element, visible, calcStyles }: PopoverProps) { }, [visible]); if (elementRect === null) { - return cloneElement(element, { ref: elementRef }); + return cloneElement(element, { + ref: elementRef, + style: { position: 'absolute' }, + }); } return createPortal( @@ -44,7 +62,13 @@ export function Popover({ element, visible, calcStyles }: PopoverProps) { visible={visible} className={styles.fade} ref={fadeRef} - style={{ ...calcStyles(elementRect) }} + style={calcFadeStyles( + elementRect, + anchorRef, + position, + horizontalAlign, + flip, + )} > {element} , diff --git a/front/src/components/ui/popover/index.ts b/front/src/components/ui/popover/index.ts deleted file mode 100644 index 192728d..0000000 --- a/front/src/components/ui/popover/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { Popover } from './component'; -export { type PopoverProps, type PopoverStyles } from './types'; diff --git a/front/src/components/ui/popover/index.tsx b/front/src/components/ui/popover/index.tsx new file mode 100644 index 0000000..31eec59 --- /dev/null +++ b/front/src/components/ui/popover/index.tsx @@ -0,0 +1 @@ +export { Popover } from './component'; diff --git a/front/src/components/ui/popover/styles.module.scss b/front/src/components/ui/popover/styles.module.scss index 4bb9de8..3614187 100644 --- a/front/src/components/ui/popover/styles.module.scss +++ b/front/src/components/ui/popover/styles.module.scss @@ -1,5 +1,4 @@ .fade { position: absolute; - top: 0; - left: 0; + z-index: 2; } diff --git a/front/src/components/ui/popover/types.ts b/front/src/components/ui/popover/types.ts index d2a30d4..dffba38 100644 --- a/front/src/components/ui/popover/types.ts +++ b/front/src/components/ui/popover/types.ts @@ -1,13 +1,12 @@ -import { ReactElement } from 'react'; +export type PopoverPosition = 'top' | 'bottom'; -export type PopoverStyles = { - left?: string; - top?: string; - width?: string; -}; +export type PopoverHorizontalAlign = 'left' | 'right' | 'center' | 'stretch'; export type PopoverProps = { - element: ReactElement; visible: boolean; - calcStyles: (elementRect: DOMRect | null) => PopoverStyles; + anchorRef: React.MutableRefObject; + position: PopoverPosition; + horizontalAlign: PopoverHorizontalAlign; + element: React.ReactElement; + flip?: boolean; }; diff --git a/front/src/components/ui/popover/utils.ts b/front/src/components/ui/popover/utils.ts new file mode 100644 index 0000000..460a50e --- /dev/null +++ b/front/src/components/ui/popover/utils.ts @@ -0,0 +1,92 @@ +import { px } from '@utils/css'; +import { CSSProperties } from 'react'; + +import { PopoverHorizontalAlign, PopoverPosition } from './types'; + +const applyPositionTop = ( + anchorRect: DOMRect, + elementRect: DOMRect, + styles: CSSProperties, +) => { + styles.top = px(anchorRect.bottom - anchorRect.height - elementRect.height); +}; + +const applyPositionBottom = (anchorRect: DOMRect, styles: CSSProperties) => { + styles.top = px(anchorRect.bottom); +}; + +const applyPosition = ( + elementRect: DOMRect, + anchorRect: DOMRect, + position: PopoverPosition, + flip: boolean, + styles: CSSProperties, +) => { + if (position === 'bottom') { + if (flip) { + const bottomSpace = window.innerHeight - anchorRect.bottom; + if (bottomSpace >= elementRect.height) { + applyPositionBottom(anchorRect, styles); + } else { + applyPositionTop(anchorRect, elementRect, styles); + } + } else { + applyPositionBottom(anchorRect, styles); + } + } + + if (position === 'top') { + if (flip) { + const topSpace = anchorRect.top; + if (topSpace >= elementRect.height) { + applyPositionTop(anchorRect, elementRect, styles); + } else { + applyPositionBottom(anchorRect, styles); + } + } else { + applyPositionTop(anchorRect, elementRect, styles); + } + } +}; + +const applyHorizontalAlign = ( + elementRect: DOMRect, + anchorRect: DOMRect, + horizontalAlign: PopoverHorizontalAlign, + styles: CSSProperties, +) => { + if (horizontalAlign === 'left') { + styles.left = px(anchorRect.left); + } + + if (horizontalAlign === 'right') { + styles.left = px(anchorRect.left + anchorRect.width - elementRect.width); + } + + if (horizontalAlign === 'center') { + styles.left = px( + anchorRect.left + (anchorRect.width - elementRect.width) / 2, + ); + } + + if (horizontalAlign === 'stretch') { + styles.left = px(anchorRect.left); + styles.width = px(anchorRect.width); + } +}; + +export const calcFadeStyles = ( + elementRect: DOMRect, + anchorRef: React.MutableRefObject, + position: PopoverPosition, + horizontalAlign: PopoverHorizontalAlign, + flip: boolean, +): CSSProperties => { + const anchorRect = anchorRef.current.getBoundingClientRect(); + const styles: CSSProperties = {}; + + applyPosition(elementRect, anchorRect, position, flip, styles); + applyHorizontalAlign(elementRect, anchorRect, horizontalAlign, styles); + + return styles; +}; diff --git a/front/src/components/ui/radio-group/styles.module.scss b/front/src/components/ui/radio-group/styles.module.scss index b1533e6..2393402 100644 --- a/front/src/components/ui/radio-group/styles.module.scss +++ b/front/src/components/ui/radio-group/styles.module.scss @@ -1,22 +1,26 @@ +@use '@components/func.scss' as f; + .checkBoxGroup { display: flex; flex-direction: column; } +$margin-bottom: 4px; + .s { .label { - margin-bottom: 3px; + margin-bottom: $margin-bottom; } } .m { .label { - margin-bottom: 5px; + margin-bottom: f.m($margin-bottom); } } .l { .label { - margin-bottom: 7px; + margin-bottom: f.l($margin-bottom); } } diff --git a/front/src/components/ui/radio/styles.module.scss b/front/src/components/ui/radio/styles.module.scss index d3be59c..928e2e8 100644 --- a/front/src/components/ui/radio/styles.module.scss +++ b/front/src/components/ui/radio/styles.module.scss @@ -1,3 +1,5 @@ +@use '@components/func.scss' as f; + .wrapper { position: relative; overflow: hidden; @@ -24,7 +26,7 @@ .radio { display: flex; justify-content: center; - border: 1px solid var(--clr-border-200); + border: 2px solid var(--clr-border-200); border-radius: 100%; background-color: var(--clr-layer-300); box-shadow: 0px 2px 2px var(--clr-shadow-200); @@ -55,32 +57,36 @@ } } +$padding-outer: 4px; +$size: 16px; +$padding-inner: 4px; + .s { - padding: 3px; + padding: $padding-outer; .radio { - width: 16px; - height: 16px; - padding: 4px; + width: $size; + height: $size; + padding: $padding-inner; } } .m { - padding: 4px; + padding: f.m($padding-outer); .radio { - width: 20px; - height: 20px; - padding: 5px; + width: f.m($size); + height: f.m($size); + padding: f.m($padding-inner); } } .l { - padding: 5px; + padding: f.l($padding-outer); .radio { - width: 24px; - height: 24px; - padding: 6px; + width: f.l($size); + height: f.l($size); + padding: f.l($padding-inner); } } diff --git a/front/src/components/ui/select/component.tsx b/front/src/components/ui/select/component.tsx index 1c60248..58b0bc0 100644 --- a/front/src/components/ui/select/component.tsx +++ b/front/src/components/ui/select/component.tsx @@ -1,5 +1,4 @@ import ArrowDownIcon from '@public/images/svg/arrow-down.svg'; -import { px } from '@utils/css'; import { useMissClick } from '@utils/miss-click'; import clsx from 'clsx'; import React, { @@ -52,25 +51,6 @@ function SelectInner( onChange?.(option); }; - const calcPopoverStyles = (menuRect: DOMRect) => { - if (menuRect === null) { - return {}; - } - - const inputWrapperRect = inputWrapperRef.current.getBoundingClientRect(); - const { width, left, bottom, top } = inputWrapperRect; - - const bottomSpace = window.innerHeight - bottom; - const popoverTop = - bottomSpace >= menuRect.height ? bottom : top - menuRect.height; - - return { - width: px(width), - left: px(left), - top: px(popoverTop), - }; - }; - return (
( /> = { @@ -11,4 +12,5 @@ export type SelectProps = { label?: LabelProps; name?: string; id?: string; + input?: TextInputProps; } & Omit, 'onChange'>; diff --git a/front/src/components/ui/span/styles.module.scss b/front/src/components/ui/span/styles.module.scss index 08059b0..71d04e6 100644 --- a/front/src/components/ui/span/styles.module.scss +++ b/front/src/components/ui/span/styles.module.scss @@ -1,13 +1,17 @@ +@use '@components/func.scss' as f; + +$font-size: 12px; + .s { - font-size: 12px; + font-size: $font-size; } .m { - font-size: 16px; + font-size: f.m($font-size); } .l { - font-size: 20px; + font-size: f.l($font-size); } .t100 { diff --git a/front/src/components/ui/text-area/component.tsx b/front/src/components/ui/text-area/component.tsx new file mode 100644 index 0000000..f588e85 --- /dev/null +++ b/front/src/components/ui/text-area/component.tsx @@ -0,0 +1,20 @@ +import clsx from 'clsx'; +import React from 'react'; + +import { Label } from '../label'; +import styles from './styles.module.scss'; +import { TextAreaProps } from './types'; + +export function TextArea({ + scale = 'm', + label = {}, + className, + ...props +}: TextAreaProps) { + const textAreaClassName = clsx(styles.textarea, styles[scale], className); + return ( +