From 1c205c69acd20595421101c024ce27c5af7afcfc Mon Sep 17 00:00:00 2001 From: Sam Liu Date: Thu, 27 Jun 2024 04:35:56 +0000 Subject: [PATCH] [haiz] haiz --- alembic/env.py | 1 + alembic/script.py.mako | 1 + ....py => 4b90a6ac504b_create_users_table.py} | 11 +- ...ef4aef5fcda_create_house_and_area_table.py | 70 +++++++++ backend/app.py | 2 +- backend/db/migration_types.py | 1 + backend/db/models/__init__.py | 1 + backend/db/models/guid.py | 56 +++++++ backend/db/models/houses/__init__.py | 1 + backend/db/models/houses/houses.py | 33 ++++ backend/db/models/users/users.py | 11 +- backend/repos/__init__.py | 1 + backend/repos/repository_houses.py | 26 ++++ backend/repos/repository_users.py | 8 +- backend/routes/__init__.py | 3 +- backend/routes/house/__init__.py | 7 + backend/routes/house/house.py | 21 +++ backend/schemas/__init__.py | 1 + backend/schemas/house/__init__.py | 1 + backend/schemas/house/house.py | 17 ++ backend/services/house/__init__.py | 1 + backend/services/house/house_service.py | 11 ++ backend/services/user/user_service.py | 1 - frontend/index.html | 1 + frontend/package.json | 4 +- frontend/pnpm-lock.yaml | 68 ++++++-- frontend/src/App.scss | 31 ++++ frontend/src/components/AreaAdd/AreaAdd.jsx | 145 ++++++++++++++++++ frontend/src/components/AreaAdd/AreaItem.jsx | 32 ++++ frontend/src/components/AreaAdd/index.js | 2 + frontend/src/components/Header.jsx | 3 +- frontend/src/components/Navbar/Navbar.jsx | 2 +- .../common/ConfirmPopup/ConfirmPopup.jsx | 22 +++ .../components/common/ConfirmPopup/index.js | 2 + .../src/components/common/Popup/Popup.jsx | 27 ++++ frontend/src/components/common/Popup/index.js | 2 + .../components/common/TextInput/TextInput.jsx | 63 ++++---- .../components/common/Textarea/Textarea.jsx | 39 +++++ .../src/components/common/Textarea/index.js | 2 + frontend/src/context/SiteContext.jsx | 6 +- frontend/src/hooks/useAuth.js | 3 +- frontend/src/hooks/useLanguage.js | 21 ++- frontend/src/index.scss | 13 ++ frontend/src/lang/vi.json | 18 ++- frontend/src/pages/Home.jsx | 10 +- frontend/src/pages/House/House.jsx | 100 +++++++----- frontend/src/pages/House/house.scss | 26 ++-- .../src/pages/HouseCreate/HouseCreate.jsx | 114 ++++++++------ frontend/src/pages/Login/Login.jsx | 72 ++++----- frontend/src/pages/Profile/Profile.jsx | 109 ++++++------- frontend/src/utils/helper.js | 14 ++ 51 files changed, 958 insertions(+), 279 deletions(-) rename alembic/versions/{68d05d045e6e_create_user_table.py => 4b90a6ac504b_create_users_table.py} (88%) create mode 100644 alembic/versions/7ef4aef5fcda_create_house_and_area_table.py create mode 100644 backend/db/migration_types.py create mode 100644 backend/db/models/guid.py create mode 100644 backend/db/models/houses/__init__.py create mode 100644 backend/db/models/houses/houses.py create mode 100644 backend/repos/repository_houses.py create mode 100644 backend/routes/house/__init__.py create mode 100644 backend/routes/house/house.py create mode 100644 backend/schemas/house/__init__.py create mode 100644 backend/schemas/house/house.py create mode 100644 backend/services/house/__init__.py create mode 100644 backend/services/house/house_service.py create mode 100644 frontend/src/components/AreaAdd/AreaAdd.jsx create mode 100644 frontend/src/components/AreaAdd/AreaItem.jsx create mode 100644 frontend/src/components/AreaAdd/index.js create mode 100644 frontend/src/components/common/ConfirmPopup/ConfirmPopup.jsx create mode 100644 frontend/src/components/common/ConfirmPopup/index.js create mode 100644 frontend/src/components/common/Popup/Popup.jsx create mode 100644 frontend/src/components/common/Popup/index.js create mode 100644 frontend/src/components/common/Textarea/Textarea.jsx create mode 100644 frontend/src/components/common/Textarea/index.js diff --git a/alembic/env.py b/alembic/env.py index 6176ca7..3f66b56 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -53,6 +53,7 @@ def run_migrations_offline() -> None: target_metadata=target_metadata, literal_binds=True, dialect_opts={"paramstyle": "named"}, + render_as_batch=True ) with context.begin_transaction(): diff --git a/alembic/script.py.mako b/alembic/script.py.mako index fbc4b07..83185a7 100644 --- a/alembic/script.py.mako +++ b/alembic/script.py.mako @@ -7,6 +7,7 @@ Create Date: ${create_date} """ from typing import Sequence, Union +import backend.db.migration_types from alembic import op import sqlalchemy as sa ${imports if imports else ""} diff --git a/alembic/versions/68d05d045e6e_create_user_table.py b/alembic/versions/4b90a6ac504b_create_users_table.py similarity index 88% rename from alembic/versions/68d05d045e6e_create_user_table.py rename to alembic/versions/4b90a6ac504b_create_users_table.py index f72f08b..9e5e5f1 100644 --- a/alembic/versions/68d05d045e6e_create_user_table.py +++ b/alembic/versions/4b90a6ac504b_create_users_table.py @@ -1,18 +1,19 @@ -"""create user table +"""create users table -Revision ID: 68d05d045e6e +Revision ID: 4b90a6ac504b Revises: -Create Date: 2024-05-24 04:12:25.599139 +Create Date: 2024-06-25 09:14:34.465698 """ from typing import Sequence, Union +import backend.db.migration_types from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. -revision: str = '68d05d045e6e' +revision: str = '4b90a6ac504b' down_revision: Union[str, None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -21,7 +22,7 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table('users', - sa.Column('id', sa.UUID(), nullable=False), + sa.Column('id', backend.db.models.guid.GUID(), nullable=False), sa.Column('username', sa.String(), nullable=False), sa.Column('password', sa.String(), nullable=False), sa.Column('name', sa.String(), nullable=True), diff --git a/alembic/versions/7ef4aef5fcda_create_house_and_area_table.py b/alembic/versions/7ef4aef5fcda_create_house_and_area_table.py new file mode 100644 index 0000000..6eae2f2 --- /dev/null +++ b/alembic/versions/7ef4aef5fcda_create_house_and_area_table.py @@ -0,0 +1,70 @@ +"""create house and area table + +Revision ID: 7ef4aef5fcda +Revises: 4b90a6ac504b +Create Date: 2024-06-25 09:20:59.915212 + +""" +from typing import Sequence, Union + +import backend.db.migration_types +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '7ef4aef5fcda' +down_revision: Union[str, None] = '4b90a6ac504b' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('houses', + sa.Column('id', backend.db.models.guid.GUID(), nullable=False), + sa.Column('icon', sa.String(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('address', sa.String(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_houses_address'), 'houses', ['address'], unique=False) + op.create_index(op.f('ix_houses_created_at'), 'houses', ['created_at'], unique=False) + op.create_index(op.f('ix_houses_icon'), 'houses', ['icon'], unique=False) + op.create_index(op.f('ix_houses_id'), 'houses', ['id'], unique=False) + op.create_index(op.f('ix_houses_name'), 'houses', ['name'], unique=False) + op.create_table('areas', + sa.Column('id', backend.db.models.guid.GUID(), nullable=False), + sa.Column('house_id', backend.db.models.guid.GUID(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('desc', sa.String(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['house_id'], ['houses.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_areas_created_at'), 'areas', ['created_at'], unique=False) + op.create_index(op.f('ix_areas_desc'), 'areas', ['desc'], unique=False) + op.create_index(op.f('ix_areas_house_id'), 'areas', ['house_id'], unique=False) + op.create_index(op.f('ix_areas_id'), 'areas', ['id'], unique=False) + op.create_index(op.f('ix_areas_name'), 'areas', ['name'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_areas_name'), table_name='areas') + op.drop_index(op.f('ix_areas_id'), table_name='areas') + op.drop_index(op.f('ix_areas_house_id'), table_name='areas') + op.drop_index(op.f('ix_areas_desc'), table_name='areas') + op.drop_index(op.f('ix_areas_created_at'), table_name='areas') + op.drop_table('areas') + op.drop_index(op.f('ix_houses_name'), table_name='houses') + op.drop_index(op.f('ix_houses_id'), table_name='houses') + op.drop_index(op.f('ix_houses_icon'), table_name='houses') + op.drop_index(op.f('ix_houses_created_at'), table_name='houses') + op.drop_index(op.f('ix_houses_address'), table_name='houses') + op.drop_table('houses') + # ### end Alembic commands ### diff --git a/backend/app.py b/backend/app.py index 50393d5..c491ab7 100644 --- a/backend/app.py +++ b/backend/app.py @@ -15,7 +15,7 @@ settings = get_app_settings() logger = get_logger() description = f""" -fuware is a web application for managing your hours items and tracking them. +fuware is a web application for managing your house items and tracking them. """ @asynccontextmanager diff --git a/backend/db/migration_types.py b/backend/db/migration_types.py new file mode 100644 index 0000000..4e4f58b --- /dev/null +++ b/backend/db/migration_types.py @@ -0,0 +1 @@ +from backend.db.models.guid import GUID # noqa: F401 diff --git a/backend/db/models/__init__.py b/backend/db/models/__init__.py index 9917a30..27ef8ed 100644 --- a/backend/db/models/__init__.py +++ b/backend/db/models/__init__.py @@ -1 +1,2 @@ from .users import * +from .houses import * diff --git a/backend/db/models/guid.py b/backend/db/models/guid.py new file mode 100644 index 0000000..813bfdd --- /dev/null +++ b/backend/db/models/guid.py @@ -0,0 +1,56 @@ +import uuid +from typing import Any + +from sqlalchemy import Dialect +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.types import CHAR, TypeDecorator + + +class GUID(TypeDecorator): + """Platform-independent GUID type. + Uses PostgreSQL's UUID type, otherwise uses + CHAR(32), storing as stringified hex values. + """ + + impl = CHAR + cache_ok = True + + @staticmethod + def generate(): + return uuid.uuid4() + + @staticmethod + def convert_value_to_guid(value: Any, dialect: Dialect) -> str | None: + if value is None: + return value + elif dialect.name == "postgresql": + return str(value) + else: + if not isinstance(value, uuid.UUID): + return "%.32x" % uuid.UUID(value).int + else: + # hexstring + return "%.32x" % value.int + + def load_dialect_impl(self, dialect): + if dialect.name == "postgresql": + return dialect.type_descriptor(UUID()) + else: + return dialect.type_descriptor(CHAR(32)) + + def process_bind_param(self, value, dialect): + return self.convert_value_to_guid(value, dialect) + + def _uuid_value(self, value): + if value is None: + return value + else: + if not isinstance(value, uuid.UUID): + value = uuid.UUID(value) + return value + + def process_result_value(self, value, dialect): + return self._uuid_value(value) + + def sort_key_function(self, value): + return self._uuid_value(value) diff --git a/backend/db/models/houses/__init__.py b/backend/db/models/houses/__init__.py new file mode 100644 index 0000000..6c5960a --- /dev/null +++ b/backend/db/models/houses/__init__.py @@ -0,0 +1 @@ +from .houses import * diff --git a/backend/db/models/houses/houses.py b/backend/db/models/houses/houses.py new file mode 100644 index 0000000..b668a04 --- /dev/null +++ b/backend/db/models/houses/houses.py @@ -0,0 +1,33 @@ +from typing import List +from sqlalchemy import ForeignKey, String +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from backend.db.models.guid import GUID + +from .._model_base import SqlAlchemyBase + +class Houses(SqlAlchemyBase): + __tablename__ = 'houses' + + id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate, index=True) + icon: Mapped[str | None] = mapped_column(String, index=True, nullable=False) + name: Mapped[str | None] = mapped_column(String, index=True, nullable=False) + address: Mapped[str | None] = mapped_column(String, index=True, nullable=False) + + areas: Mapped[List["Areas"]] = relationship() + + def __repr__(self): + return f"{self.__class__.__name__}, name: {self.name}" + +class Areas(SqlAlchemyBase): + __tablename__ = 'areas' + + id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate, index=True) + house_id: Mapped[GUID] = mapped_column(GUID, ForeignKey('houses.id'), index=True, nullable=False) + name: Mapped[str | None] = mapped_column(String, index=True, nullable=False) + desc: Mapped[str | None] = mapped_column(String, index=True, nullable=False) + + user: Mapped['Houses'] = relationship(back_populates="areas") + + def __repr__(self): + return f"{self.__class__.__name__}, name: {self.name}" diff --git a/backend/db/models/users/users.py b/backend/db/models/users/users.py index 42aff88..28dde12 100644 --- a/backend/db/models/users/users.py +++ b/backend/db/models/users/users.py @@ -1,15 +1,14 @@ -from uuid import uuid4 -from sqlalchemy import Boolean, ForeignKey, String -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy import Boolean, String +from sqlalchemy.orm import Mapped, mapped_column -from sqlalchemy.dialects.postgresql import UUID +from backend.db.models.guid import GUID from .._model_base import SqlAlchemyBase -class User(SqlAlchemyBase): +class Users(SqlAlchemyBase): __tablename__ = 'users' - id: Mapped[UUID] = mapped_column(UUID, primary_key=True, default=uuid4, index=True) + id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate, index=True) username: Mapped[str | None] = mapped_column(String, unique=True, index=True, nullable=False) password: Mapped[str | None] = mapped_column(String, index=True, nullable=False) name: Mapped[str | None] = mapped_column(String, index=True, nullable=True) diff --git a/backend/repos/__init__.py b/backend/repos/__init__.py index bdb81bd..584a0fb 100644 --- a/backend/repos/__init__.py +++ b/backend/repos/__init__.py @@ -1 +1,2 @@ from .repository_users import * +from .repository_houses import * diff --git a/backend/repos/repository_houses.py b/backend/repos/repository_houses.py new file mode 100644 index 0000000..2008169 --- /dev/null +++ b/backend/repos/repository_houses.py @@ -0,0 +1,26 @@ +from backend.db.models.houses import Houses +from sqlalchemy.orm import Session + +from backend.schemas.house import HouseCreate + +class RepositoryHouses: + def __init__(self): + self.houses = Houses() + + def get_all(self, skip: int = 0, limit: int = 100): + return self.houses.query.offset(skip).limit(limit).all() + + def get_by_id(self, house_id: str): + return self.houses.query.filter_by(id=house_id).one() + + def create(self, db: Session, house: HouseCreate): + try: + db_house = Houses(**house.dict()) + db.add(db_house) + db.commit() + except Exception: + db.rollback() + raise + + db.refresh(db_house) + return db_house diff --git a/backend/repos/repository_users.py b/backend/repos/repository_users.py index d2d15b0..1198e96 100644 --- a/backend/repos/repository_users.py +++ b/backend/repos/repository_users.py @@ -1,6 +1,6 @@ from backend.core.config import get_app_settings from backend.core.security.security import hash_password -from backend.db.models import User +from backend.db.models import Users from backend.schemas import UserCreate, UserProfile from sqlalchemy.orm import Session from uuid import UUID @@ -11,7 +11,7 @@ settings = get_app_settings() class RepositoryUsers: def __init__(self): - self.user = User() + self.user = Users() def get_all(self, skip: int = 0, limit: int = 100): return self.user.query.offset(skip).limit(limit).all() @@ -25,7 +25,7 @@ class RepositoryUsers: def create(self, db: Session, user: UserCreate | UserSeeds): try: password = getattr(user, "password") - db_user = User(**user.dict(exclude={"password"}), password=hash_password(password)) + db_user = Users(**user.dict(exclude={"password"}), password=hash_password(password)) db.add(db_user) db.commit() except Exception: @@ -41,7 +41,7 @@ class RepositoryUsers: return None try: user.update_password() - self.user.query.where(User.id == user_id).update(user.dict(exclude_unset=True, exclude_none=True)) + self.user.query.where(Users.id == user_id).update(user.dict(exclude_unset=True, exclude_none=True)) db.commit() except Exception: db.rollback() diff --git a/backend/routes/__init__.py b/backend/routes/__init__.py index 2b9470e..db5200a 100644 --- a/backend/routes/__init__.py +++ b/backend/routes/__init__.py @@ -1,9 +1,10 @@ from fastapi import APIRouter -from . import (auth, user) +from . import (auth, user, house) router = APIRouter(prefix='/api') router.include_router(auth.router) router.include_router(user.router) +router.include_router(house.router) diff --git a/backend/routes/house/__init__.py b/backend/routes/house/__init__.py new file mode 100644 index 0000000..3cccca0 --- /dev/null +++ b/backend/routes/house/__init__.py @@ -0,0 +1,7 @@ + +from fastapi import APIRouter +from . import house + +router = APIRouter(prefix='/house') + +router.include_router(house.public_router) diff --git a/backend/routes/house/house.py b/backend/routes/house/house.py new file mode 100644 index 0000000..dc050b1 --- /dev/null +++ b/backend/routes/house/house.py @@ -0,0 +1,21 @@ +from typing import Annotated, Any +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from backend.core.config import get_app_settings +from backend.core.dependencies import is_logged_in +from backend.db.db_setup import generate_session +from backend.schemas.common import ReturnValue +from backend.schemas.house import HouseCreate +from backend.schemas.user import ProfileResponse +from backend.services.house import HouseService + +public_router = APIRouter(tags=["Houses: Control"]) +house_service = HouseService() +settings = get_app_settings() + +db_dependency = Annotated[Session, Depends(generate_session)] +current_user_token = Annotated[ProfileResponse, Depends(is_logged_in)] + +@public_router.get("/create", response_model=ReturnValue[Any]) +def create_house(house: HouseCreate, db: db_dependency, current_user: current_user_token) -> ReturnValue[Any]: + return ReturnValue(status=200, data="created") diff --git a/backend/schemas/__init__.py b/backend/schemas/__init__.py index deffa8e..22447fc 100644 --- a/backend/schemas/__init__.py +++ b/backend/schemas/__init__.py @@ -1,2 +1,3 @@ from .common import * from .user import * +from .house import * diff --git a/backend/schemas/house/__init__.py b/backend/schemas/house/__init__.py new file mode 100644 index 0000000..cacc29c --- /dev/null +++ b/backend/schemas/house/__init__.py @@ -0,0 +1 @@ +from .house import * diff --git a/backend/schemas/house/house.py b/backend/schemas/house/house.py new file mode 100644 index 0000000..bd394c2 --- /dev/null +++ b/backend/schemas/house/house.py @@ -0,0 +1,17 @@ +from backend.schemas.main_model import MainModel + +class HouseBase(MainModel): + pass + +class AreaBase(MainModel): + pass + +class AreaCreate(AreaBase): + name: str + desc: str + +class HouseCreate(HouseBase): + icon: str + name: str + address: str + areas: list[AreaCreate] diff --git a/backend/services/house/__init__.py b/backend/services/house/__init__.py new file mode 100644 index 0000000..40f4d69 --- /dev/null +++ b/backend/services/house/__init__.py @@ -0,0 +1 @@ +from .house_service import * diff --git a/backend/services/house/house_service.py b/backend/services/house/house_service.py new file mode 100644 index 0000000..2022856 --- /dev/null +++ b/backend/services/house/house_service.py @@ -0,0 +1,11 @@ +from sqlalchemy.orm import Session +from backend.repos import RepositoryHouses +from backend.schemas import HouseCreate +from backend.services._base_service import BaseService + +class HouseService(BaseService): + def __init__(self): + self.repos = RepositoryHouses() + + def create(self, db: Session, house: HouseCreate): + return self.repos.create(db=db, house=house) diff --git a/backend/services/user/user_service.py b/backend/services/user/user_service.py index 5a968b7..103c5a3 100644 --- a/backend/services/user/user_service.py +++ b/backend/services/user/user_service.py @@ -31,7 +31,6 @@ class UserService(BaseService): return self.repos.create(db=db, user=user) def check_exist(self, user: UserRequest): - print(f"user: {user}") db_user = self.get_by_username(username=user.username) if not db_user: return False diff --git a/frontend/index.html b/frontend/index.html index edb83bc..ad3f3e7 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -15,6 +15,7 @@
+ diff --git a/frontend/package.json b/frontend/package.json index 8f060a4..23847f5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,13 +15,15 @@ "prettier": "prettier \"src/**/*.{js,jsx}\" --write" }, "dependencies": { + "@felte/reporter-solid": "^1.2.10", + "@felte/solid": "^1.2.13", + "@felte/validator-yup": "^1.1.3", "@solidjs/meta": "^0.29.4", "@solidjs/router": "^0.13.3", "@stitches/core": "^1.2.8", "@tabler/icons-solidjs": "^3.3.0", "axios": "^1.6.8", "crypto-js": "^4.2.0", - "solid-form-handler": "^1.2.3", "solid-js": "^1.8.15", "solid-toast": "^0.5.0", "yup": "^1.4.0" diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index c0decb8..c87676b 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -8,6 +8,15 @@ importers: .: dependencies: + '@felte/reporter-solid': + specifier: ^1.2.10 + version: 1.2.10(solid-js@1.8.17) + '@felte/solid': + specifier: ^1.2.13 + version: 1.2.13(solid-js@1.8.17) + '@felte/validator-yup': + specifier: ^1.1.3 + version: 1.1.3(yup@1.4.0) '@solidjs/meta': specifier: ^0.29.4 version: 0.29.4(solid-js@1.8.17) @@ -26,9 +35,6 @@ importers: crypto-js: specifier: ^4.2.0 version: 4.2.0 - solid-form-handler: - specifier: ^1.2.3 - version: 1.2.3(solid-js@1.8.17) solid-js: specifier: ^1.8.15 version: 1.8.17 @@ -343,6 +349,32 @@ packages: resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@felte/common@1.1.8': + resolution: {integrity: sha512-VbEOfNLWfDx0SpCfeE+fNWDpvcntND4MFs7Lxd18RIjrZYH82D0wWe9th2oVF9QT5XzgBEdMF5NGIttcwU4sjg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + '@felte/core@1.4.3': + resolution: {integrity: sha512-DoXTuHD4atDG0SfTfI4orllcnriHRgM/ijMdRsUbLPL7O/UWGSWNXkxErx7XPbWOXMjX0J79KLfxZzm9abOCxw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + '@felte/reporter-solid@1.2.10': + resolution: {integrity: sha512-PA53U8faMpTfeijDH3hq6wdIGJfbHwG4OkpcNghvVsrWWvfTErAynFBLdVrqO1f4ZH1lgHK8rZfNYitChd89og==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + solid-js: ^1.2.0 + + '@felte/solid@1.2.13': + resolution: {integrity: sha512-ngJgNRe0Frxl6iypCFbpKxs+xGw3MXucttqbCDAx7rSFe4BL9NqLDr65CZhd28KY4NRjbnYgAqT0mf0HxHz9Vw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + solid-js: ^1.2.0 + + '@felte/validator-yup@1.1.3': + resolution: {integrity: sha512-wbu2tPc4CfNwqmOUWEHVcT3j4gwKdHnTvB/HbqnpMWjLbGlVU02Z9OMDWpNheQu/UvUF0yr01saAmF0Z2CJtdg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + yup: '>=1.2.0' + '@humanwhocodes/config-array@0.11.14': resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} @@ -1497,11 +1529,6 @@ packages: resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} engines: {node: '>=18'} - solid-form-handler@1.2.3: - resolution: {integrity: sha512-OCQ358dgxXeUi4TkA7D/xrrhrsUeY0K/m1EGn5ZUPMuPyx+1Hp2PjtthsAyvsaoj9jyJppK3pAwY2eytseUlZw==} - peerDependencies: - solid-js: ^1 - solid-js@1.8.17: resolution: {integrity: sha512-E0FkUgv9sG/gEBWkHr/2XkBluHb1fkrHywUgA6o6XolPDCJ4g1HaLmQufcBBhiF36ee40q+HpG/vCZu7fLpI3Q==} @@ -1972,6 +1999,27 @@ snapshots: '@eslint/js@8.57.0': {} + '@felte/common@1.1.8': {} + + '@felte/core@1.4.3': + dependencies: + '@felte/common': 1.1.8 + + '@felte/reporter-solid@1.2.10(solid-js@1.8.17)': + dependencies: + '@felte/common': 1.1.8 + solid-js: 1.8.17 + + '@felte/solid@1.2.13(solid-js@1.8.17)': + dependencies: + '@felte/core': 1.4.3 + solid-js: 1.8.17 + + '@felte/validator-yup@1.1.3(yup@1.4.0)': + dependencies: + '@felte/common': 1.1.8 + yup: 1.4.0 + '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -3102,10 +3150,6 @@ snapshots: ansi-styles: 6.2.1 is-fullwidth-code-point: 5.0.0 - solid-form-handler@1.2.3(solid-js@1.8.17): - dependencies: - solid-js: 1.8.17 - solid-js@1.8.17: dependencies: csstype: 3.1.3 diff --git a/frontend/src/App.scss b/frontend/src/App.scss index d99b35f..e7ceb1a 100644 --- a/frontend/src/App.scss +++ b/frontend/src/App.scss @@ -35,3 +35,34 @@ a { a:hover { color: #535bf2; } + +.scroll-shadow-horizontal { + background: + linear-gradient(90deg,white 33%,rgba(255,255,255,0)), + linear-gradient(90deg,rgba(255,255,255,0),white 66%) 0 100%, + radial-gradient(farthest-side at 0 50%,rgba(0,0,0,.1),transparent), + radial-gradient(farthest-side at 100% 50%,rgba(0,0,0,.1),transparent) 0 100%; + background-repeat: no-repeat; + background-attachment: local, local, scroll, scroll; + background-position: 0 0,100%,0 0,100%; + background-size: 20px 100%, 20px 100%, 10px 100%, 10px 100%; +} + +.scroll-shadow-vertical { + background: + linear-gradient(white 30%, rgba(255, 255, 255, 0)) center top, + linear-gradient(rgba(255, 255, 255, 0), white 70%) center bottom, + radial-gradient(farthest-side at 50% 0, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)) center top, + radial-gradient(farthest-side at 50% 100%, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)) center bottom; + background-repeat: no-repeat; + background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px; + background-attachment: local, local, scroll, scroll; +} + +.input, +.textarea { + &:focus, + &:focus-within { + outline-color: transparent !important; + } +} diff --git a/frontend/src/components/AreaAdd/AreaAdd.jsx b/frontend/src/components/AreaAdd/AreaAdd.jsx new file mode 100644 index 0000000..4af99e5 --- /dev/null +++ b/frontend/src/components/AreaAdd/AreaAdd.jsx @@ -0,0 +1,145 @@ +import Popup from '@components/common/Popup' +import TextInput from '@components/common/TextInput' +import Textarea from '@components/common/Textarea' +import { createForm } from '@felte/solid' +import { validator } from '@felte/validator-yup' +import useLanguage from '@hooks/useLanguage' +import useToast from '@hooks/useToast' +import { + IconAddressBook, + IconCirclePlus, + IconFileDescription, + IconInfoCircle, + IconVector, +} from '@tabler/icons-solidjs' +import { For, Show, createSignal } from 'solid-js' +import * as yup from 'yup' +import AreaItem from './AreaItem' + +/** + * Returns a Yup schema object for validating an area object. + * + * @param {Object} language - An object containing the language settings. + * @param {Function} isRequired - A function that returns a validation message for required fields. + * @return {Object} A Yup schema object with two required fields: name and description. + */ +const areaSchema = (language, isRequired) => + yup.object({ + name: yup.string().required(isRequired(language.ui.areaName)), + description: yup.string().required(isRequired(language.ui.areaName)), + }) + +export default function AreaAdd(props) { + const [openModal, setOpenModal] = createSignal(false) + const [data, setData] = createSignal([]) + const { language, isRequired } = useLanguage() + const notify = useToast() + const { form, reset, errors } = createForm({ + extend: [validator({ schema: areaSchema(language, isRequired) })], + onSubmit: async (values) => { + setData((prev) => [...prev, values]) + onModalClose() + }, + }) + + const onModalClose = () => { + setOpenModal(false), reset() + } + + const onOpenModal = () => { + setOpenModal(true) + } + + const onDeleteAreaItem = (index) => { + setData((prev) => [...prev.slice(0, index), ...prev.slice(index + 1)]) + } + + // console.log(error()) + + return ( +
+
+
+ + +
+
+
+ 0} fallback={language.ui.empty}> + + {(item, index) => ( + + )} + + +
+
+
+ } + title={language.ui.addArea} + titleClass="text-lg" + openModal={openModal()} + onModalClose={onModalClose} + class="!w-6/12 !max-w-5xl" + > +
+