[haiz] haiz
This commit is contained in:
parent
e6923277ed
commit
1c205c69ac
@ -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():
|
||||
|
@ -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 ""}
|
||||
|
@ -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),
|
70
alembic/versions/7ef4aef5fcda_create_house_and_area_table.py
Normal file
70
alembic/versions/7ef4aef5fcda_create_house_and_area_table.py
Normal file
@ -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 ###
|
@ -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
|
||||
|
1
backend/db/migration_types.py
Normal file
1
backend/db/migration_types.py
Normal file
@ -0,0 +1 @@
|
||||
from backend.db.models.guid import GUID # noqa: F401
|
@ -1 +1,2 @@
|
||||
from .users import *
|
||||
from .houses import *
|
||||
|
56
backend/db/models/guid.py
Normal file
56
backend/db/models/guid.py
Normal file
@ -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)
|
1
backend/db/models/houses/__init__.py
Normal file
1
backend/db/models/houses/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .houses import *
|
33
backend/db/models/houses/houses.py
Normal file
33
backend/db/models/houses/houses.py
Normal file
@ -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}"
|
@ -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)
|
||||
|
@ -1 +1,2 @@
|
||||
from .repository_users import *
|
||||
from .repository_houses import *
|
||||
|
26
backend/repos/repository_houses.py
Normal file
26
backend/repos/repository_houses.py
Normal file
@ -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
|
@ -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()
|
||||
|
@ -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)
|
||||
|
7
backend/routes/house/__init__.py
Normal file
7
backend/routes/house/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
from fastapi import APIRouter
|
||||
from . import house
|
||||
|
||||
router = APIRouter(prefix='/house')
|
||||
|
||||
router.include_router(house.public_router)
|
21
backend/routes/house/house.py
Normal file
21
backend/routes/house/house.py
Normal file
@ -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")
|
@ -1,2 +1,3 @@
|
||||
from .common import *
|
||||
from .user import *
|
||||
from .house import *
|
||||
|
1
backend/schemas/house/__init__.py
Normal file
1
backend/schemas/house/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .house import *
|
17
backend/schemas/house/house.py
Normal file
17
backend/schemas/house/house.py
Normal file
@ -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]
|
1
backend/services/house/__init__.py
Normal file
1
backend/services/house/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .house_service import *
|
11
backend/services/house/house_service.py
Normal file
11
backend/services/house/house_service.py
Normal file
@ -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)
|
@ -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
|
||||
|
@ -15,6 +15,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="modal-portal"></div>
|
||||
<script type="module" src="/src/index.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -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"
|
||||
|
68
frontend/pnpm-lock.yaml
generated
68
frontend/pnpm-lock.yaml
generated
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
145
frontend/src/components/AreaAdd/AreaAdd.jsx
Normal file
145
frontend/src/components/AreaAdd/AreaAdd.jsx
Normal file
@ -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 (
|
||||
<div class="form-control mb-3">
|
||||
<div class="join join-vertical">
|
||||
<div
|
||||
class="flex items-center justify-between bg-base-200 border border-gray-300 h-12 px-3 join-item"
|
||||
classList={{ 'border-red-500': props.error }}
|
||||
>
|
||||
<label class="label justify-start gap-2">
|
||||
<IconVector size={18} />
|
||||
<span class="label-text font-bold whitespace-nowrap">
|
||||
{language.ui.areas}
|
||||
</span>
|
||||
<Show when={props.error}>
|
||||
<div
|
||||
class="tooltip tooltip-right tooltip-error before:text-white text-red-500"
|
||||
data-tip={props.error}
|
||||
>
|
||||
<IconInfoCircle size={18} />
|
||||
</div>
|
||||
</Show>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-circle btn-xs text-green-400 hover:bg-green-100 hover:text-green-500"
|
||||
onClick={onOpenModal}
|
||||
>
|
||||
<IconCirclePlus size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="scroll-shadow-vertical no-scrollbar border border-gray-300 join-item"
|
||||
classList={{ 'border-red-500': props.error }}
|
||||
>
|
||||
<div class="p-3 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<Show when={data().length > 0} fallback={language.ui.empty}>
|
||||
<For each={data()}>
|
||||
{(item, index) => (
|
||||
<AreaItem
|
||||
{...item}
|
||||
name={props.name}
|
||||
key={index()}
|
||||
onDelete={onDeleteAreaItem}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Popup
|
||||
icon={<IconCirclePlus size={20} class="text-green-500" />}
|
||||
title={language.ui.addArea}
|
||||
titleClass="text-lg"
|
||||
openModal={openModal()}
|
||||
onModalClose={onModalClose}
|
||||
class="!w-6/12 !max-w-5xl"
|
||||
>
|
||||
<form autocomplete="off" use:form>
|
||||
<div class="modal-body mt-4">
|
||||
<TextInput
|
||||
icon={IconAddressBook}
|
||||
name="name"
|
||||
label={language.ui.areaName}
|
||||
placeholder={language.ui.areaName}
|
||||
error={errors('name')}
|
||||
/>
|
||||
<Textarea
|
||||
icon={IconFileDescription}
|
||||
name="description"
|
||||
label={language.ui.areaDesc}
|
||||
placeholder={language.ui.areaDesc}
|
||||
error={errors('description')}
|
||||
/>
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{language.ui.save}
|
||||
</button>
|
||||
<button type="button" class="btn btn-ghost" onClick={onModalClose}>
|
||||
{language.ui.cancel}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Popup>
|
||||
</div>
|
||||
)
|
||||
}
|
32
frontend/src/components/AreaAdd/AreaItem.jsx
Normal file
32
frontend/src/components/AreaAdd/AreaItem.jsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { IconTrash } from '@tabler/icons-solidjs'
|
||||
import { Show } from 'solid-js'
|
||||
|
||||
export default function AreaItem(props) {
|
||||
return (
|
||||
<div class="flex justify-between items-center shadow rounded-lg p-3 border border-gray-300">
|
||||
<div class="">
|
||||
<span class="text-md font-bold">{props.name}</span>
|
||||
<p class="text-xs">{props.description}</p>
|
||||
<input
|
||||
type="hidden"
|
||||
name={`${props.name}.${props.key}.name`}
|
||||
value={props.name}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name={`${props.name}.${props.key}.desc`}
|
||||
value={props.description}
|
||||
/>
|
||||
</div>
|
||||
<Show when={props.onDelete}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-circle btn-ghost btn-sm text-red-500 hover:bg-red-100"
|
||||
onClick={() => props.onDelete(props.key)}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
2
frontend/src/components/AreaAdd/index.js
Normal file
2
frontend/src/components/AreaAdd/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './AreaAdd'
|
||||
export { default } from './AreaAdd'
|
@ -31,8 +31,7 @@ export default function Header() {
|
||||
try {
|
||||
await clickLogOut()
|
||||
} catch (error) {
|
||||
console.log({
|
||||
status: 'danger',
|
||||
notify.error({
|
||||
title: 'Logout fail!',
|
||||
closable: false,
|
||||
})
|
||||
|
@ -12,7 +12,7 @@ import { For, Show, mergeProps } from 'solid-js'
|
||||
import { Dynamic } from 'solid-js/web'
|
||||
import './navbar.scss'
|
||||
|
||||
const language = useLanguage()
|
||||
const { language } = useLanguage()
|
||||
|
||||
export const NAV_ITEM = (admin = false) => [
|
||||
{
|
||||
|
22
frontend/src/components/common/ConfirmPopup/ConfirmPopup.jsx
Normal file
22
frontend/src/components/common/ConfirmPopup/ConfirmPopup.jsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { IconCirclePlus } from '@tabler/icons-solidjs'
|
||||
import { Show, createSignal } from 'solid-js'
|
||||
import Popup from '../Popup'
|
||||
|
||||
export default function ConfirmPopup(props) {
|
||||
const [openModal, setOpenModal] = createSignal(true)
|
||||
|
||||
return (
|
||||
<Show when={openModal()}>
|
||||
<Popup
|
||||
icon={<IconCirclePlus size={20} class="text-green-500" />}
|
||||
title="abc"
|
||||
titleClass="text-lg"
|
||||
openModal={openModal()}
|
||||
onModalClose={() => setOpenModal(false)}
|
||||
class="!w-4/12 !max-w-4xl"
|
||||
>
|
||||
<div class="modal-body">{props.children}</div>
|
||||
</Popup>
|
||||
</Show>
|
||||
)
|
||||
}
|
2
frontend/src/components/common/ConfirmPopup/index.js
Normal file
2
frontend/src/components/common/ConfirmPopup/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './ConfirmPopup'
|
||||
export { default } from './ConfirmPopup'
|
27
frontend/src/components/common/Popup/Popup.jsx
Normal file
27
frontend/src/components/common/Popup/Popup.jsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { IconX } from '@tabler/icons-solidjs'
|
||||
import { Dynamic, Portal } from 'solid-js/web'
|
||||
|
||||
export default function Popup(props) {
|
||||
return (
|
||||
<Portal mount={document.getElementById('modal-portal')}>
|
||||
<div class="modal" classList={{ 'modal-open': props.openModal }}>
|
||||
<div class={`modal-box ${props.class || ''}`}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
|
||||
onClick={() => props.onModalClose()}
|
||||
>
|
||||
<IconX size={18} />
|
||||
</button>
|
||||
<h3
|
||||
class={`flex jutify-center items-center gap-2 text-lg font-bold ${props.titleClass || ''}`}
|
||||
>
|
||||
<Dynamic component={props.icon} />
|
||||
{props.title}
|
||||
</h3>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
)
|
||||
}
|
2
frontend/src/components/common/Popup/index.js
Normal file
2
frontend/src/components/common/Popup/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './Popup'
|
||||
export { default } from './Popup'
|
@ -1,48 +1,41 @@
|
||||
import { Field } from 'solid-form-handler'
|
||||
import { IconInfoCircle } from '@tabler/icons-solidjs'
|
||||
import { Show, splitProps } from 'solid-js'
|
||||
import { Dynamic } from 'solid-js/web'
|
||||
|
||||
export default function Finput(props) {
|
||||
export default function Textinput(props) {
|
||||
const [local, rest] = splitProps(props, ['label', 'icon'])
|
||||
|
||||
return (
|
||||
<Field
|
||||
{...props}
|
||||
mode="input"
|
||||
render={(field) => (
|
||||
<div class="form-control w-full [&:not(:last-child)]:pb-3">
|
||||
<Show when={local.label && !props.line}>
|
||||
<div class="label">
|
||||
<span class="label-text">{local.label}</span>
|
||||
</div>
|
||||
</Show>
|
||||
<div class="join">
|
||||
<Show when={local.icon}>
|
||||
<div class="form-control w-full [&:not(:last-child)]:pb-3">
|
||||
<div class="join join-vertical">
|
||||
<div
|
||||
class="flex items-center justify-between bg-base-200 border border-gray-300 h-12 px-3 join-item"
|
||||
classList={{ 'border-red-500': props.error }}
|
||||
>
|
||||
<label class="label justify-start gap-2 bg-base-200">
|
||||
<Show when={local.icon && local.label}>
|
||||
<Dynamic component={local.icon} size={18} />
|
||||
<span class="label-text font-bold whitespace-nowrap">
|
||||
{local.label}
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={props.error}>
|
||||
<div
|
||||
class={`join-item flex items-center bg-base-200 px-3 border border-gray-300 w-auto ${props.labelClass ? props.labelClass : ''}`}
|
||||
class="tooltip tooltip-right tooltip-error before:text-white text-red-500"
|
||||
data-tip={props.error}
|
||||
>
|
||||
<Dynamic component={local.icon} size={18} />
|
||||
<Show when={local.label && !!props.line}>
|
||||
<span class="ml-2 hidden md:block">{local.label}</span>
|
||||
</Show>
|
||||
<IconInfoCircle size={18} />
|
||||
</div>
|
||||
</Show>
|
||||
<label
|
||||
class="input input-bordered flex items-center gap-2 w-full join-item"
|
||||
classList={{ 'input-error': field.helpers.error }}
|
||||
>
|
||||
<input {...rest} class="grow w-full" {...field.props} />
|
||||
</label>
|
||||
</div>
|
||||
<Show when={field.helpers.error}>
|
||||
<div class="label">
|
||||
<span class="label-text-alt text-red-600">
|
||||
{field.helpers.errorMessage}
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
</label>
|
||||
{props.children}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<input
|
||||
{...rest}
|
||||
class="input input-bordered w-full join-item"
|
||||
classList={{ 'input-error': props.error }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
39
frontend/src/components/common/Textarea/Textarea.jsx
Normal file
39
frontend/src/components/common/Textarea/Textarea.jsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { IconInfoCircle } from '@tabler/icons-solidjs'
|
||||
import { Show, splitProps } from 'solid-js'
|
||||
import { Dynamic } from 'solid-js/web'
|
||||
|
||||
export default function Textarea(props) {
|
||||
const [local, rest] = splitProps(props, ['label', 'icon'])
|
||||
|
||||
return (
|
||||
<label class="form-control w-full [&:not(:last-child)]:pb-3">
|
||||
<div class="join join-vertical">
|
||||
<label
|
||||
class="label justify-start gap-2 bg-base-200 border border-gray-300 h-12 px-3 join-item"
|
||||
classList={{ 'border-red-500': props.error }}
|
||||
>
|
||||
<Show when={local.icon && local.label}>
|
||||
<Dynamic component={local.icon} size={18} />
|
||||
<span class="label-text font-bold whitespace-nowrap">
|
||||
{local.label}
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={props.error}>
|
||||
<div
|
||||
class="tooltip tooltip-right tooltip-error before:text-white text-red-500"
|
||||
data-tip={props.error}
|
||||
>
|
||||
<IconInfoCircle size={18} />
|
||||
</div>
|
||||
</Show>
|
||||
</label>
|
||||
|
||||
<textarea
|
||||
{...rest}
|
||||
class={`textarea textarea-bordered h-24 w-full resize-none join-item ${props.class || ''}`}
|
||||
classList={{ 'textarea-error': props.error }}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
)
|
||||
}
|
2
frontend/src/components/common/Textarea/index.js
Normal file
2
frontend/src/components/common/Textarea/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './Textarea'
|
||||
export { default } from './Textarea'
|
@ -36,11 +36,7 @@ export function SiteContextProvider(props) {
|
||||
}
|
||||
|
||||
const setUser = (user) => {
|
||||
setStore(
|
||||
produce((s) => {
|
||||
s.userInfo = user
|
||||
}),
|
||||
)
|
||||
setStore('userInfo', user)
|
||||
setLocalStore()
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@ import { Helpers } from '@utils/helper'
|
||||
export default function useAuth(setAuth) {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const clickLogIn = async (username, password, cbFormReset) => {
|
||||
const clickLogIn = async (username, password) => {
|
||||
const resp = await postLogin({ username, password })
|
||||
|
||||
if (resp.status === 200) {
|
||||
@ -17,7 +17,6 @@ export default function useAuth(setAuth) {
|
||||
localStorage.setItem(LOGIN_KEY, Helpers.encrypt(JSON.stringify(rest)))
|
||||
}
|
||||
|
||||
cbFormReset()
|
||||
navigate('/', { replace: true })
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,13 @@
|
||||
/**
|
||||
* Returns an object containing the language messages and a function to generate
|
||||
* a required field message based on the provided selectLanguage parameter.
|
||||
*
|
||||
* @param {string} selectLanguage - The language code to use for the messages.
|
||||
* @return {Object} An object with two properties:
|
||||
* - language: An object containing the language messages.
|
||||
* - isRequired: A function that takes a field name and returns a required field
|
||||
* message in the selected language.
|
||||
*/
|
||||
export default function useLanguage(selectLanguage = 'vi') {
|
||||
const data = import.meta.glob('@lang/*.json', {
|
||||
import: 'default',
|
||||
@ -11,5 +21,14 @@ export default function useLanguage(selectLanguage = 'vi') {
|
||||
imp[keypath] = data[path]
|
||||
}
|
||||
|
||||
return imp[selectLanguage]
|
||||
/**
|
||||
* Returns a string representing a required field message in the selected language.
|
||||
*
|
||||
* @param {string} fieldName - The name of the field.
|
||||
* @return {string} The required field message.
|
||||
*/
|
||||
const isRequired = (fieldName) =>
|
||||
imp[selectLanguage].message['IS_REQUIRED'].replace('%s', fieldName)
|
||||
|
||||
return { language: imp[selectLanguage], isRequired }
|
||||
}
|
||||
|
@ -1,6 +1,19 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer utilities {
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
}
|
||||
|
||||
@import './assets/css/fu-theme.scss';
|
||||
|
||||
:root {
|
||||
|
@ -11,7 +11,7 @@
|
||||
"clear": "Xóa",
|
||||
"house": "Nhà",
|
||||
"action": "Thao Tác",
|
||||
"createHouse": "Tạo mới nhà",
|
||||
"createNew": "Tạo mới",
|
||||
"location": "Nhà kho",
|
||||
"displayName": "Tên hiển thị",
|
||||
"newPassword": "Mật khẩu mới",
|
||||
@ -20,7 +20,17 @@
|
||||
"houseName": "Tên nhà",
|
||||
"houseIcon": "Ký tự nhà",
|
||||
"houseAddress": "Địa chỉ nhà",
|
||||
"create": "Tạo"
|
||||
"areas": "Khu vực",
|
||||
"areaName": "Tên khu vực",
|
||||
"areaDesc": "Mô tả",
|
||||
"addArea": "Thêm khu vực",
|
||||
"create": "Tạo",
|
||||
"update": "Cập nhật",
|
||||
"delete": "Xóa",
|
||||
"confirm": "Xác nhận",
|
||||
"cancel": "Huỷ",
|
||||
"findIconHere": "Tìm ở đây",
|
||||
"empty": "Trống"
|
||||
},
|
||||
"table": {
|
||||
"columnName": {
|
||||
@ -34,6 +44,8 @@
|
||||
"message": {
|
||||
"CREATED_USER": "Username already registered!",
|
||||
"LOGIN_WRONG": "Your username or password input is wrong!",
|
||||
"USER_LOCK": "Your Account was locked"
|
||||
"USER_LOCK": "Your Account was locked",
|
||||
"IS_REQUIRED": "%s là bắt buộc",
|
||||
"PASSWORD_MUSTMATCH": "Cần nhập trùng với mật khẩu"
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { NAV_ITEM } from '@components/Navbar'
|
||||
import { useSiteContext } from '@context/SiteContext'
|
||||
import { useNavigate } from '@solidjs/router'
|
||||
import { onMount } from 'solid-js'
|
||||
import { createEffect } from 'solid-js'
|
||||
|
||||
function getFirstItem(array) {
|
||||
const first = array.filter((item) => item.show)[0]
|
||||
@ -15,9 +15,11 @@ export default function Home() {
|
||||
const { store } = useSiteContext()
|
||||
const navigate = useNavigate()
|
||||
|
||||
onMount(() => {
|
||||
const first = getFirstItem(NAV_ITEM(store?.userInfo?.isAdmin))
|
||||
navigate(first ? first.path : '/me', { replace: true })
|
||||
createEffect(() => {
|
||||
if (store?.userInfo?.isAdmin) {
|
||||
const first = getFirstItem(NAV_ITEM(store?.userInfo?.isAdmin))
|
||||
navigate(first ? first.path : '/me', { replace: true })
|
||||
}
|
||||
})
|
||||
|
||||
return <></>
|
||||
|
@ -8,14 +8,23 @@ import {
|
||||
IconHome2,
|
||||
IconHomeDot,
|
||||
IconPencil,
|
||||
IconSquareRoundedPlus,
|
||||
IconTrash,
|
||||
} from '@tabler/icons-solidjs'
|
||||
import './house.scss'
|
||||
|
||||
export default function House() {
|
||||
const language = useLanguage()
|
||||
const { language } = useLanguage()
|
||||
const [view, setView] = createSignal(VIEWDATA['list'])
|
||||
|
||||
const onEdit = () => {
|
||||
console.log('edit')
|
||||
}
|
||||
|
||||
const onDelete = () => {
|
||||
console.log('delete')
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="house">
|
||||
<div class="flex items-center gap-2 mb-5 text-xl">
|
||||
@ -30,53 +39,68 @@ export default function House() {
|
||||
href="/house/create"
|
||||
class="btn btn-success text-white hover:text-white btn-sm"
|
||||
>
|
||||
{language.ui.createHouse}
|
||||
<IconSquareRoundedPlus size={15} />
|
||||
{language.ui.createNew}
|
||||
</A>
|
||||
</div>
|
||||
<div
|
||||
class="view-layout"
|
||||
class="view-layout scroll-shadow-horizontal no-scrollbar"
|
||||
classList={{
|
||||
'view-list': view() === VIEWDATA['list'],
|
||||
'view-grid': view() === VIEWDATA['grid'],
|
||||
}}
|
||||
>
|
||||
<div class="row view-head">
|
||||
<div class="col w-1/12">{language.table.columnName.no}</div>
|
||||
<div class="col w-1/12">{language.table.columnName.icon}</div>
|
||||
<div class="col w-4/12">{language.table.columnName.name}</div>
|
||||
<div class="col w-4/12">{language.table.columnName.address}</div>
|
||||
<div class="col w-2/12">{language.table.columnName.action}</div>
|
||||
</div>
|
||||
<div class="row view-item">
|
||||
<div class="col hide w-1/12">1</div>
|
||||
<div class="col w-1/12">
|
||||
<IconHome2 size={21} />
|
||||
<div class="view-table">
|
||||
<div class="row view-head">
|
||||
<div class="col w-1/12">{language.table.columnName.no}</div>
|
||||
<div class="col w-1/12">{language.table.columnName.icon}</div>
|
||||
<div class="col w-4/12">{language.table.columnName.name}</div>
|
||||
<div class="col w-4/12">{language.table.columnName.address}</div>
|
||||
<div class="col w-2/12">{language.table.columnName.action}</div>
|
||||
</div>
|
||||
<div class="col w-4/12">Nhà 1</div>
|
||||
<div class="col w-4/12">Data 4</div>
|
||||
<div class="col actionbar w-2/12">
|
||||
<button class="btn btn-ghost btn-sm px-1 text-blue-500 mr-2">
|
||||
<IconPencil size={20} />
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm px-1 text-red-500">
|
||||
<IconTrash size={20} />
|
||||
</button>
|
||||
<div class="row view-item">
|
||||
<div class="col hide w-1/12">1</div>
|
||||
<div class="col w-1/12">
|
||||
<IconHome2 size={21} />
|
||||
</div>
|
||||
<div class="col w-4/12">Nhà 1</div>
|
||||
<div class="col w-4/12">Data 4</div>
|
||||
<div class="col actionbar w-2/12">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm px-1 text-blue-500 mr-2"
|
||||
onClick={onEdit}
|
||||
>
|
||||
<IconPencil size={20} />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-ghost btn-sm px-1 text-red-500"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row view-item">
|
||||
<div class="col hide w-1/12">2</div>
|
||||
<div class="col w-1/12">
|
||||
<IconHomeDot size={21} />
|
||||
</div>
|
||||
<div class="col w-4/12">Nhà 2</div>
|
||||
<div class="col w-4/12">Data 4</div>
|
||||
<div class="col actionbar w-2/12">
|
||||
<button class="btn btn-ghost btn-sm px-1 text-blue-500 mr-2">
|
||||
<IconPencil size={20} />
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm px-1 text-red-500">
|
||||
<IconTrash size={20} />
|
||||
</button>
|
||||
<div class="row view-item">
|
||||
<div class="col hide w-1/12">2</div>
|
||||
<div class="col w-1/12">
|
||||
<IconHomeDot size={21} />
|
||||
</div>
|
||||
<div class="col w-4/12">Nhà 2</div>
|
||||
<div class="col w-4/12">Data 4</div>
|
||||
<div class="col actionbar w-2/12">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm px-1 text-blue-500 mr-2"
|
||||
onClick={onEdit}
|
||||
>
|
||||
<IconPencil size={20} />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-ghost btn-sm px-1 text-red-500"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,7 +1,12 @@
|
||||
.view-layout {
|
||||
&.view-list {
|
||||
@apply flex flex-col rounded-2xl border shadow-md overflow-hidden;
|
||||
& > .row {
|
||||
@apply rounded-2xl border shadow-md overflow-auto;
|
||||
|
||||
& .view-table {
|
||||
@apply flex flex-col min-w-[800px];
|
||||
}
|
||||
|
||||
& .row {
|
||||
@apply flex justify-between items-center flex-nowrap px-3 border-b border-gray-200 h-14;
|
||||
|
||||
&.view-head {
|
||||
@ -11,13 +16,17 @@
|
||||
}
|
||||
|
||||
&.view-grid {
|
||||
@apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5 mt-5;
|
||||
@apply mt-5;
|
||||
|
||||
& > .row {
|
||||
& .view-table {
|
||||
@apply grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-5;
|
||||
}
|
||||
|
||||
& .row {
|
||||
&.view-item {
|
||||
@apply card bg-base-100 shadow-md border border-gray-200 p-3 relative pb-8;
|
||||
|
||||
& > .col {
|
||||
& .col {
|
||||
@apply w-auto
|
||||
}
|
||||
|
||||
@ -27,11 +36,8 @@
|
||||
}
|
||||
|
||||
& > .actionbar {
|
||||
@apply absolute right-0 bottom-0 flex px-3 py-1 w-auto invisible;
|
||||
}
|
||||
|
||||
&:hover > .actionbar {
|
||||
@apply visible;
|
||||
@apply absolute right-0 bottom-0 flex px-3 py-1 w-auto border-t border-l rounded-tl-xl;
|
||||
box-shadow: -1px -1px 10px 0px rgba(0,0,0,0.1);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,35 +1,33 @@
|
||||
import AreaAdd from '@components/AreaAdd'
|
||||
import TextInput from '@components/common/TextInput'
|
||||
import { createForm } from '@felte/solid'
|
||||
import { validator } from '@felte/validator-yup'
|
||||
import useLanguage from '@hooks/useLanguage'
|
||||
import { A } from '@solidjs/router'
|
||||
import {
|
||||
IconAddressBook,
|
||||
IconHome,
|
||||
IconHomePlus,
|
||||
IconIcons,
|
||||
IconTag,
|
||||
} from '@tabler/icons-solidjs'
|
||||
import { useFormHandler } from 'solid-form-handler'
|
||||
import { yupSchema } from 'solid-form-handler/yup'
|
||||
import * as yup from 'yup'
|
||||
|
||||
const houseSchema = yup.object({
|
||||
name: yup.string().required('Name is required'),
|
||||
password: yup.string().nullable().optional(),
|
||||
'confirm-password': yup.string().when('password', {
|
||||
is: (val) => !!(val && val.length > 0),
|
||||
then: (schema) =>
|
||||
schema.oneOf([yup.ref('password'), null], 'Passwords must match'),
|
||||
}),
|
||||
})
|
||||
const houseSchema = (language, isRequired) =>
|
||||
yup.object({
|
||||
icon: yup.string().required(isRequired(language.ui.houseIcon)),
|
||||
name: yup.string().required(isRequired(language.ui.houseName)),
|
||||
address: yup.string().required(isRequired(language.ui.houseAddress)),
|
||||
areas: yup.array().required(isRequired(language.ui.areas)),
|
||||
})
|
||||
|
||||
export default function HouseCreate() {
|
||||
const language = useLanguage()
|
||||
const formHandler = useFormHandler(yupSchema(houseSchema))
|
||||
const { formData } = formHandler
|
||||
|
||||
const submit = async (event) => {
|
||||
event.preventDefault()
|
||||
await formHandler.validateForm()
|
||||
}
|
||||
const { language, isRequired } = useLanguage()
|
||||
const { form, errors } = createForm({
|
||||
extend: [validator({ schema: houseSchema(language, isRequired) })],
|
||||
onSubmit: async (values) => {
|
||||
console.log(values)
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="house-create">
|
||||
@ -43,40 +41,58 @@ export default function HouseCreate() {
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mb-5 text-xl">
|
||||
<span class="text-secondary">
|
||||
<IconHome size={30} />
|
||||
<IconHomePlus size={30} />
|
||||
</span>
|
||||
{language.ui.newHouse}
|
||||
</div>
|
||||
<div class="card w-full bg-base-100 shadow-lg border border-gray-200">
|
||||
<div class="card-body">
|
||||
<form autoComplete="off" onSubmit={submit}>
|
||||
<TextInput
|
||||
icon={IconIcons}
|
||||
name="icon"
|
||||
label={language.ui.houseIcon}
|
||||
line
|
||||
labelClass="md:w-40"
|
||||
placeholder={language.ui.houseIcon}
|
||||
formHandler={formHandler}
|
||||
/>
|
||||
<TextInput
|
||||
icon={IconTag}
|
||||
name="name"
|
||||
label={language.ui.houseName}
|
||||
line
|
||||
labelClass="md:w-40"
|
||||
placeholder={language.ui.houseName}
|
||||
formHandler={formHandler}
|
||||
/>
|
||||
<TextInput
|
||||
icon={IconAddressBook}
|
||||
name="address"
|
||||
label={language.ui.houseAddress}
|
||||
line
|
||||
labelClass="md:w-40"
|
||||
placeholder={language.ui.houseAddress}
|
||||
formHandler={formHandler}
|
||||
/>
|
||||
<form autoComplete="off" use:form>
|
||||
<div class="grid gap-4 grid-cols-1 lg:grid-cols-2">
|
||||
<div class="col-auto">
|
||||
<TextInput
|
||||
icon={IconIcons}
|
||||
name="icon"
|
||||
label={language.ui.houseIcon}
|
||||
labelClass="md:w-40"
|
||||
placeholder={language.ui.houseIcon}
|
||||
error={errors('icon')}
|
||||
>
|
||||
<div class="label">
|
||||
<a
|
||||
class="label-text link link-info"
|
||||
href="https://tabler.io/icons"
|
||||
target="_blank"
|
||||
>
|
||||
{language.ui.findIconHere}
|
||||
</a>
|
||||
</div>
|
||||
</TextInput>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<TextInput
|
||||
icon={IconTag}
|
||||
name="name"
|
||||
label={language.ui.houseName}
|
||||
labelClass="md:w-40"
|
||||
placeholder={language.ui.houseName}
|
||||
error={errors('name')}
|
||||
/>
|
||||
</div>
|
||||
<div class="col-auto lg:col-span-2">
|
||||
<TextInput
|
||||
icon={IconAddressBook}
|
||||
name="address"
|
||||
label={language.ui.houseAddress}
|
||||
labelClass="md:w-40"
|
||||
placeholder={language.ui.houseAddress}
|
||||
error={errors('address')}
|
||||
/>
|
||||
</div>
|
||||
<div class="col-auto lg:col-span-2">
|
||||
<AreaAdd name="areas" error={errors('areas')} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{language.ui.create}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { useSiteContext } from '@context/SiteContext'
|
||||
import { createForm } from '@felte/solid'
|
||||
import { validator } from '@felte/validator-yup'
|
||||
import useLanguage from '@hooks/useLanguage'
|
||||
import { useNavigate } from '@solidjs/router'
|
||||
import { IconKey, IconUser } from '@tabler/icons-solidjs'
|
||||
import { useFormHandler } from 'solid-form-handler'
|
||||
import { yupSchema } from 'solid-form-handler/yup'
|
||||
import { onMount } from 'solid-js'
|
||||
import * as yup from 'yup'
|
||||
import './login.scss'
|
||||
@ -13,20 +13,36 @@ import TextInput from '@components/common/TextInput'
|
||||
import useAuth from '@hooks/useAuth'
|
||||
import useToast from '@hooks/useToast'
|
||||
|
||||
const loginSchema = yup.object({
|
||||
username: yup.string().required('Username is required'),
|
||||
password: yup.string().required('Password is required'),
|
||||
})
|
||||
|
||||
const language = useLanguage()
|
||||
const loginSchema = (language, isRequired) =>
|
||||
yup.object({
|
||||
username: yup.string().required(isRequired(language.ui.username)),
|
||||
password: yup.string().required(isRequired(language.ui.password)),
|
||||
})
|
||||
|
||||
export default function Login() {
|
||||
const { store, setAuth } = useSiteContext()
|
||||
const { language, isRequired } = useLanguage()
|
||||
const navigate = useNavigate()
|
||||
const { clickLogIn } = useAuth(setAuth)
|
||||
const notify = useToast()
|
||||
const formHandler = useFormHandler(yupSchema(loginSchema))
|
||||
const { formData } = formHandler
|
||||
|
||||
const { form, errors } = createForm({
|
||||
extend: [validator({ schema: loginSchema(language, isRequired) })],
|
||||
onSubmit: async (values) => {
|
||||
try {
|
||||
const { username, password } = values
|
||||
return await clickLogIn(username, password)
|
||||
} catch (error) {
|
||||
notify.error({
|
||||
title: 'Login fail!',
|
||||
description: error?.data
|
||||
? language.message[error.data]
|
||||
: 'Your username or password input is wrong!',
|
||||
closable: true,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
if (store.auth) {
|
||||
@ -34,28 +50,6 @@ export default function Login() {
|
||||
}
|
||||
})
|
||||
|
||||
const submit = async (event) => {
|
||||
event.preventDefault()
|
||||
await formHandler.validateForm()
|
||||
try {
|
||||
const { username, password } = formData()
|
||||
await clickLogIn(username, password, formHandler.resetForm)
|
||||
notify.success({
|
||||
title: 'Login success!',
|
||||
description: 'Welcome back!',
|
||||
closable: true,
|
||||
})
|
||||
} catch (error) {
|
||||
notify.error({
|
||||
title: 'Login fail!',
|
||||
description: error?.data
|
||||
? language.message[error.data]
|
||||
: 'Your username or password input is wrong!',
|
||||
closable: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="login-page">
|
||||
<div class="card glass card-compact login-wrap shadow-xl">
|
||||
@ -67,21 +61,23 @@ export default function Login() {
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">{language.ui.login}</h1>
|
||||
<form autoComplete="off" onSubmit={submit}>
|
||||
<form autoComplete="off" use:form>
|
||||
<TextInput
|
||||
name="username"
|
||||
placeholder="Username"
|
||||
placeholder={language.ui.username}
|
||||
icon={IconUser}
|
||||
formHandler={formHandler}
|
||||
label={language.ui.username}
|
||||
error={errors('username')}
|
||||
/>
|
||||
<TextInput
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
placeholder={language.ui.password}
|
||||
icon={IconKey}
|
||||
formHandler={formHandler}
|
||||
label={language.ui.password}
|
||||
error={errors('password')}
|
||||
/>
|
||||
<div class="card-actions justify-end mt-5">
|
||||
<div class="card-actions justify-end mt-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{language.ui.login}
|
||||
</button>
|
||||
|
@ -1,72 +1,66 @@
|
||||
import { putUpdateProfile } from '@api/user'
|
||||
import TextInput from '@components/common/TextInput'
|
||||
import { useSiteContext } from '@context/SiteContext'
|
||||
import { createForm } from '@felte/solid'
|
||||
import { validator } from '@felte/validator-yup'
|
||||
import useLanguage from '@hooks/useLanguage'
|
||||
import useToast from '@hooks/useToast'
|
||||
import { IconKey, IconUser, IconUserCircle } from '@tabler/icons-solidjs'
|
||||
import { Helpers } from '@utils/helper'
|
||||
import { useFormHandler } from 'solid-form-handler'
|
||||
import { yupSchema } from 'solid-form-handler/yup'
|
||||
import { createEffect } from 'solid-js'
|
||||
import * as yup from 'yup'
|
||||
|
||||
const profileSchema = yup.object({
|
||||
name: yup.string().required('Name is required'),
|
||||
password: yup.string().nullable().optional(),
|
||||
'confirm-password': yup.string().when('password', {
|
||||
is: (val) => !!(val && val.length > 0),
|
||||
then: (schema) =>
|
||||
schema.oneOf([yup.ref('password'), null], 'Passwords must match'),
|
||||
}),
|
||||
})
|
||||
|
||||
const language = useLanguage()
|
||||
const notify = useToast()
|
||||
const profileSchema = (language, isRequired) =>
|
||||
yup.object({
|
||||
name: yup.string().required(isRequired(language.ui.displayName)),
|
||||
password: yup.string().nullable().optional(),
|
||||
'confirm-password': yup.string().when('password', {
|
||||
is: (val) => !!(val && val.length > 0),
|
||||
then: (schema) =>
|
||||
schema.oneOf(
|
||||
[yup.ref('password'), null],
|
||||
language.message['PASSWORD_MUSTMATCH'],
|
||||
),
|
||||
}),
|
||||
})
|
||||
|
||||
export default function Profile() {
|
||||
const {
|
||||
store: { userInfo },
|
||||
setUser,
|
||||
} = useSiteContext()
|
||||
const formHandler = useFormHandler(yupSchema(profileSchema))
|
||||
const { formData } = formHandler
|
||||
const { language, isRequired } = useLanguage()
|
||||
const notify = useToast()
|
||||
|
||||
createEffect(() => {
|
||||
formHandler.fillForm({
|
||||
name: userInfo?.name,
|
||||
})
|
||||
})
|
||||
const { form, errors, reset } = createForm({
|
||||
extend: [validator({ schema: profileSchema(language, isRequired) })],
|
||||
onSubmit: async (values) => {
|
||||
try {
|
||||
const { name, password } = values
|
||||
const clearObj = Helpers.clearObject({
|
||||
name: name || null,
|
||||
password: password || null,
|
||||
})
|
||||
const resp = await putUpdateProfile(clearObj)
|
||||
|
||||
const submit = async (event) => {
|
||||
event.preventDefault()
|
||||
await formHandler.validateForm()
|
||||
try {
|
||||
const { name, password } = formData()
|
||||
const clearObj = Helpers.clearObject({
|
||||
name: name || null,
|
||||
password: password || null,
|
||||
})
|
||||
const resp = await putUpdateProfile(clearObj)
|
||||
|
||||
if (resp.status === 200) {
|
||||
setUser(resp.data)
|
||||
formHandler.setFieldValue('password', '')
|
||||
formHandler.setFieldValue('confirm-password', '')
|
||||
notify.success({
|
||||
title: 'Update profile success!',
|
||||
description: 'Your profile has been updated!',
|
||||
if (resp.status === 200) {
|
||||
setUser(resp.data)
|
||||
reset()
|
||||
notify.success({
|
||||
title: 'Update profile success!',
|
||||
description: 'Your profile has been updated!',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
notify.error({
|
||||
title: 'Update profile fail!',
|
||||
description: error?.data
|
||||
? language.message[error.data]
|
||||
: 'Your username or password input is wrong!',
|
||||
closable: true,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
notify.error({
|
||||
title: 'Update profile fail!',
|
||||
description: error?.data
|
||||
? language.message[error.data]
|
||||
: 'Your username or password input is wrong!',
|
||||
closable: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="profile">
|
||||
@ -78,37 +72,32 @@ export default function Profile() {
|
||||
</div>
|
||||
<div class="card w-full bg-base-100 shadow-lg border border-gray-200">
|
||||
<div class="card-body">
|
||||
<form autoComplete="off" onSubmit={submit}>
|
||||
<form autoComplete="off" use:form>
|
||||
<p class="card-title">{language.ui.changeInfo}</p>
|
||||
<div class="form-content py-5">
|
||||
<TextInput
|
||||
icon={IconUser}
|
||||
name="name"
|
||||
label={language.ui.displayName}
|
||||
line
|
||||
labelClass="md:w-56"
|
||||
value={userInfo?.name}
|
||||
placeholder={language.ui.displayName}
|
||||
formHandler={formHandler}
|
||||
error={errors('name')}
|
||||
/>
|
||||
<TextInput
|
||||
icon={IconKey}
|
||||
name="password"
|
||||
type="password"
|
||||
label={language.ui.newPassword}
|
||||
line
|
||||
labelClass="md:w-56"
|
||||
placeholder={language.ui.newPassword}
|
||||
formHandler={formHandler}
|
||||
error={errors('password')}
|
||||
/>
|
||||
<TextInput
|
||||
icon={IconKey}
|
||||
name="confirm-password"
|
||||
type="password"
|
||||
label={language.ui.confirmNewPassword}
|
||||
line
|
||||
labelClass="md:w-56"
|
||||
placeholder={language.ui.confirmNewPassword}
|
||||
formHandler={formHandler}
|
||||
error={errors('confirm-password')}
|
||||
/>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
|
@ -56,4 +56,18 @@ export class Helpers {
|
||||
}
|
||||
return object
|
||||
}
|
||||
|
||||
static clearArrayWithNullObject = (array) => {
|
||||
console.log(array)
|
||||
array.forEach((element, i) => {
|
||||
const obk = this.clearObject(element)
|
||||
if (obk) array.splice(i, 1)
|
||||
})
|
||||
// for (let i = 0; i < array.length; i++) {
|
||||
// if (array[i] === null || array[i] === undefined) {
|
||||
// array.splice(i, 1)
|
||||
// }
|
||||
// }
|
||||
return array.length > 0 ? array : null
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user