[haiz] haiz

This commit is contained in:
Sam Liu 2024-06-27 04:35:56 +00:00
parent e6923277ed
commit 1c205c69ac
51 changed files with 958 additions and 279 deletions

View File

@ -53,6 +53,7 @@ def run_migrations_offline() -> None:
target_metadata=target_metadata, target_metadata=target_metadata,
literal_binds=True, literal_binds=True,
dialect_opts={"paramstyle": "named"}, dialect_opts={"paramstyle": "named"},
render_as_batch=True
) )
with context.begin_transaction(): with context.begin_transaction():

View File

@ -7,6 +7,7 @@ Create Date: ${create_date}
""" """
from typing import Sequence, Union from typing import Sequence, Union
import backend.db.migration_types
from alembic import op from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
${imports if imports else ""} ${imports if imports else ""}

View File

@ -1,18 +1,19 @@
"""create user table """create users table
Revision ID: 68d05d045e6e Revision ID: 4b90a6ac504b
Revises: Revises:
Create Date: 2024-05-24 04:12:25.599139 Create Date: 2024-06-25 09:14:34.465698
""" """
from typing import Sequence, Union from typing import Sequence, Union
import backend.db.migration_types
from alembic import op from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = '68d05d045e6e' revision: str = '4b90a6ac504b'
down_revision: Union[str, None] = None down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None
depends_on: 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: def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.create_table('users', 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('username', sa.String(), nullable=False),
sa.Column('password', sa.String(), nullable=False), sa.Column('password', sa.String(), nullable=False),
sa.Column('name', sa.String(), nullable=True), sa.Column('name', sa.String(), nullable=True),

View 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 ###

View File

@ -15,7 +15,7 @@ settings = get_app_settings()
logger = get_logger() logger = get_logger()
description = f""" 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 @asynccontextmanager

View File

@ -0,0 +1 @@
from backend.db.models.guid import GUID # noqa: F401

View File

@ -1 +1,2 @@
from .users import * from .users import *
from .houses import *

56
backend/db/models/guid.py Normal file
View 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)

View File

@ -0,0 +1 @@
from .houses import *

View 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}"

View File

@ -1,15 +1,14 @@
from uuid import uuid4 from sqlalchemy import Boolean, String
from sqlalchemy import Boolean, ForeignKey, String from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID from backend.db.models.guid import GUID
from .._model_base import SqlAlchemyBase from .._model_base import SqlAlchemyBase
class User(SqlAlchemyBase): class Users(SqlAlchemyBase):
__tablename__ = 'users' __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) username: Mapped[str | None] = mapped_column(String, unique=True, index=True, nullable=False)
password: Mapped[str | None] = mapped_column(String, 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) name: Mapped[str | None] = mapped_column(String, index=True, nullable=True)

View File

@ -1 +1,2 @@
from .repository_users import * from .repository_users import *
from .repository_houses import *

View 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

View File

@ -1,6 +1,6 @@
from backend.core.config import get_app_settings from backend.core.config import get_app_settings
from backend.core.security.security import hash_password 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 backend.schemas import UserCreate, UserProfile
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from uuid import UUID from uuid import UUID
@ -11,7 +11,7 @@ settings = get_app_settings()
class RepositoryUsers: class RepositoryUsers:
def __init__(self): def __init__(self):
self.user = User() self.user = Users()
def get_all(self, skip: int = 0, limit: int = 100): def get_all(self, skip: int = 0, limit: int = 100):
return self.user.query.offset(skip).limit(limit).all() return self.user.query.offset(skip).limit(limit).all()
@ -25,7 +25,7 @@ class RepositoryUsers:
def create(self, db: Session, user: UserCreate | UserSeeds): def create(self, db: Session, user: UserCreate | UserSeeds):
try: try:
password = getattr(user, "password") 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.add(db_user)
db.commit() db.commit()
except Exception: except Exception:
@ -41,7 +41,7 @@ class RepositoryUsers:
return None return None
try: try:
user.update_password() 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() db.commit()
except Exception: except Exception:
db.rollback() db.rollback()

View File

@ -1,9 +1,10 @@
from fastapi import APIRouter from fastapi import APIRouter
from . import (auth, user) from . import (auth, user, house)
router = APIRouter(prefix='/api') router = APIRouter(prefix='/api')
router.include_router(auth.router) router.include_router(auth.router)
router.include_router(user.router) router.include_router(user.router)
router.include_router(house.router)

View File

@ -0,0 +1,7 @@
from fastapi import APIRouter
from . import house
router = APIRouter(prefix='/house')
router.include_router(house.public_router)

View 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")

View File

@ -1,2 +1,3 @@
from .common import * from .common import *
from .user import * from .user import *
from .house import *

View File

@ -0,0 +1 @@
from .house import *

View 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]

View File

@ -0,0 +1 @@
from .house_service import *

View 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)

View File

@ -31,7 +31,6 @@ class UserService(BaseService):
return self.repos.create(db=db, user=user) return self.repos.create(db=db, user=user)
def check_exist(self, user: UserRequest): def check_exist(self, user: UserRequest):
print(f"user: {user}")
db_user = self.get_by_username(username=user.username) db_user = self.get_by_username(username=user.username)
if not db_user: if not db_user:
return False return False

View File

@ -15,6 +15,7 @@
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<div id="modal-portal"></div>
<script type="module" src="/src/index.jsx"></script> <script type="module" src="/src/index.jsx"></script>
</body> </body>
</html> </html>

View File

@ -15,13 +15,15 @@
"prettier": "prettier \"src/**/*.{js,jsx}\" --write" "prettier": "prettier \"src/**/*.{js,jsx}\" --write"
}, },
"dependencies": { "dependencies": {
"@felte/reporter-solid": "^1.2.10",
"@felte/solid": "^1.2.13",
"@felte/validator-yup": "^1.1.3",
"@solidjs/meta": "^0.29.4", "@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.13.3", "@solidjs/router": "^0.13.3",
"@stitches/core": "^1.2.8", "@stitches/core": "^1.2.8",
"@tabler/icons-solidjs": "^3.3.0", "@tabler/icons-solidjs": "^3.3.0",
"axios": "^1.6.8", "axios": "^1.6.8",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"solid-form-handler": "^1.2.3",
"solid-js": "^1.8.15", "solid-js": "^1.8.15",
"solid-toast": "^0.5.0", "solid-toast": "^0.5.0",
"yup": "^1.4.0" "yup": "^1.4.0"

View File

@ -8,6 +8,15 @@ importers:
.: .:
dependencies: 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': '@solidjs/meta':
specifier: ^0.29.4 specifier: ^0.29.4
version: 0.29.4(solid-js@1.8.17) version: 0.29.4(solid-js@1.8.17)
@ -26,9 +35,6 @@ importers:
crypto-js: crypto-js:
specifier: ^4.2.0 specifier: ^4.2.0
version: 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: solid-js:
specifier: ^1.8.15 specifier: ^1.8.15
version: 1.8.17 version: 1.8.17
@ -343,6 +349,32 @@ packages:
resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 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': '@humanwhocodes/config-array@0.11.14':
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
engines: {node: '>=10.10.0'} engines: {node: '>=10.10.0'}
@ -1497,11 +1529,6 @@ packages:
resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==}
engines: {node: '>=18'} 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: solid-js@1.8.17:
resolution: {integrity: sha512-E0FkUgv9sG/gEBWkHr/2XkBluHb1fkrHywUgA6o6XolPDCJ4g1HaLmQufcBBhiF36ee40q+HpG/vCZu7fLpI3Q==} resolution: {integrity: sha512-E0FkUgv9sG/gEBWkHr/2XkBluHb1fkrHywUgA6o6XolPDCJ4g1HaLmQufcBBhiF36ee40q+HpG/vCZu7fLpI3Q==}
@ -1972,6 +1999,27 @@ snapshots:
'@eslint/js@8.57.0': {} '@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': '@humanwhocodes/config-array@0.11.14':
dependencies: dependencies:
'@humanwhocodes/object-schema': 2.0.3 '@humanwhocodes/object-schema': 2.0.3
@ -3102,10 +3150,6 @@ snapshots:
ansi-styles: 6.2.1 ansi-styles: 6.2.1
is-fullwidth-code-point: 5.0.0 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: solid-js@1.8.17:
dependencies: dependencies:
csstype: 3.1.3 csstype: 3.1.3

View File

@ -35,3 +35,34 @@ a {
a:hover { a:hover {
color: #535bf2; 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;
}
}

View 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>
)
}

View 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>
)
}

View File

@ -0,0 +1,2 @@
export * from './AreaAdd'
export { default } from './AreaAdd'

View File

@ -31,8 +31,7 @@ export default function Header() {
try { try {
await clickLogOut() await clickLogOut()
} catch (error) { } catch (error) {
console.log({ notify.error({
status: 'danger',
title: 'Logout fail!', title: 'Logout fail!',
closable: false, closable: false,
}) })

View File

@ -12,7 +12,7 @@ import { For, Show, mergeProps } from 'solid-js'
import { Dynamic } from 'solid-js/web' import { Dynamic } from 'solid-js/web'
import './navbar.scss' import './navbar.scss'
const language = useLanguage() const { language } = useLanguage()
export const NAV_ITEM = (admin = false) => [ export const NAV_ITEM = (admin = false) => [
{ {

View 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>
)
}

View File

@ -0,0 +1,2 @@
export * from './ConfirmPopup'
export { default } from './ConfirmPopup'

View 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>
)
}

View File

@ -0,0 +1,2 @@
export * from './Popup'
export { default } from './Popup'

View File

@ -1,48 +1,41 @@
import { Field } from 'solid-form-handler' import { IconInfoCircle } from '@tabler/icons-solidjs'
import { Show, splitProps } from 'solid-js' import { Show, splitProps } from 'solid-js'
import { Dynamic } from 'solid-js/web' import { Dynamic } from 'solid-js/web'
export default function Finput(props) { export default function Textinput(props) {
const [local, rest] = splitProps(props, ['label', 'icon']) const [local, rest] = splitProps(props, ['label', 'icon'])
return ( return (
<Field <div class="form-control w-full [&:not(:last-child)]:pb-3">
{...props} <div class="join join-vertical">
mode="input" <div
render={(field) => ( class="flex items-center justify-between bg-base-200 border border-gray-300 h-12 px-3 join-item"
<div class="form-control w-full [&:not(:last-child)]:pb-3"> classList={{ 'border-red-500': props.error }}
<Show when={local.label && !props.line}> >
<div class="label"> <label class="label justify-start gap-2 bg-base-200">
<span class="label-text">{local.label}</span> <Show when={local.icon && local.label}>
</div> <Dynamic component={local.icon} size={18} />
</Show> <span class="label-text font-bold whitespace-nowrap">
<div class="join"> {local.label}
<Show when={local.icon}> </span>
</Show>
<Show when={props.error}>
<div <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} /> <IconInfoCircle size={18} />
<Show when={local.label && !!props.line}>
<span class="ml-2 hidden md:block">{local.label}</span>
</Show>
</div> </div>
</Show> </Show>
<label </label>
class="input input-bordered flex items-center gap-2 w-full join-item" {props.children}
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>
</div> </div>
)} <input
/> {...rest}
class="input input-bordered w-full join-item"
classList={{ 'input-error': props.error }}
/>
</div>
</div>
) )
} }

View 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>
)
}

View File

@ -0,0 +1,2 @@
export * from './Textarea'
export { default } from './Textarea'

View File

@ -36,11 +36,7 @@ export function SiteContextProvider(props) {
} }
const setUser = (user) => { const setUser = (user) => {
setStore( setStore('userInfo', user)
produce((s) => {
s.userInfo = user
}),
)
setLocalStore() setLocalStore()
} }

View File

@ -6,7 +6,7 @@ import { Helpers } from '@utils/helper'
export default function useAuth(setAuth) { export default function useAuth(setAuth) {
const navigate = useNavigate() const navigate = useNavigate()
const clickLogIn = async (username, password, cbFormReset) => { const clickLogIn = async (username, password) => {
const resp = await postLogin({ username, password }) const resp = await postLogin({ username, password })
if (resp.status === 200) { if (resp.status === 200) {
@ -17,7 +17,6 @@ export default function useAuth(setAuth) {
localStorage.setItem(LOGIN_KEY, Helpers.encrypt(JSON.stringify(rest))) localStorage.setItem(LOGIN_KEY, Helpers.encrypt(JSON.stringify(rest)))
} }
cbFormReset()
navigate('/', { replace: true }) navigate('/', { replace: true })
} }
} }

View File

@ -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') { export default function useLanguage(selectLanguage = 'vi') {
const data = import.meta.glob('@lang/*.json', { const data = import.meta.glob('@lang/*.json', {
import: 'default', import: 'default',
@ -11,5 +21,14 @@ export default function useLanguage(selectLanguage = 'vi') {
imp[keypath] = data[path] 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 }
} }

View File

@ -1,6 +1,19 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @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'; @import './assets/css/fu-theme.scss';
:root { :root {

View File

@ -11,7 +11,7 @@
"clear": "Xóa", "clear": "Xóa",
"house": "Nhà", "house": "Nhà",
"action": "Thao Tác", "action": "Thao Tác",
"createHouse": "Tạo mới nhà", "createNew": "Tạo mới",
"location": "Nhà kho", "location": "Nhà kho",
"displayName": "Tên hiển thị", "displayName": "Tên hiển thị",
"newPassword": "Mật khẩu mới", "newPassword": "Mật khẩu mới",
@ -20,7 +20,17 @@
"houseName": "Tên nhà", "houseName": "Tên nhà",
"houseIcon": "Ký tự nhà", "houseIcon": "Ký tự nhà",
"houseAddress": "Địa chỉ 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": { "table": {
"columnName": { "columnName": {
@ -34,6 +44,8 @@
"message": { "message": {
"CREATED_USER": "Username already registered!", "CREATED_USER": "Username already registered!",
"LOGIN_WRONG": "Your username or password input is wrong!", "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"
} }
} }

View File

@ -1,7 +1,7 @@
import { NAV_ITEM } from '@components/Navbar' import { NAV_ITEM } from '@components/Navbar'
import { useSiteContext } from '@context/SiteContext' import { useSiteContext } from '@context/SiteContext'
import { useNavigate } from '@solidjs/router' import { useNavigate } from '@solidjs/router'
import { onMount } from 'solid-js' import { createEffect } from 'solid-js'
function getFirstItem(array) { function getFirstItem(array) {
const first = array.filter((item) => item.show)[0] const first = array.filter((item) => item.show)[0]
@ -15,9 +15,11 @@ export default function Home() {
const { store } = useSiteContext() const { store } = useSiteContext()
const navigate = useNavigate() const navigate = useNavigate()
onMount(() => { createEffect(() => {
const first = getFirstItem(NAV_ITEM(store?.userInfo?.isAdmin)) if (store?.userInfo?.isAdmin) {
navigate(first ? first.path : '/me', { replace: true }) const first = getFirstItem(NAV_ITEM(store?.userInfo?.isAdmin))
navigate(first ? first.path : '/me', { replace: true })
}
}) })
return <></> return <></>

View File

@ -8,14 +8,23 @@ import {
IconHome2, IconHome2,
IconHomeDot, IconHomeDot,
IconPencil, IconPencil,
IconSquareRoundedPlus,
IconTrash, IconTrash,
} from '@tabler/icons-solidjs' } from '@tabler/icons-solidjs'
import './house.scss' import './house.scss'
export default function House() { export default function House() {
const language = useLanguage() const { language } = useLanguage()
const [view, setView] = createSignal(VIEWDATA['list']) const [view, setView] = createSignal(VIEWDATA['list'])
const onEdit = () => {
console.log('edit')
}
const onDelete = () => {
console.log('delete')
}
return ( return (
<div class="house"> <div class="house">
<div class="flex items-center gap-2 mb-5 text-xl"> <div class="flex items-center gap-2 mb-5 text-xl">
@ -30,53 +39,68 @@ export default function House() {
href="/house/create" href="/house/create"
class="btn btn-success text-white hover:text-white btn-sm" class="btn btn-success text-white hover:text-white btn-sm"
> >
{language.ui.createHouse} <IconSquareRoundedPlus size={15} />
{language.ui.createNew}
</A> </A>
</div> </div>
<div <div
class="view-layout" class="view-layout scroll-shadow-horizontal no-scrollbar"
classList={{ classList={{
'view-list': view() === VIEWDATA['list'], 'view-list': view() === VIEWDATA['list'],
'view-grid': view() === VIEWDATA['grid'], 'view-grid': view() === VIEWDATA['grid'],
}} }}
> >
<div class="row view-head"> <div class="view-table">
<div class="col w-1/12">{language.table.columnName.no}</div> <div class="row view-head">
<div class="col w-1/12">{language.table.columnName.icon}</div> <div class="col w-1/12">{language.table.columnName.no}</div>
<div class="col w-4/12">{language.table.columnName.name}</div> <div class="col w-1/12">{language.table.columnName.icon}</div>
<div class="col w-4/12">{language.table.columnName.address}</div> <div class="col w-4/12">{language.table.columnName.name}</div>
<div class="col w-2/12">{language.table.columnName.action}</div> <div class="col w-4/12">{language.table.columnName.address}</div>
</div> <div class="col w-2/12">{language.table.columnName.action}</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> </div>
<div class="col w-4/12">Nhà 1</div> <div class="row view-item">
<div class="col w-4/12">Data 4</div> <div class="col hide w-1/12">1</div>
<div class="col actionbar w-2/12"> <div class="col w-1/12">
<button class="btn btn-ghost btn-sm px-1 text-blue-500 mr-2"> <IconHome2 size={21} />
<IconPencil size={20} /> </div>
</button> <div class="col w-4/12">Nhà 1</div>
<button class="btn btn-ghost btn-sm px-1 text-red-500"> <div class="col w-4/12">Data 4</div>
<IconTrash size={20} /> <div class="col actionbar w-2/12">
</button> <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> <div class="row view-item">
<div class="row view-item"> <div class="col hide w-1/12">2</div>
<div class="col hide w-1/12">2</div> <div class="col w-1/12">
<div class="col w-1/12"> <IconHomeDot size={21} />
<IconHomeDot size={21} /> </div>
</div> <div class="col w-4/12">Nhà 2</div>
<div class="col w-4/12">Nhà 2</div> <div class="col w-4/12">Data 4</div>
<div class="col w-4/12">Data 4</div> <div class="col actionbar w-2/12">
<div class="col actionbar w-2/12"> <button
<button class="btn btn-ghost btn-sm px-1 text-blue-500 mr-2"> class="btn btn-ghost btn-sm px-1 text-blue-500 mr-2"
<IconPencil size={20} /> onClick={onEdit}
</button> >
<button class="btn btn-ghost btn-sm px-1 text-red-500"> <IconPencil size={20} />
<IconTrash size={20} /> </button>
</button> <button
class="btn btn-ghost btn-sm px-1 text-red-500"
onClick={onDelete}
>
<IconTrash size={20} />
</button>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,7 +1,12 @@
.view-layout { .view-layout {
&.view-list { &.view-list {
@apply flex flex-col rounded-2xl border shadow-md overflow-hidden; @apply rounded-2xl border shadow-md overflow-auto;
& > .row {
& .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; @apply flex justify-between items-center flex-nowrap px-3 border-b border-gray-200 h-14;
&.view-head { &.view-head {
@ -11,13 +16,17 @@
} }
&.view-grid { &.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 { &.view-item {
@apply card bg-base-100 shadow-md border border-gray-200 p-3 relative pb-8; @apply card bg-base-100 shadow-md border border-gray-200 p-3 relative pb-8;
& > .col { & .col {
@apply w-auto @apply w-auto
} }
@ -27,11 +36,8 @@
} }
& > .actionbar { & > .actionbar {
@apply absolute right-0 bottom-0 flex px-3 py-1 w-auto invisible; @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);
&:hover > .actionbar {
@apply visible;
} }
} }

View File

@ -1,35 +1,33 @@
import AreaAdd from '@components/AreaAdd'
import TextInput from '@components/common/TextInput' import TextInput from '@components/common/TextInput'
import { createForm } from '@felte/solid'
import { validator } from '@felte/validator-yup'
import useLanguage from '@hooks/useLanguage' import useLanguage from '@hooks/useLanguage'
import { A } from '@solidjs/router' import { A } from '@solidjs/router'
import { import {
IconAddressBook, IconAddressBook,
IconHome, IconHomePlus,
IconIcons, IconIcons,
IconTag, IconTag,
} from '@tabler/icons-solidjs' } from '@tabler/icons-solidjs'
import { useFormHandler } from 'solid-form-handler'
import { yupSchema } from 'solid-form-handler/yup'
import * as yup from 'yup' import * as yup from 'yup'
const houseSchema = yup.object({ const houseSchema = (language, isRequired) =>
name: yup.string().required('Name is required'), yup.object({
password: yup.string().nullable().optional(), icon: yup.string().required(isRequired(language.ui.houseIcon)),
'confirm-password': yup.string().when('password', { name: yup.string().required(isRequired(language.ui.houseName)),
is: (val) => !!(val && val.length > 0), address: yup.string().required(isRequired(language.ui.houseAddress)),
then: (schema) => areas: yup.array().required(isRequired(language.ui.areas)),
schema.oneOf([yup.ref('password'), null], 'Passwords must match'), })
}),
})
export default function HouseCreate() { export default function HouseCreate() {
const language = useLanguage() const { language, isRequired } = useLanguage()
const formHandler = useFormHandler(yupSchema(houseSchema)) const { form, errors } = createForm({
const { formData } = formHandler extend: [validator({ schema: houseSchema(language, isRequired) })],
onSubmit: async (values) => {
const submit = async (event) => { console.log(values)
event.preventDefault() },
await formHandler.validateForm() })
}
return ( return (
<div class="house-create"> <div class="house-create">
@ -43,40 +41,58 @@ export default function HouseCreate() {
</div> </div>
<div class="flex items-center gap-2 mb-5 text-xl"> <div class="flex items-center gap-2 mb-5 text-xl">
<span class="text-secondary"> <span class="text-secondary">
<IconHome size={30} /> <IconHomePlus size={30} />
</span> </span>
{language.ui.newHouse} {language.ui.newHouse}
</div> </div>
<div class="card w-full bg-base-100 shadow-lg border border-gray-200"> <div class="card w-full bg-base-100 shadow-lg border border-gray-200">
<div class="card-body"> <div class="card-body">
<form autoComplete="off" onSubmit={submit}> <form autoComplete="off" use:form>
<TextInput <div class="grid gap-4 grid-cols-1 lg:grid-cols-2">
icon={IconIcons} <div class="col-auto">
name="icon" <TextInput
label={language.ui.houseIcon} icon={IconIcons}
line name="icon"
labelClass="md:w-40" label={language.ui.houseIcon}
placeholder={language.ui.houseIcon} labelClass="md:w-40"
formHandler={formHandler} placeholder={language.ui.houseIcon}
/> error={errors('icon')}
<TextInput >
icon={IconTag} <div class="label">
name="name" <a
label={language.ui.houseName} class="label-text link link-info"
line href="https://tabler.io/icons"
labelClass="md:w-40" target="_blank"
placeholder={language.ui.houseName} >
formHandler={formHandler} {language.ui.findIconHere}
/> </a>
<TextInput </div>
icon={IconAddressBook} </TextInput>
name="address" </div>
label={language.ui.houseAddress} <div class="col-auto">
line <TextInput
labelClass="md:w-40" icon={IconTag}
placeholder={language.ui.houseAddress} name="name"
formHandler={formHandler} 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"> <div class="card-actions">
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
{language.ui.create} {language.ui.create}

View File

@ -1,9 +1,9 @@
import { useSiteContext } from '@context/SiteContext' import { useSiteContext } from '@context/SiteContext'
import { createForm } from '@felte/solid'
import { validator } from '@felte/validator-yup'
import useLanguage from '@hooks/useLanguage' import useLanguage from '@hooks/useLanguage'
import { useNavigate } from '@solidjs/router' import { useNavigate } from '@solidjs/router'
import { IconKey, IconUser } from '@tabler/icons-solidjs' 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 { onMount } from 'solid-js'
import * as yup from 'yup' import * as yup from 'yup'
import './login.scss' import './login.scss'
@ -13,20 +13,36 @@ import TextInput from '@components/common/TextInput'
import useAuth from '@hooks/useAuth' import useAuth from '@hooks/useAuth'
import useToast from '@hooks/useToast' import useToast from '@hooks/useToast'
const loginSchema = yup.object({ const loginSchema = (language, isRequired) =>
username: yup.string().required('Username is required'), yup.object({
password: yup.string().required('Password is required'), username: yup.string().required(isRequired(language.ui.username)),
}) password: yup.string().required(isRequired(language.ui.password)),
})
const language = useLanguage()
export default function Login() { export default function Login() {
const { store, setAuth } = useSiteContext() const { store, setAuth } = useSiteContext()
const { language, isRequired } = useLanguage()
const navigate = useNavigate() const navigate = useNavigate()
const { clickLogIn } = useAuth(setAuth) const { clickLogIn } = useAuth(setAuth)
const notify = useToast() 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(() => { onMount(() => {
if (store.auth) { 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 ( return (
<div class="login-page"> <div class="login-page">
<div class="card glass card-compact login-wrap shadow-xl"> <div class="card glass card-compact login-wrap shadow-xl">
@ -67,21 +61,23 @@ export default function Login() {
</div> </div>
<div class="card-body"> <div class="card-body">
<h1 class="card-title">{language.ui.login}</h1> <h1 class="card-title">{language.ui.login}</h1>
<form autoComplete="off" onSubmit={submit}> <form autoComplete="off" use:form>
<TextInput <TextInput
name="username" name="username"
placeholder="Username" placeholder={language.ui.username}
icon={IconUser} icon={IconUser}
formHandler={formHandler} label={language.ui.username}
error={errors('username')}
/> />
<TextInput <TextInput
name="password" name="password"
type="password" type="password"
placeholder="Password" placeholder={language.ui.password}
icon={IconKey} 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"> <button type="submit" class="btn btn-primary">
{language.ui.login} {language.ui.login}
</button> </button>

View File

@ -1,72 +1,66 @@
import { putUpdateProfile } from '@api/user' import { putUpdateProfile } from '@api/user'
import TextInput from '@components/common/TextInput' import TextInput from '@components/common/TextInput'
import { useSiteContext } from '@context/SiteContext' import { useSiteContext } from '@context/SiteContext'
import { createForm } from '@felte/solid'
import { validator } from '@felte/validator-yup'
import useLanguage from '@hooks/useLanguage' import useLanguage from '@hooks/useLanguage'
import useToast from '@hooks/useToast' import useToast from '@hooks/useToast'
import { IconKey, IconUser, IconUserCircle } from '@tabler/icons-solidjs' import { IconKey, IconUser, IconUserCircle } from '@tabler/icons-solidjs'
import { Helpers } from '@utils/helper' 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' import * as yup from 'yup'
const profileSchema = yup.object({ const profileSchema = (language, isRequired) =>
name: yup.string().required('Name is required'), yup.object({
password: yup.string().nullable().optional(), name: yup.string().required(isRequired(language.ui.displayName)),
'confirm-password': yup.string().when('password', { password: yup.string().nullable().optional(),
is: (val) => !!(val && val.length > 0), 'confirm-password': yup.string().when('password', {
then: (schema) => is: (val) => !!(val && val.length > 0),
schema.oneOf([yup.ref('password'), null], 'Passwords must match'), then: (schema) =>
}), schema.oneOf(
}) [yup.ref('password'), null],
language.message['PASSWORD_MUSTMATCH'],
const language = useLanguage() ),
const notify = useToast() }),
})
export default function Profile() { export default function Profile() {
const { const {
store: { userInfo }, store: { userInfo },
setUser, setUser,
} = useSiteContext() } = useSiteContext()
const formHandler = useFormHandler(yupSchema(profileSchema)) const { language, isRequired } = useLanguage()
const { formData } = formHandler const notify = useToast()
createEffect(() => { const { form, errors, reset } = createForm({
formHandler.fillForm({ extend: [validator({ schema: profileSchema(language, isRequired) })],
name: userInfo?.name, 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) => { if (resp.status === 200) {
event.preventDefault() setUser(resp.data)
await formHandler.validateForm() reset()
try { notify.success({
const { name, password } = formData() title: 'Update profile success!',
const clearObj = Helpers.clearObject({ description: 'Your profile has been updated!',
name: name || null, })
password: password || null, }
}) } catch (error) {
const resp = await putUpdateProfile(clearObj) notify.error({
title: 'Update profile fail!',
if (resp.status === 200) { description: error?.data
setUser(resp.data) ? language.message[error.data]
formHandler.setFieldValue('password', '') : 'Your username or password input is wrong!',
formHandler.setFieldValue('confirm-password', '') closable: true,
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,
})
}
}
return ( return (
<div class="profile"> <div class="profile">
@ -78,37 +72,32 @@ export default function Profile() {
</div> </div>
<div class="card w-full bg-base-100 shadow-lg border border-gray-200"> <div class="card w-full bg-base-100 shadow-lg border border-gray-200">
<div class="card-body"> <div class="card-body">
<form autoComplete="off" onSubmit={submit}> <form autoComplete="off" use:form>
<p class="card-title">{language.ui.changeInfo}</p> <p class="card-title">{language.ui.changeInfo}</p>
<div class="form-content py-5"> <div class="form-content py-5">
<TextInput <TextInput
icon={IconUser} icon={IconUser}
name="name" name="name"
label={language.ui.displayName} label={language.ui.displayName}
line value={userInfo?.name}
labelClass="md:w-56"
placeholder={language.ui.displayName} placeholder={language.ui.displayName}
formHandler={formHandler} error={errors('name')}
/> />
<TextInput <TextInput
icon={IconKey} icon={IconKey}
name="password" name="password"
type="password" type="password"
label={language.ui.newPassword} label={language.ui.newPassword}
line
labelClass="md:w-56"
placeholder={language.ui.newPassword} placeholder={language.ui.newPassword}
formHandler={formHandler} error={errors('password')}
/> />
<TextInput <TextInput
icon={IconKey} icon={IconKey}
name="confirm-password" name="confirm-password"
type="password" type="password"
label={language.ui.confirmNewPassword} label={language.ui.confirmNewPassword}
line
labelClass="md:w-56"
placeholder={language.ui.confirmNewPassword} placeholder={language.ui.confirmNewPassword}
formHandler={formHandler} error={errors('confirm-password')}
/> />
</div> </div>
<div class="card-actions"> <div class="card-actions">

View File

@ -56,4 +56,18 @@ export class Helpers {
} }
return object 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
}
} }