Merge pull request #8 from Nez27/feat/add-table-and-menu-components

Add table and menu context components
This commit is contained in:
Loi Phan
2023-10-24 15:12:52 +07:00
committed by GitHub
10 changed files with 313 additions and 0 deletions
@@ -0,0 +1,83 @@
import { useContext, useState } from 'react';
// Components
import { HiEllipsisVertical } from 'react-icons/hi2';
import { useOutsideClick } from '../../hooks/useOutsideClick';
import { StyledMenu, StyledButton, StyledList, StyledToggle } from './styled';
// Interfaces
import { IButton } from '../../globals/interfaces';
// Contexts
import MenusContext from '../../contexts/MenuContext';
const Menus = ({ children }: { children: JSX.Element }) => {
const [openId, setOpenId] = useState('');
const close = () => setOpenId('');
const open = setOpenId;
return (
<MenusContext.Provider value={{ openId, close, open }}>
{children}
</MenusContext.Provider>
);
};
const Toggle = ({ id }: { id: string }): React.JSX.Element => {
const { openId, close, open } = useContext(MenusContext);
const handleClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
// prettier-ignore
openId === '' || openId !== id
? open!(id)
: close!();
};
return (
<StyledToggle onClick={handleClick}>
<HiEllipsisVertical />
</StyledToggle>
);
};
const List = ({
id,
children,
}: {
id: string;
children: JSX.Element[];
}): React.JSX.Element | null => {
const { openId, close } = useContext(MenusContext);
const ref = useOutsideClick(close!, false);
if (openId !== id) return null;
return <StyledList ref={ref}>{children}</StyledList>;
};
const Button = ({ children, icon, onClick }: IButton): React.JSX.Element => {
const { close } = useContext(MenusContext);
const handleClick = () => {
onClick?.();
close!();
};
return (
<li>
<StyledButton onClick={handleClick}>
{icon} <span>{children}</span>
</StyledButton>
</li>
);
};
Menus.Menu = StyledMenu;
Menus.Toggle = Toggle;
Menus.List = List;
Menus.Button = Button;
export default Menus;
@@ -0,0 +1,65 @@
import styled from 'styled-components';
const StyledMenu = styled.div`
position: relative;
display: flex;
align-items: center;
justify-content: flex-end;
`;
const StyledButton = styled.button`
width: 100%;
text-align: left;
background: none;
border: none;
padding: 10px 20px;
font-size: var(--fs-sm-x);
transition: all 0.2s;
display: flex;
align-items: center;
gap: 10px;
&:hover {
background-color: var(--hover-background-color);
}
& svg {
width: 18px;
height: 18px;
color: var(--primary-color);
transition: all 0.3s;
}
`;
const StyledToggle = styled.button`
background: none;
border: none;
padding: 8px;
border-radius: var(--radius-sm);
transform: translateX(5px);
transition: all 0.2s;
&:hover {
background-color: var(--hover-background-color);
}
& svg {
width: 18px;
height: 18px;
}
`;
const StyledList = styled.ul`
position: absolute;
z-index: 1000;
background-color: white;
box-shadow: var(--shadow-sm);
border-radius: var(--radius-sm);
right: -10px;
top: 40px;
`;
export { StyledMenu, StyledButton, StyledList, StyledToggle };
@@ -0,0 +1,13 @@
import styled from 'styled-components';
const StyledMessage = styled.p`
font-size: var(--fs-sm);
text-align: center;
padding: 20px;
`;
const Message = ({ children }: { children: string }) => {
return <StyledMessage>{children}</StyledMessage>;
};
export default Message;
@@ -0,0 +1,44 @@
import { ReactNode, useContext } from 'react';
// Components
import { StyledBody, StyledHeader, StyledRow, StyledTable } from './styled';
// Interfaces
import { ITable, ITableBody } from '../../globals/interfaces';
// Contexts
import TableContext from '../../contexts/TableContext';
type CallbackMapFunc<T> = (value: T, index: number, array: T[]) => ReactNode;
const Table = ({ columns, children }: ITable) => {
return (
<TableContext.Provider value={columns!}>
<StyledTable>{children}</StyledTable>
</TableContext.Provider>
);
};
const Header = ({ children }: ITable) => {
const columns = useContext(TableContext);
return <StyledHeader columns={columns}>{children}</StyledHeader>;
};
const Body = <T,>({ data, render }: ITableBody<T>) => {
return (
data!.length && (
<StyledBody>{data?.map(render as CallbackMapFunc<T>)}</StyledBody>
)
);
};
const Row = ({ children }: ITable) => {
const columns = useContext(TableContext);
return <StyledRow columns={columns}>{children}</StyledRow>;
};
Table.Header = Header;
Table.Body = Body;
Table.Row = Row;
export default Table;
@@ -0,0 +1,43 @@
import styled from 'styled-components';
// Interfaces
import { ITable } from '../../globals/interfaces';
export const StyledTable = styled.div`
border: 1px solid var(--border-color);
font-size: 20px;
border-radius: var(--radius-md);
`;
const CommonRow = styled.div<ITable>`
display: grid;
grid-template-columns: ${(props) => props.columns};
column-gap: 10px;
align-items: center;
`;
export const StyledBody = styled.div`
font-size: var(--fs-sm-x);
`;
export const StyledRow = styled(CommonRow)`
padding: 20px;
&:not(:last-child) {
border-bottom: 1px solid var(--border-color);
}
`;
export const StyledHeader = styled(CommonRow)`
padding: 10px 20px;
border-bottom: 1px solid var(--border-color);
background-color: var(--header-table-color);
border-top-left-radius: var(--radius-md);
border-top-right-radius: var(--radius-md);
font-size: var(--fs-sm);
text-transform: capitalize;
font-weight: 600;
`;
@@ -0,0 +1,8 @@
import { createContext } from 'react';
// Interfaces
import { IMenusContext } from '../globals/interfaces';
const MenusContext = createContext<IMenusContext>({});
export default MenusContext;
@@ -0,0 +1,5 @@
import { createContext } from 'react';
const TableContext = createContext('');
export default TableContext;
@@ -0,0 +1,21 @@
export interface IMenusContext {
openId?: string;
close?: () => void;
open?: React.Dispatch<React.SetStateAction<string>>;
}
export interface IButton {
children?: string;
icon?: JSX.Element;
onClick?: () => void;
}
export interface ITable {
columns?: string;
children: React.ReactNode;
}
export interface ITableBody<T> {
data?: T[];
render?: (value: T) => JSX.Element;
}
+8
View File
@@ -0,0 +1,8 @@
export type TUser = {
id: string;
name: string;
identifiedCode: string;
address: string;
phone: string;
roomId: string;
};
@@ -0,0 +1,23 @@
import { useEffect, useRef } from 'react';
export const useOutsideClick = (
handler: () => void,
listeningCapturing = true,
): React.MutableRefObject<HTMLUListElement | null> => {
const ref = useRef<HTMLUListElement>(null);
useEffect(() => {
const handleClick = (e: Event) => {
if (ref.current && !ref.current.contains(e.target as Node)) {
handler();
}
};
document.addEventListener('click', handleClick, listeningCapturing);
return () =>
document.removeEventListener('click', handleClick, listeningCapturing);
}, [handler, listeningCapturing]);
return ref;
};