[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,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
render_as_batch=True
)
with context.begin_transaction():

View File

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

View File

@ -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),

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

View File

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

View File

@ -1 +1,2 @@
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, 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)

View File

@ -1 +1,2 @@
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.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()

View File

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

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

View File

@ -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>

View File

@ -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"

View File

@ -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

View File

@ -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;
}
}

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 {
await clickLogOut()
} catch (error) {
console.log({
status: 'danger',
notify.error({
title: 'Logout fail!',
closable: false,
})

View File

@ -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) => [
{

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

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) => {
setStore(
produce((s) => {
s.userInfo = user
}),
)
setStore('userInfo', user)
setLocalStore()
}

View File

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

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') {
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 }
}

View File

@ -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 {

View File

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

View File

@ -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 <></>

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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}

View File

@ -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>

View File

@ -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">

View File

@ -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
}
}