mirror of
https://github.com/Nezumi-2711/react-training.git
synced 2026-09-22 13:38:51 +00:00
Add modal component and replace room dialog form
This commit is contained in:
@@ -58,7 +58,7 @@ const List = ({
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}): Nullable<ReactNode> => {
|
}): Nullable<ReactNode> => {
|
||||||
const { openId, close } = useContext(MenusContext);
|
const { openId, close } = useContext(MenusContext);
|
||||||
const ref = useOutsideClick(close!, false);
|
const ref = useOutsideClick<HTMLUListElement>(close!, false);
|
||||||
|
|
||||||
if (openId !== id) return null;
|
if (openId !== id) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import {
|
||||||
|
ReactElement,
|
||||||
|
ReactNode,
|
||||||
|
cloneElement,
|
||||||
|
useContext,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
// Hooks
|
||||||
|
import { useOutsideClick } from '../../hooks/useOutsideClick';
|
||||||
|
|
||||||
|
// Contexts
|
||||||
|
import { ModalContext } from '../../contexts/ModalContext';
|
||||||
|
|
||||||
|
// Styled
|
||||||
|
import { Overlay, StyledModal, StyledModalContent, TitleModal } from './styled';
|
||||||
|
|
||||||
|
interface IModal {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Modal = ({ children }: IModal) => {
|
||||||
|
const [openName, setOpenName] = useState('');
|
||||||
|
|
||||||
|
const close = () => setOpenName('');
|
||||||
|
const open = setOpenName;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContext.Provider value={{ openName, close, open }}>
|
||||||
|
{children}
|
||||||
|
</ModalContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IOpen {
|
||||||
|
children: ReactElement;
|
||||||
|
modalName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Open = ({ children, modalName }: IOpen) => {
|
||||||
|
const { open } = useContext(ModalContext);
|
||||||
|
|
||||||
|
return cloneElement(children, { onClick: () => open!(modalName) });
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IWindow {
|
||||||
|
children: ReactElement;
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Window = ({ children, name, title }: IWindow) => {
|
||||||
|
const { openName, close } = useContext(ModalContext);
|
||||||
|
const ref = useOutsideClick<HTMLDivElement>(close!);
|
||||||
|
|
||||||
|
console.log('name: ', name);
|
||||||
|
console.log('openName: ', openName);
|
||||||
|
|
||||||
|
if (name !== openName) return null;
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<Overlay>
|
||||||
|
<StyledModal ref={ref}>
|
||||||
|
<StyledModalContent>
|
||||||
|
<TitleModal>{title}</TitleModal>
|
||||||
|
{cloneElement(children, { onCloseModal: close })}
|
||||||
|
</StyledModalContent>
|
||||||
|
</StyledModal>
|
||||||
|
</Overlay>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Modal.Open = Open;
|
||||||
|
Modal.Window = Window;
|
||||||
|
|
||||||
|
export default Modal;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const StyledModal = styled.div`
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
background-color: var(--color-grey-0);
|
||||||
|
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
|
||||||
|
padding: 3.2rem 4rem;
|
||||||
|
|
||||||
|
transition: all 0.5s;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledModalContent = styled.div`
|
||||||
|
background-color: var(--form-color);
|
||||||
|
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
|
||||||
|
padding: 30px 30px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Overlay = styled.div`
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
background-color: var(--overlay-color);
|
||||||
|
|
||||||
|
z-index: 1000;
|
||||||
|
transition: all 0.5s;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TitleModal = styled.p`
|
||||||
|
font-size: var(--fs-md);
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
margin-bottom: 30px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export { StyledModal, Overlay, StyledModalContent, TitleModal };
|
||||||
@@ -2,6 +2,6 @@ export const DASHBOARD = '/dashboard';
|
|||||||
export const USER = '/user';
|
export const USER = '/user';
|
||||||
export const ROOM = '/room';
|
export const ROOM = '/room';
|
||||||
export const OTHER_PATH = '*';
|
export const OTHER_PATH = '*';
|
||||||
export const BASE_URL = 'https://hotel-management-api.loiphan.com/';
|
export const BASE_URL = 'http://localhost:3000/';
|
||||||
export const USER_PATH = 'users';
|
export const USER_PATH = 'users';
|
||||||
export const ROOM_PATH = 'rooms';
|
export const ROOM_PATH = 'rooms';
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
interface IModalContext {
|
||||||
|
openName?: string;
|
||||||
|
close?: () => void;
|
||||||
|
open?: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalContext = createContext<IModalContext>({});
|
||||||
|
|
||||||
|
export { ModalContext };
|
||||||
@@ -9,11 +9,11 @@ import { Nullable } from '../types/common';
|
|||||||
* @param listeningCapturing A boolean value indicating that events of this type will be dispatched to the registered listener before being dispatched to any EventTarget beneath it in the DOM tree. If not specified, defaults to false.
|
* @param listeningCapturing A boolean value indicating that events of this type will be dispatched to the registered listener before being dispatched to any EventTarget beneath it in the DOM tree. If not specified, defaults to false.
|
||||||
* @returns Return a mutable ref object
|
* @returns Return a mutable ref object
|
||||||
*/
|
*/
|
||||||
export const useOutsideClick = (
|
export const useOutsideClick = <T extends Node>(
|
||||||
handler: () => void,
|
handler: () => void,
|
||||||
listeningCapturing = true
|
listeningCapturing = true
|
||||||
): MutableRefObject<Nullable<HTMLUListElement>> => {
|
): MutableRefObject<Nullable<T>> => {
|
||||||
const ref = useRef<HTMLUListElement>(null);
|
const ref = useRef<T>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClick = (e: Event) => {
|
const handleClick = (e: Event) => {
|
||||||
|
|||||||
@@ -48,15 +48,15 @@ const FormBtn = styled(Button)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
interface IRoomFormProp {
|
interface IRoomFormProp {
|
||||||
onClose: () => void;
|
onCloseModal?: () => void;
|
||||||
reload: boolean;
|
reload?: boolean;
|
||||||
setReload: Dispatch<SetStateAction<boolean>>;
|
setReload?: Dispatch<SetStateAction<boolean>>;
|
||||||
room?: Nullable<IRoom>;
|
room?: Nullable<IRoom>;
|
||||||
isAdd: boolean;
|
isAdd?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RoomForm = ({
|
const RoomForm = ({
|
||||||
onClose,
|
onCloseModal,
|
||||||
reload,
|
reload,
|
||||||
setReload,
|
setReload,
|
||||||
room,
|
room,
|
||||||
@@ -101,7 +101,7 @@ const RoomForm = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Reload table data
|
// Reload table data
|
||||||
setReload(!reload);
|
// setReload(!reload);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
@@ -109,7 +109,7 @@ const RoomForm = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
onClose();
|
// onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -168,7 +168,7 @@ const RoomForm = ({
|
|||||||
: 'Save'
|
: 'Save'
|
||||||
}
|
}
|
||||||
</FormBtn>
|
</FormBtn>
|
||||||
<FormBtn type="button" styled="secondary" onClick={onClose}>
|
<FormBtn type="button" styled="secondary" onClick={onCloseModal}>
|
||||||
Close
|
Close
|
||||||
</FormBtn>
|
</FormBtn>
|
||||||
</Form.Action>
|
</Form.Action>
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import SortBy from '../../components/SortBy';
|
|||||||
import OrderBy from '../../components/OrderBy';
|
import OrderBy from '../../components/OrderBy';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { Nullable } from '../../types/common';
|
|
||||||
import { IRoom } from '../../types/rooms';
|
import { IRoom } from '../../types/rooms';
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
@@ -31,27 +30,16 @@ import Spinner from '../../commons/styles/Spinner';
|
|||||||
// Helpers
|
// Helpers
|
||||||
import { sendRequest } from '../../helpers/sendRequest';
|
import { sendRequest } from '../../helpers/sendRequest';
|
||||||
import { formatCurrency } from '../../helpers/helper';
|
import { formatCurrency } from '../../helpers/helper';
|
||||||
|
import Modal from '../../components/Modal';
|
||||||
|
import RoomForm from './Form';
|
||||||
|
|
||||||
interface IRoomRow {
|
interface IRoomRow {
|
||||||
room: IRoom;
|
room: IRoom;
|
||||||
openFormDialog: () => void;
|
|
||||||
setRoom: Dispatch<SetStateAction<Nullable<IRoom>>>;
|
|
||||||
reload: boolean;
|
reload: boolean;
|
||||||
setReload: Dispatch<SetStateAction<boolean>>;
|
setReload: Dispatch<SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RoomRow = ({
|
const RoomRow = ({ room, reload, setReload }: IRoomRow) => {
|
||||||
room,
|
|
||||||
openFormDialog,
|
|
||||||
setRoom,
|
|
||||||
reload,
|
|
||||||
setReload,
|
|
||||||
}: IRoomRow) => {
|
|
||||||
const handleEdit = (room: IRoom) => {
|
|
||||||
setRoom(room);
|
|
||||||
openFormDialog();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (room: IRoom) => {
|
const handleDelete = async (room: IRoom) => {
|
||||||
if (confirm(CONFIRM_DELETE)) {
|
if (confirm(CONFIRM_DELETE)) {
|
||||||
const response = await sendRequest(ROOM_PATH + `/${room.id}`, 'DELETE');
|
const response = await sendRequest(ROOM_PATH + `/${room.id}`, 'DELETE');
|
||||||
@@ -67,9 +55,7 @@ const RoomRow = ({
|
|||||||
|
|
||||||
const { id, name, finalPrice, status } = room;
|
const { id, name, finalPrice, status } = room;
|
||||||
|
|
||||||
const statusText = status
|
const statusText = status ? 'Unavailable' : 'Available';
|
||||||
? 'Unavailable'
|
|
||||||
: 'Available';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
@@ -78,21 +64,30 @@ const RoomRow = ({
|
|||||||
<div>{formatCurrency(finalPrice)}</div>
|
<div>{formatCurrency(finalPrice)}</div>
|
||||||
<div>{statusText}</div>
|
<div>{statusText}</div>
|
||||||
|
|
||||||
<Menus.Menu>
|
<div>
|
||||||
<Menus.Toggle id={id.toString()} />
|
<Modal>
|
||||||
|
<Menus.Menu>
|
||||||
|
<Menus.Toggle id={id.toString()} />
|
||||||
|
|
||||||
<Menus.List id={id.toString()}>
|
<Menus.List id={id.toString()}>
|
||||||
<Menus.Button
|
<Modal.Open modalName="edit">
|
||||||
icon={<RiEditBoxFill />}
|
<Menus.Button icon={<RiEditBoxFill />}>Edit</Menus.Button>
|
||||||
onClick={() => handleEdit(room)}
|
</Modal.Open>
|
||||||
>
|
|
||||||
Edit
|
<Menus.Button
|
||||||
</Menus.Button>
|
icon={<HiTrash />}
|
||||||
<Menus.Button icon={<HiTrash />} onClick={() => handleDelete(room)}>
|
onClick={() => handleDelete(room)}
|
||||||
Delete
|
>
|
||||||
</Menus.Button>
|
Delete
|
||||||
</Menus.List>
|
</Menus.Button>
|
||||||
</Menus.Menu>
|
</Menus.List>
|
||||||
|
|
||||||
|
<Modal.Window name="edit" title="Edit Room">
|
||||||
|
<RoomForm room={room} />
|
||||||
|
</Modal.Window>
|
||||||
|
</Menus.Menu>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -100,16 +95,9 @@ const RoomRow = ({
|
|||||||
interface IRoomTable {
|
interface IRoomTable {
|
||||||
reload: boolean;
|
reload: boolean;
|
||||||
setReload: Dispatch<SetStateAction<boolean>>;
|
setReload: Dispatch<SetStateAction<boolean>>;
|
||||||
openFormDialog: () => void;
|
|
||||||
setRoom?: Dispatch<SetStateAction<Nullable<IRoom>>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const RoomTable = ({
|
const RoomTable = ({ reload, setReload }: IRoomTable) => {
|
||||||
reload,
|
|
||||||
setReload,
|
|
||||||
openFormDialog,
|
|
||||||
setRoom,
|
|
||||||
}: IRoomTable) => {
|
|
||||||
const [nameSearch, setNameSearch] = useState('');
|
const [nameSearch, setNameSearch] = useState('');
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [rooms, setRooms] = useState<IRoom[]>([]);
|
const [rooms, setRooms] = useState<IRoom[]>([]);
|
||||||
@@ -173,8 +161,6 @@ const RoomTable = ({
|
|||||||
key={room.id}
|
key={room.id}
|
||||||
reload={reload}
|
reload={reload}
|
||||||
setReload={setReload}
|
setReload={setReload}
|
||||||
openFormDialog={openFormDialog}
|
|
||||||
setRoom={setRoom!}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useRef, useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import RoomTable from './Table';
|
import RoomTable from './Table';
|
||||||
@@ -7,51 +7,35 @@ import RoomTable from './Table';
|
|||||||
import { StyledRoom, Title } from './styled';
|
import { StyledRoom, Title } from './styled';
|
||||||
import Direction from '../../commons/styles/Direction';
|
import Direction from '../../commons/styles/Direction';
|
||||||
import Button from '../../commons/styles/Button';
|
import Button from '../../commons/styles/Button';
|
||||||
import RoomDialog from './Dialog';
|
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { Nullable } from '../../types/common';
|
import Modal from '../../components/Modal';
|
||||||
import { IRoom } from '../../types/rooms';
|
import RoomForm from './Form';
|
||||||
|
|
||||||
const Room = () => {
|
const Room = () => {
|
||||||
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
||||||
const [reload, setReload] = useState(true);
|
const [reload, setReload] = useState(true);
|
||||||
const [room, setRoom] = useState<Nullable<IRoom>>(null);
|
|
||||||
const [isAdd, setIsAdd] = useState(false);
|
|
||||||
|
|
||||||
const openFormDialog = (isAddForm: boolean = false) => {
|
|
||||||
setIsAdd(isAddForm);
|
|
||||||
dialogRef.current?.showModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeFormDialog = () => {
|
|
||||||
dialogRef.current?.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledRoom>
|
<StyledRoom>
|
||||||
<Direction type="horizontal">
|
<Direction type="horizontal">
|
||||||
<Title>List Room</Title>
|
<Title>List Room</Title>
|
||||||
<Button onClick={() => openFormDialog(true)}>Add room</Button>
|
|
||||||
|
<Modal>
|
||||||
|
<Modal.Open modalName="cabin-form">
|
||||||
|
<Button>Add room</Button>
|
||||||
|
</Modal.Open>
|
||||||
|
<Modal.Window name="cabin-form" title="Add form">
|
||||||
|
<RoomForm setReload={setReload} reload={reload} />
|
||||||
|
</Modal.Window>
|
||||||
|
</Modal>
|
||||||
</Direction>
|
</Direction>
|
||||||
|
|
||||||
<RoomTable
|
<RoomTable
|
||||||
reload={reload}
|
reload={reload}
|
||||||
setReload={setReload}
|
setReload={setReload}
|
||||||
openFormDialog={() => openFormDialog()}
|
|
||||||
setRoom={setRoom}
|
|
||||||
/>
|
/>
|
||||||
</StyledRoom>
|
</StyledRoom>
|
||||||
|
|
||||||
<RoomDialog
|
|
||||||
onClose={closeFormDialog}
|
|
||||||
ref={dialogRef}
|
|
||||||
setReload={setReload}
|
|
||||||
reload={reload}
|
|
||||||
room={room}
|
|
||||||
isAdd={isAdd}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,10 @@
|
|||||||
--radius-sm: 5px;
|
--radius-sm: 5px;
|
||||||
--radius-md: 10px;
|
--radius-md: 10px;
|
||||||
|
|
||||||
|
--overlay-color: #0000004d;
|
||||||
|
|
||||||
|
--form-color: #fff;
|
||||||
|
|
||||||
--shadow-sm: 0px 10px 20px rgba(0, 0, 0, 0.2);
|
--shadow-sm: 0px 10px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
--fs-md: 30px;
|
--fs-md: 30px;
|
||||||
|
|||||||
Reference in New Issue
Block a user