diff --git a/public/assets/icons/right-paging.svg b/public/assets/icons/right-paging.svg new file mode 100644 index 0000000000000000000000000000000000000000..3c107bb88b27fee6c5e251a4f47adadfa05554a6 --- /dev/null +++ b/public/assets/icons/right-paging.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/icons/search.svg b/public/assets/icons/search.svg new file mode 100644 index 0000000000000000000000000000000000000000..e638e3081a228b3cf19bec36fd5d6abbe6a9ee7c --- /dev/null +++ b/public/assets/icons/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/icons/zoom.svg b/public/assets/icons/zoom.svg new file mode 100644 index 0000000000000000000000000000000000000000..c9d4a59c2a2eff6cd3dd0db81bef8aa42feadc22 --- /dev/null +++ b/public/assets/icons/zoom.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/App/components/Layout/index.test.tsx b/src/App/components/Layout/index.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cea7c9877887de040b8665176ff78181ab4ce91a --- /dev/null +++ b/src/App/components/Layout/index.test.tsx @@ -0,0 +1,20 @@ +/** + * @format + */ + +import React from 'react'; +import Layout from '.'; + +// Note: test renderer must be required after react-native. +import renderer from 'react-test-renderer'; +import { BrowserRouter } from 'react-router-dom'; + +it('renders correctly', () => { + // Default render + renderer.create( + + + + + ); +}); diff --git a/src/App/components/Layout/index.tsx b/src/App/components/Layout/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f9fd2e36dd17a490628a850d304d2e3d2c5cb5a7 --- /dev/null +++ b/src/App/components/Layout/index.tsx @@ -0,0 +1,124 @@ +import React, { useContext, useEffect } from 'react'; +import { Text, Box, Image, Gap, Button } from 'components'; +import styled from 'styled-components'; +import { Link, useLocation } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; +import { AppContext } from 'contexts'; +import { LocalStorage } from 'services'; + +interface LayoutProps { + children?: any; +} + +const navigators = [ + { + name: "Beranda", + to: "/", + }, + { + name: "Manajemen Akun", + to: "/account-management" + } +] + +const Layout = ((props: LayoutProps) => { + const { user, setToken, setUser, token } = useContext(AppContext) + const location = useLocation() + const history = useHistory() + + useEffect(() => { + if (token) { + history.push("/") + } + }, [token]) + + return ( + + + + + + + + Admin + + + + + + + {navigators.map((navigator, index) => { + const component = ( + + + {navigator.name} + + + ) + + if (index + 1 < navigators.length) { + return [ + component, + + ] + } + return component + })} + + + + + + + {user?.name} + + + + + + + {props.children} + + + + ); +}) + +export default Layout; + +const Container = styled.div` + display: flex; + width: 100%; + min-height: 100%; +` + +const Sidebar = styled.div` + display: flex; + flex-direction: column; + width: 20vw; +` + +const Content = styled.div` + background: #FAFAFA; + width: 80vw; + max-width: 80vw; + padding: 45px 0 32px 32px; +` + diff --git a/src/App/index.tsx b/src/App/index.tsx index 31372e0993cb939a4b041c9cf37c7b92d62d8551..92b49bfbccda394349d1f30b84087e865eae611c 100644 --- a/src/App/index.tsx +++ b/src/App/index.tsx @@ -5,8 +5,10 @@ import styled, { ThemeProvider } from 'styled-components'; import './style.css'; import { AppContext } from 'contexts'; -import { Home, Login } from 'scenes'; +import { Home, AccountManagement, Login } from 'scenes'; import { useMainService, LocalStorage } from 'services'; +import { Cloud } from 'components'; +import Layout from './components/Layout'; declare global { interface ThemeProps { @@ -65,15 +67,15 @@ interface SkyProps { const Sky = styled.div` position: fixed; - opacity: ${({shouldExist = true}: SkyProps) => shouldExist? 1: 0}; - pointer-events: ${({shouldExist = true}: SkyProps) => shouldExist? "visible": "none"}; + opacity: ${({ shouldExist = true }: SkyProps) => (shouldExist ? 1 : 0)}; + pointer-events: ${({ shouldExist = true }: SkyProps) => + shouldExist ? 'visible' : 'none'}; z-index: 1; top: 0px; left: 0px; height: 100vh; width: 100vw; - background: rgba(0, 0, 0, 0.6); -` +`; export default function App() { const [token, setToken] = useState(''); @@ -148,8 +150,15 @@ export default function App() { setShouldShowModal(false)} > + setShouldShowModal(false)} + /> {modal} @@ -158,7 +167,10 @@ export default function App() { ? ( // If user exists <> - + + + + ) : ( // Otherwise diff --git a/src/components/Box/index.tsx b/src/components/Box/index.tsx index a785448096e758dab438d1e9dadb784d61db7eac..0e2bf31ca7f4bd88b44c89b89ababbaf1fb02375 100644 --- a/src/components/Box/index.tsx +++ b/src/components/Box/index.tsx @@ -16,6 +16,7 @@ interface BoxProps { shouldWrap?: boolean; mainAxis?: string; crossAxis?: string; + align?: string; shouldExist?: boolean; border?: string; borderTop?: string; @@ -23,6 +24,10 @@ interface BoxProps { borderBottom?: string; borderLeft?: string; borderRadius?: string; + borderBottomLeftRadius?: string; + borderBottomRightRadius?: string; + borderTopLeftRadius?: string; + borderTopRightRadius?: string; margin?: string; padding?: string; clipPath?: string; @@ -69,14 +74,19 @@ const StyledBox = styled.div` chooseDirection(props.axis || Axis.Horizontal)}; justify-content: ${(props: BoxProps) => props.mainAxis || 'flex-start'}; align-items: ${(props: BoxProps) => props.crossAxis || 'flex-start'}; + align-self: ${(props: BoxProps) => props.align || 'flex-start'}; flex-wrap: ${(props: BoxProps) => (props.shouldWrap ? 'wrap' : 'nowrap')}; transform: ${(props: BoxProps) => props.transform || 'none'}; clip-path: ${(props: BoxProps) => props.clipPath || 'none'}; margin: ${(props: BoxProps) => props.margin || '0px'}; padding: ${(props: BoxProps) => props.padding || '0px'}; - + border: ${(props: BoxProps) => props.border || '0px'}; + border-bottom-left-radius: ${(props: BoxProps) => props.borderBottomLeftRadius || '0px'}; + border-bottom-right-radius: ${(props: BoxProps) => props.borderBottomRightRadius || '0px'}; + border-top-left-radius: ${(props: BoxProps) => props.borderTopLeftRadius || '0px'}; + border-top-right-radius: ${(props: BoxProps) => props.borderTopRightRadius || '0px'}; border-radius: ${(props: BoxProps) => props.borderRadius || '0px'}; border-top-width: ${(props: BoxProps) => splitArgs(props.border || props.borderTop || '0px solid black')[0]}; @@ -98,6 +108,7 @@ const StyledBox = styled.div` interface BoxType extends BoxProps { children?: React.ReactNode; + onClick?: () => void; } export default function Box({ children, ...props }: BoxType) { @@ -106,6 +117,4 @@ export default function Box({ children, ...props }: BoxType) { Box.Axis = Axis; -export { - StyledBox, -} \ No newline at end of file +export { StyledBox }; diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 43e5f78f6aec6488a39eebc3d61bd60d3d9f2fe3..5dfd376b8408583e5c7556dd90600a054590681a 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -22,7 +22,10 @@ const StyledButton = styled.button` padding: 8px 36px; height: auto; width: auto; +<<<<<<< HEAD +======= flex: 1 0 auto; +>>>>>>> staging width: ${(props: StyledButtonProps) => props.width || 'auto'}; background: ${(props: StyledButtonProps) => props.background}; border-width: ${(props: StyledButtonProps) => props.borderWidth || '0px'}; @@ -64,7 +67,7 @@ function Button({ children, clickable = true, isBold = true, - width, + width = 'auto', onClick = () => {}, }: ButtonProps) { const { colors } = useContext(ThemeContext) || DEFAULT_THEME; diff --git a/src/components/CategoryButton/index.test.tsx b/src/components/CategoryButton/index.test.tsx index 38867f2fe4aeea376434c4bf7c824fa12a16fdf2..154198647cdba87364cc21acd0e4e2ae8fade937 100644 --- a/src/components/CategoryButton/index.test.tsx +++ b/src/components/CategoryButton/index.test.tsx @@ -4,78 +4,75 @@ import React from 'react'; import CategoryButton from '.'; -import renderer, {act} from 'react-test-renderer'; +import renderer, { act } from 'react-test-renderer'; // Note: test renderer must be required after react-native. -describe("CategoryButton tests", () => { +describe('CategoryButton tests', () => { it('renders correctly', () => { const options = [ - {label: 'Laki-laki', value: 1}, - {label: 'Perempuan', value: 0}, + { label: 'Laki-laki', value: 1 }, + { label: 'Perempuan', value: 0 }, ]; - - let value; + + let value; const inst = renderer.create( - value = val} - />, + (value = val)} /> ); - + expect(inst).toBeTruthy(); - - const buttons = inst.root.findAll(elem => elem.props["data-test-id"] === "button"); + + const buttons = inst.root.findAll( + (elem) => elem.props['data-test-id'] === 'button' + ); act(() => { buttons[0].props.onClick(); }); - + expect(value).toBe(1); }); it('should be okay if there is no onClick function', () => { const options = [ - {label: 'Laki-laki', value: 1}, - {label: 'Perempuan', value: 0}, + { label: 'Laki-laki', value: 1 }, + { label: 'Perempuan', value: 0 }, ]; - const inst = renderer.create( - , - ); - + const inst = renderer.create(); + expect(inst).toBeTruthy(); }); - it("Should change value after set value from outside", () => { + it('Should change value after set value from outside', () => { const options = [ - {label: 'Laki-laki', value: 1}, - {label: 'Perempuan', value: 0}, + { label: 'Laki-laki', value: 1 }, + { label: 'Perempuan', value: 0 }, ]; - + let val; - + const inst = renderer.create( val = newValue} - />, + onClick={(newValue) => (val = newValue)} + /> ); - + expect(inst).toBeTruthy(); - - const buttons = inst.root.findAll(elem => elem.props["data-test-id"] === "button"); - - let numberIteration = 1 - const tolerance = 100 - - while(val !== 0 && numberIteration < tolerance) { + + const buttons = inst.root.findAll( + (elem) => elem.props['data-test-id'] === 'button' + ); + + let numberIteration = 1; + const tolerance = 100; + + while (val !== 0 && numberIteration < tolerance) { act(() => { buttons[1].props.onClick(); }); - numberIteration++ + numberIteration++; } expect(val).toBe(0); - }) -}) + }); +}); diff --git a/src/components/CategoryButton/index.tsx b/src/components/CategoryButton/index.tsx index 34ce8f5a8a98a52a5a6f94a334ec6faedc609f42..f6998a9e9fef0d425cc7a694663c4dc4e494bbcf 100644 --- a/src/components/CategoryButton/index.tsx +++ b/src/components/CategoryButton/index.tsx @@ -3,90 +3,93 @@ import { Box, Text } from 'components'; import styled, { ThemeContext } from 'styled-components'; const Click = styled.div` - display: flex; - flex-grow: 1; - position: relative; - cursor: pointer; -` + display: flex; + flex-grow: 1; + position: relative; + cursor: pointer; +`; -type LabelType = string -type ValueType = any +type LabelType = string; +type ValueType = any; interface ItemType { - label: LabelType; - value: ValueType; + label: LabelType; + value: ValueType; } interface CategoryButtonProps { - value?: ValueType, - values: Array, - onClick?: (value: ValueType) => void, - width?: string, + value?: ValueType; + values: Array; + onClick?: (value: ValueType) => void; + width?: string; } const DEFAULT_THEME = { - colors: { - green: "green", - black: "black", - totallyWhite: "white", - mediumGray: "gray", - } -} + colors: { + green: 'green', + black: 'black', + totallyWhite: 'white', + mediumGray: 'gray', + }, +}; export default function CategoryButton({ - value = "", - values = [], - onClick = () => {}, - width = "100%", + value = '', + values = [], + onClick = () => {}, + width = '100%', }: CategoryButtonProps) { - const { colors } = useContext(ThemeContext) || DEFAULT_THEME - const [innerValues, setInnerValues] = useState(values) - const [currentValue, setCurrentValue] = useState(value) + const { colors } = useContext(ThemeContext) || DEFAULT_THEME; + const [innerValues, setInnerValues] = useState(values); + const [currentValue, setCurrentValue] = useState(value); + + useEffect(() => { + setInnerValues(values); + }, [values]); - useEffect(() => { - setInnerValues(values) - }, [values]) - - useEffect(() => { - setCurrentValue(value) - }, [value]) + useEffect(() => { + setCurrentValue(value); + }, [value]); - useEffect(() => { - onClick(currentValue) - }, [currentValue]) + useEffect(() => { + onClick(currentValue); + }, [currentValue]); - useEffect(() => { - setCurrentValue(value || innerValues[0].value) - }, []) + useEffect(() => { + setCurrentValue(value || innerValues[0].value); + }, []); - return ( - - {innerValues.map((item: ItemType, index: number) => { - return ( - setCurrentValue(item.value)} - > - - - {item.label} - - - - ) - })} - - ) -} \ No newline at end of file + return ( + + {innerValues.map((item: ItemType, index: number) => { + return ( + setCurrentValue(item.value)} + > + + + {item.label} + + + + ); + })} + + ); +} diff --git a/src/components/Cloud/index.test.tsx b/src/components/Cloud/index.test.tsx index 6c83c5188aad99ce3942386fba3b2613db43fc59..d8fbbf7969aa1b7764bfe6be87aa376d23227514 100644 --- a/src/components/Cloud/index.test.tsx +++ b/src/components/Cloud/index.test.tsx @@ -3,13 +3,7 @@ import Cloud from '.'; import renderer from 'react-test-renderer'; it('should render properly', () => { - const inst = renderer.create( - , - ); - - expect(inst).toBeTruthy(); + const inst = renderer.create(); + + expect(inst).toBeTruthy(); }); diff --git a/src/components/Cloud/index.tsx b/src/components/Cloud/index.tsx index 81726b8678a8b17e6a22dd49ea99e16d67d0585e..f7147fbf4e09cd1b4d1e51579e56cadc66c32033 100644 --- a/src/components/Cloud/index.tsx +++ b/src/components/Cloud/index.tsx @@ -1,21 +1,21 @@ -import { StyledBox } from "components"; +import { StyledBox } from 'components'; import styled from 'styled-components'; interface CloudProps { - index?: string; - top?: string; - left?: string; - bottom?: string; - right?: string; + index?: string; + top?: string; + left?: string; + bottom?: string; + right?: string; } const Cloud = styled(StyledBox)` - position: absolute; - z-index: ${(props: CloudProps) => props.index}; - top: ${(props: CloudProps) => props.top}; - left: ${(props: CloudProps) => props.left}; - right: ${(props: CloudProps) => props.right}; - bottom: ${(props: CloudProps) => props.bottom}; -` + position: absolute; + z-index: ${(props: CloudProps) => props.index}; + top: ${(props: CloudProps) => props.top}; + left: ${(props: CloudProps) => props.left}; + right: ${(props: CloudProps) => props.right}; + bottom: ${(props: CloudProps) => props.bottom}; +`; export default Cloud; diff --git a/src/components/Content/index.test.tsx b/src/components/Content/index.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0a9a6e1ef0a57557571dbc2d2c190098e1fe1fe2 --- /dev/null +++ b/src/components/Content/index.test.tsx @@ -0,0 +1,20 @@ +/** + * @format + */ + +import React from 'react'; +import Content from '.'; + +// Note: test renderer must be required after react-native. +import renderer from 'react-test-renderer'; +import { BrowserRouter } from 'react-router-dom'; + +it('renders correctly', () => { + // Default render + renderer.create( + + + + + ); +}); diff --git a/src/components/Content/index.tsx b/src/components/Content/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e9b427143e4d478f2915ab7b040480a1e220770e --- /dev/null +++ b/src/components/Content/index.tsx @@ -0,0 +1,49 @@ +import React, { useContext } from 'react'; +import { Text, Box, Cloud, Gap } from 'components'; +import { ThemeContext } from 'styled-components'; + +interface ContentProps { + title: string; + children?: any; +} + +const DEFAULT_THEME = { + colors: { + mediumGray: 'gray', + } +} + +const Content = ((props: ContentProps) => { + const { colors } = useContext(ThemeContext) || DEFAULT_THEME + + return ( + + + + + + {props.title} + + + + + + {props.children} + + + + ); +}); + +export default Content; \ No newline at end of file diff --git a/src/components/Image/index.test.tsx b/src/components/Image/index.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..dac720a11b8af925e14a649008455433a1fa9bf7 --- /dev/null +++ b/src/components/Image/index.test.tsx @@ -0,0 +1,44 @@ +/** + * @format + */ + +import React from 'react'; +import Image from '.'; +import { AppContext } from 'contexts'; + +// Note: test renderer must be required after react-native. +import renderer, { act } from 'react-test-renderer'; + +describe('Image tests', () => { + it('renders correctly', () => { + const image = renderer.create( + + ); + expect(image).toBeTruthy() + }); + + it('should be able to zoom in the image', () => { + let someNumber = 1 + const image = renderer.create( + someNumber += 1}}> + + + ); + expect(image).toBeTruthy() + act(() => { + image.root.find(elem => elem.props['data-test-id'] === "image").props.onClick() + }) + expect(someNumber).toEqual(2) + }); +}) + diff --git a/src/components/Image/index.tsx b/src/components/Image/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7bdaf8879077271ff20657d45b7c73b96891e498 --- /dev/null +++ b/src/components/Image/index.tsx @@ -0,0 +1,60 @@ +import React, { useContext } from 'react' +import styled from 'styled-components'; +import { Cloud } from 'components'; +import { AppContext } from 'contexts'; + +interface StyledImageProps { + height?: string; + width?: string; + radius?: string; + fit?: string; +} + +const StyledImage = styled.img` + height: ${(props: StyledImageProps) => props.height || "auto"}; + width: ${(props: StyledImageProps) => props.width || "auto"}; + border-radius: ${(props: StyledImageProps) => props.radius || "0"}; + object-fit: ${(props: StyledImageProps) => props.fit || "cover"}; +` +interface ImageProps { + src: string; + height?: string; + width?: string; + radius?: string; + showable?: boolean; +} + +export default function Image({ + src, + height, + width, + radius, + showable = true, +}: ImageProps) { + const { setModal } = useContext(AppContext); + + return ( + { + if (setModal) { + setModal( + + ) + } + } : () => {} + } + /> + ) +} \ No newline at end of file diff --git a/src/components/Table/index.test.tsx b/src/components/Table/index.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..49f22c0702f163f464058e8ba4c5833bda21798d --- /dev/null +++ b/src/components/Table/index.test.tsx @@ -0,0 +1,272 @@ +/** + * @format + */ + +import React from 'react'; +import Table from '.'; + +// Note: test renderer must be required after react-native. +import renderer, { act } from 'react-test-renderer'; + +describe('Table tests', () => { + const createTable = (values) => { + let table + act(() => { + table = renderer.create( + { + values.searchValue = searchValue + values.pageNumber = pageNumber + + return [ + [ + 'Muhammad Rasyid', + 'page', + 'irsyad77@gmail.com', + '08128237232', + 'Jl. Bunga 3 Lestari', + Math.random() * 100, + ], + [ + 'Muhammad Rasyid', + 'page', + 'irsyad77@gmail.com', + '08128237232', + 'Jl. Bunga 3 Lestari', + Math.random() * 100, + ], + [ + 'Muhammad Rasyid', + 'page', + 'irsyad77@gmail.com', + '08128237232', + 'Jl. Bunga 3 Lestari', + Math.random() * 100, + ], + [ + 'Muhammad Rasyid', + 'page', + 'irsyad77@gmail.com', + '08128237232', + 'Jl. Bunga 3 Lestari', + Math.random() * 100, + ], + [ + 'Muhammad Rasyid', + 'page', + 'irsyad77@gmail.com', + '08128237232', + 'Jl. Bunga 3 Lestari', + Math.random() * 100, + ], + [ + 'Muhammad Rasyid', + 'page', + 'irsyad77@gmail.com', + '08128237232', + 'Jl. Bunga 3 Lestari', + Math.random() * 100, + ], + [ + 'Muhammad Rasyid', + 'page', + 'irsyad77@gmail.com', + '08128237232', + 'Jl. Bunga 3 Lestari', + Math.random() * 100, + ], + [ + 'Muhammad Rasyid', + 'page', + 'irsyad77@gmail.com', + '08128237232', + 'Jl. Bunga 3 Lestari', + Math.random() * 100, + ], + [ + 'Muhammad Rasyid', + 'page', + 'irsyad77@gmail.com', + '08128237232', + 'Jl. Bunga 3 Lestari', + Math.random() * 100, + ], + [ + 'Muhammad Rasyid', + 'page', + 'irsyad77@gmail.com', + '08128237232', + 'Jl. Bunga 3 Lestari', + Math.random() * 100, + ], + ]; + }} + /> + ); + }) + return table + } + + it('should works perfectly with fine properties', () => { + const values = { + searchValue: "", + pageNumber: 0, + } + const table = createTable(values) + expect(table).toBeTruthy(); + }); + + it('should not works with inconsistent properties', () => { + expect(() => { + const table = renderer.create( +
+ ) + }).toThrow() + }) + + it('should not be able to go previous page when already on first page', () => { + const values = { + searchValue: "", + pageNumber: 1, + } + const table = createTable(values) + + act(() => { + table.root.find(elem => elem.props["data-test-id"] === "prev-button").props.onClick() + }) + + expect(values.searchValue).toEqual("") + expect(values.pageNumber).toEqual(1) + }); + + it('should be able to go next page', () => { + const values = { + searchValue: "", + pageNumber: 1, + } + const table = createTable(values) + act(() => { + table.root.find(elem => elem.props["data-test-id"] === "next-button").props.onClick() + }) + + expect(values.searchValue).toEqual("") + expect(values.pageNumber).toEqual(2) + }); + + it('should be able to search and expect on the first page', () => { + const values = { + searchValue: "", + pageNumber: 1, + } + const table = createTable(values) + const SEARCH_VALUE = "adabakadaba" + act(() => { + table.root.find(elem => elem.props["data-test-id"] === "search-input").props.onChange({ + target: { + value: SEARCH_VALUE, + } + }) + }) + + act(() => { + table.root.find(elem => elem.props["data-test-id"] === "search-button").props.onClick() + }) + + expect(values.searchValue).toEqual(SEARCH_VALUE) + expect(values.pageNumber).toEqual(1) + }); +}); diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5b661175e5fd125f741c057320f1990792a1dfdb --- /dev/null +++ b/src/components/Table/index.tsx @@ -0,0 +1,311 @@ +import React, { useState, useEffect, useContext } from 'react'; +import styled, { ThemeContext } from 'styled-components'; + +import { Button, Box, Cloud, Text, Gap } from 'components'; + +type ValueType = string | number; + +interface TableProps { + header: Array; + data: Array>; + onChange?: (value: string, pageNumber: number) => Array>; + searchPlaceholder?: string; + maximumData?: number; + rowOnClick?: (row: Array) => void +} + +const Click = styled.div` + cursor: pointer; +` + +interface IconProps { + height?: string; + width?: string; + cursor?: string; + origin?: string; + transform?: string; + opacity?: number; +} + +const Icon = styled.img` + cursor: ${(props: IconProps) => props.cursor || 'auto'}; + height: ${(props: IconProps) => props.height || 'auto'}; + width: ${(props: IconProps) => props.width || 'auto'}; + origin: ${(props: IconProps) => props.origin || '50% 50%'}; + transform: ${(props: IconProps) => props.transform || 'none'}; + opacity: ${(props: IconProps) => props.opacity || 1}; +`; + +interface SearchBarProps { + theme: ThemeProps; +} + +const SearchBar = styled.input` + display: flex; + flex-grow: 1; + border: none; + height: 100%; + background: ${({ theme }: SearchBarProps) => + theme?.colors?.almostWhite || 'transparent'}; + + padding: 0 0 0 44px; + font-family: Dosis; + font-weight: 400; + border-radius: 100px 3px 3px 100px; +`; + +const StyledTable = styled.table` + border-collapse: collapse; + width: 100%; +`; + +interface RowProps { + background?: string; + theme: ThemeProps; +} + +const Row = styled.tr` + box-sizing: border-box; + height: 34px; + background: ${({ background }: RowProps) => background || 'transparent'}; + clip-path: inset(0 round 100px); +`; + +const TableHeader = styled.th` + padding: 0 2px; +` + +const DEFAULT_THEME = { + colors: { + mediumGray: 'gray', + black: 'black', + almostWhite: 'gray', + }, +}; + +enum PageAction { + Next = 1, + Prev = -1, +} + +export default function Table({ + header, + data, + searchPlaceholder = 'Search something...', + onChange = () => [[]], + maximumData = -1, + rowOnClick, +}: TableProps) { + const { colors } = useContext(ThemeContext) || DEFAULT_THEME; + const [innerData, setInnerData] = useState(data); + const [previousDataTotal, setPreviousDataTotal] = useState(0); + const [pageNumber, setPageNumber] = useState(1); + const [searchValue, setSearchValue] = useState(''); + + const changePage = async (action: PageAction) => { + let sign: number = 0; + switch (action) { + case PageAction.Next: + sign = 1; + break; + case PageAction.Prev: + sign = -1; + break; + } + const newPageNumber = pageNumber + sign; + if ( + maximumData > 0 && + previousDataTotal + ((1 + sign) / 2) * innerData.length >= maximumData + ) + return; + + const newData: Array> = await onChange( + searchValue, + newPageNumber + ); + + if (newData.length === 0 || newData[0].length === 0) return; + setPageNumber(newPageNumber); + setPreviousDataTotal(previousDataTotal + sign * innerData.length); + setInnerData([...newData]); + }; + + const [couldGoBack, couldGoNext] = [ + pageNumber > 1, + maximumData <= 0 || previousDataTotal + innerData.length < maximumData, + ]; + + useEffect(() => { + setInnerData(data); + }, [JSON.stringify(data)]); + + if (innerData[0] && (header.length > innerData[0].length)) { + throw new Error('The shape of Header and data are not consistent'); + } + + return ( + + + + + + + setSearchValue(e.target.value.toString())} + /> + + + + + + + + + {header.map((head: string, index: number) => { + return ( + + + + + + {head} + + + + + ); + })} + + + + {innerData.map((row: Array, index: number) => { + return ( + + {row.filter((_, index) => index < header.length).map((value: ValueType, index: number) => { + return ( + + ); + })} + {rowOnClick? ( + + ) : <>} + + ); + })} + + + {innerData.length === 0 + ? ( + + + Tidak ada data + + + ) : <> + } + + + + + {`${previousDataTotal + innerData.length}${ + maximumData > 0 ? ` dari ${maximumData}` : '' + }`} + + + changePage(PageAction.Prev) : () => {}} + src="/assets/icons/right-paging.svg" + origin="50% 50%" + height="16px" + opacity={couldGoBack ? 1 : 0.5} + transform="rotateZ(180deg)" + /> + + {`${pageNumber}`} + + changePage(PageAction.Next) : () => {}} + src="/assets/icons/right-paging.svg" + height="16px" + opacity={couldGoNext ? 1 : 0.5} + /> + + + + ); +} diff --git a/src/components/index.ts b/src/components/index.ts index f9e1eb80589bb43c14ae504f131b697ccdfc4406..8f369e881953ca5dbe440ddd11289719e37ae30a 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -5,5 +5,20 @@ import Gap from './Gap'; import Field from './Field'; import CategoryButton from './CategoryButton'; import Cloud from './Cloud'; +import Table from './Table'; +import Image from './Image'; +import Content from './Content/index'; -export { StyledBox, Box, Button, Text, Gap, Field, CategoryButton, Cloud }; +export { + StyledBox, + Box, + Button, + Text, + Gap, + Field, + CategoryButton, + Cloud, + Table, + Image, + Content, +}; diff --git a/src/constant/index.ts b/src/constant/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..c3d674a90330e0974ad882cfd448effecad6c3a2 --- /dev/null +++ b/src/constant/index.ts @@ -0,0 +1,161 @@ +const KELURAHAN_VALUES: {label: string; value: string}[] = [ + {label: 'Beji', value: 'Beji'}, + {label: 'Beji Timur', value: 'Beji Timur'}, + {label: 'Kemirimuka', value: 'Kemirimuka'}, + {label: 'Kukusan', value: 'Kukusan'}, + {label: 'Pondok Cina', value: 'Pondok Cina'}, + {label: 'Tanah Baru', value: 'Tanah Baru'}, + {label: 'Bojongsari Baru', value: 'Bojongsari Baru'}, + {label: 'Bojongsari Lama', value: 'Bojongsari Lama'}, + {label: 'Curug', value: 'Curug'}, + {label: 'Duren Mekar', value: 'Duren Mekar'}, + {label: 'Duren Seribu', value: 'Duren Seribu'}, + {label: 'Pondok Petir', value: 'Pondok Petir'}, + {label: 'Serua', value: 'Serua'}, + {label: 'Cilodong', value: 'Cilodong'}, + {label: 'Jatimulya', value: 'Jatimulya'}, + {label: 'Kalibaru', value: 'Kalibaru'}, + {label: 'Kalimulya', value: 'Kalimulya'}, + {label: 'Sukamaju', value: 'Sukamaju'}, + {label: 'Cisalak Pasar', value: 'Cisalak Pasar'}, + {label: 'Curug', value: 'Curug'}, + {label: 'Harjamukti', value: 'Harjamukti'}, + {label: 'Mekarsari', value: 'Mekarsari'}, + {label: 'Pasir Gunung Selatan', value: 'Pasir Gunung Selatan'}, + {label: 'Tugu', value: 'Tugu'}, + {label: 'Cinere', value: 'Cinere'}, + {label: 'Gandul', value: 'Gandul'}, + {label: 'Pangkalan Jati', value: 'Pangkalan Jati'}, + {label: 'Pangkalan Jati Baru', value: 'Pangkalan Jati Baru'}, + {label: 'Bojong Pondok Terong', value: 'Bojong Pondok Terong'}, + {label: 'Cipayung', value: 'Cipayung'}, + {label: 'Cipayung Jaya', value: 'Cipayung Jaya'}, + {label: 'Pondok Jaya', value: 'Pondok Jaya'}, + {label: 'Ratujaya', value: 'Ratujaya'}, + {label: 'Grogol', value: 'Grogol'}, + {label: 'Krukut', value: 'Krukut'}, + {label: 'Limo', value: 'Limo'}, + {label: 'Meruyung', value: 'Meruyung'}, + {label: 'Depok', value: 'Depok'}, + {label: 'Depok Jaya', value: 'Depok Jaya'}, + {label: 'Mampang', value: 'Mampang'}, + {label: 'Pancoran Mas', value: 'Pancoran Mas'}, + {label: 'Rangkapan Jaya', value: 'Rangkapan Jaya'}, + {label: 'Rangkapan Jaya Baru', value: 'Rangkapan Jaya Baru'}, + {label: 'Bedahan', value: 'Bedahan'}, + {label: 'Cinangka', value: 'Cinangka'}, + {label: 'Kedaung', value: 'Kedaung'}, + {label: 'Pasir Putih', value: 'Pasir Putih'}, + {label: 'Pengasinan', value: 'Pengasinan'}, + {label: 'Sawangan Baru', value: 'Sawangan Baru'}, + {label: 'Sawangan Lama', value: 'Sawangan Lama'}, + {label: 'Abadijaya', value: 'Abadijaya'}, + {label: 'Bakti Jaya', value: 'Bakti Jaya'}, + {label: 'Cisalak', value: 'Cisalak'}, + {label: 'Mekar Jaya', value: 'Mekar Jaya'}, + {label: 'Sukmajaya', value: 'Sukmajaya'}, + {label: 'Tirtajaya', value: 'Tirtajaya'}, + {label: 'Cilangkap', value: 'Cilangkap'}, + {label: 'Cimpaeun', value: 'Cimpaeun'}, + {label: 'Jatijajar', value: 'Jatijajar'}, + {label: 'Leuwinanggung', value: 'Leuwinanggung'}, + {label: 'Sukamaju Baru', value: 'Sukamaju Baru'}, + {label: 'Sukatani', value: 'Sukatani'}, + {label: 'Tapos', value: 'Tapos'}, +]; + +const SIGNUP_CATEGORY = [ + { label: 'Kader', value: false}, + { label: 'Admin', value: true}, +]; + +const ACCOUNT_CATEGORY = [ + { label: 'Terverifikasi', value: true}, + { label: 'Permintaan', value: false}, +]; + +const DUMMY_TABLE = [ + [ + 'Muhammad Rasyid', + 'page', + 'irsyad77@gmail.com', + '08128237232', + 'Jl. Bunga 3 Lestari', + Math.random() * 100, + ], + [ + 'Muhammad Rasyid', + 'page', + 'irsyad77@gmail.com', + '08128237232', + 'Jl. Bunga 3 Lestari', + Math.random() * 100, + ], + [ + 'Muhammad Rasyid', + 'page', + 'irsyad77@gmail.com', + '08128237232', + 'Jl. Bunga 3 Lestari', + Math.random() * 100, + ], + [ + 'Muhammad Rasyid', + 'page', + 'irsyad77@gmail.com', + '08128237232', + 'Jl. Bunga 3 Lestari', + Math.random() * 100, + ], + [ + 'Muhammad Rasyid', + 'page', + 'irsyad77@gmail.com', + '08128237232', + 'Jl. Bunga 3 Lestari', + Math.random() * 100, + ], + [ + 'Muhammad Rasyid', + 'page', + 'irsyad77@gmail.com', + '08128237232', + 'Jl. Bunga 3 Lestari', + Math.random() * 100, + ], + [ + 'Muhammad Rasyid', + 'page', + 'irsyad77@gmail.com', + '08128237232', + 'Jl. Bunga 3 Lestari', + Math.random() * 100, + ], + [ + 'Muhammad Rasyid', + 'page', + 'irsyad77@gmail.com', + '08128237232', + 'Jl. Bunga 3 Lestari', + Math.random() * 100, + ], + [ + 'Muhammad Rasyid', + 'page', + 'irsyad77@gmail.com', + '08128237232', + 'Jl. Bunga 3 Lestari', + Math.random() * 100, + ], + [ + 'Muhammad Rasyid', + 'page', + 'irsyad77@gmail.com', + '08128237232', + 'Jl. Bunga 3 Lestari', + Math.random() * 100, + ], +]; + +export {KELURAHAN_VALUES, SIGNUP_CATEGORY, ACCOUNT_CATEGORY, DUMMY_TABLE}; + \ No newline at end of file diff --git a/src/contexts/AppContext/types.ts b/src/contexts/AppContext/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..bcb6a401d26d57cfb1d5dd8e8ad4e0e3f68cba4c --- /dev/null +++ b/src/contexts/AppContext/types.ts @@ -0,0 +1,11 @@ +export interface Account { + id: string, + username: string, + name: string, + email: string, + phone_number: string, + area: string, + is_admin: boolean, + is_verified: boolean, + is_active: boolean +} \ No newline at end of file diff --git a/src/helper/hooks/useFormState/index.tsx b/src/helper/hooks/useFormState/index.tsx index 641d23a1e6e123d6b20597d8467c119e0faf22ef..016330042b3816931c6d2a62ae0d55757b841091 100644 --- a/src/helper/hooks/useFormState/index.tsx +++ b/src/helper/hooks/useFormState/index.tsx @@ -6,7 +6,7 @@ function selectFieldPattern(type: string, priorityPattern?: RegExp): RegExp { } switch (type.toUpperCase()) { case 'ANY': - return /^.+$/; + return /^.*$/; case 'TEXT': return /^[a-zA-Z ]+$/; case 'TEXTAREA': @@ -110,9 +110,11 @@ interface OutputFormType { fields: OutputFieldsType; } -type OutputSetterType = (name: string, value: string) => void +type OutputSetterType = (name: string, value: string) => void; -export default function useFormState(fields: FieldsInObjectType): [OutputFormType, OutputSetterType] { +export default function useFormState( + fields: FieldsInObjectType +): [OutputFormType, OutputSetterType] { const [form, dispatchForm] = useReducer(formManipulator, { isValid: false, fields: Object.keys(fields).reduce( @@ -153,6 +155,5 @@ export default function useFormState(fields: FieldsInObjectType): [OutputFormTyp const setField = (name: string, value: string) => { dispatchForm({ type: FormActionTypes.Update, field: { name, value } }); }; - return [returnedForm, setField]; } diff --git a/src/scenes/AccountManagement/components/UserProfile/index.test.tsx b/src/scenes/AccountManagement/components/UserProfile/index.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1635ab9bd32af45b7067203abf37bc7d611190b6 --- /dev/null +++ b/src/scenes/AccountManagement/components/UserProfile/index.test.tsx @@ -0,0 +1,24 @@ +/** + * @format + */ + +import React from 'react'; +import UserProfile from '.'; + +// Note: test renderer must be required after react-native. +import renderer from 'react-test-renderer'; + +it('renders correctly', () => { + // For horizontal axis + renderer.create( + ); +}); diff --git a/src/scenes/AccountManagement/components/UserProfile/index.tsx b/src/scenes/AccountManagement/components/UserProfile/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e252e3b787379275035f38930a0eafcae2257911 --- /dev/null +++ b/src/scenes/AccountManagement/components/UserProfile/index.tsx @@ -0,0 +1,221 @@ +import React, { useState, useEffect, useContext } from 'react'; + +import { KELURAHAN_VALUES, ACCOUNT_CATEGORY, SIGNUP_CATEGORY, DUMMY_TABLE } from 'constant'; +import { Box, Image, Cloud, Button, Field, Gap } from 'components'; +import { useFormState } from 'helper'; +import { AppContext } from 'contexts'; + +interface UserProfileProps { + id: string; + name: string; + username: string; + email: string; + phoneNumber: string; + area: string; + numberOfActivities: number; + is_admin: boolean; + is_verified: boolean; + is_active: boolean; +} + +const userFormFormat = { + name: { + type: "text" + }, + username: { + type: "text" + }, + email: { + type: "email" + }, + phoneNumber: { + type: "text" + }, + area: { + type: "text" + } +} + +const APP_CONTEXT = { + services: { + main: { + editAccount: () => {} + } + } +} + +export default function UserProfile({ + id, + name, + username, + email, + phoneNumber, + area, + is_admin, + is_verified, + is_active, + numberOfActivities = -1, +}: UserProfileProps) { + const { services } = useContext(AppContext) || APP_CONTEXT + const [shouldEdit, setShouldEdit] = useState(false); + const [userForm, setUserField] = useFormState(userFormFormat); + const [isVerified, setIsVerified] = useState(is_verified); + + useEffect(() => { + setUserField("name", name); + setUserField("username", username); + setUserField("email", email); + setUserField("phoneNumber", phoneNumber); + setUserField("area", area); + }, []) + + console.log("test") + return ( + + + + + + + {/* */} + + {/* {!shouldEdit + ? ( + + ) : <> + } */} + + + {/* User profile */} + + + setUserField('name', value)} + /> + + setUserField('username', value)} + /> + + setUserField('email', value)} + /> + + + + setUserField('phoneNumber', value)} + /> + {/* + setUserField('area', value)} + /> */} + + + + + + {!shouldEdit + ? ( + + ) : <> + } + + + ) +} \ No newline at end of file diff --git a/src/scenes/AccountManagement/index.test.tsx b/src/scenes/AccountManagement/index.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5be7cd45fd85a40a918eac47e5a20aa6dd895097 --- /dev/null +++ b/src/scenes/AccountManagement/index.test.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import AccountManagement from '.'; + +it('render AccountManagement component without crashing', () => { + shallow(); +}); diff --git a/src/scenes/AccountManagement/index.tsx b/src/scenes/AccountManagement/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b0b325be35aec169b6b915698c45760c40c70abe --- /dev/null +++ b/src/scenes/AccountManagement/index.tsx @@ -0,0 +1,251 @@ +import React, { useContext, useState, useEffect } from 'react'; +import { ThemeContext } from 'styled-components'; + +import { + Box, + Gap, + CategoryButton, + Cloud, + Table, + Content, + Text, +} from 'components'; +import { AppContext } from 'contexts'; +import { useFormState } from 'helper'; +import { Account } from 'contexts/AppContext/types'; +import { KELURAHAN_VALUES, ACCOUNT_CATEGORY, SIGNUP_CATEGORY, DUMMY_TABLE } from 'constant'; +import UserProfile from './components/UserProfile'; + +const DEFAULT_THEME = { + colors: { + totallyWhite: 'white', + mediumGray: 'gray', + } +} + +export default function Home() { + const { setModal } = useContext(AppContext); + const { colors } = useContext(ThemeContext) || DEFAULT_THEME; + // const [signupForm, setField] = useFormState({ + // name: { type: 'text' }, + // username: { type: 'any' }, + // password: { type: 'password' }, + // email: { type: 'email' }, + // phone_number: { type: 'phone' }, + // area: { type: 'any' }, + // }); + + const [page, setPage] = useState(1); + const [accountCount, setAccountCount] = useState(0); + const [masterData, setMasterData] = useState([]); + const [cleanedData, setCleanedData] = useState([["namamu", "namamu", "namamu", "namamu", "namamu", "namamu"]]); + // const [accountCategory, setAccountCategory] = useState(true); + const global = useContext(AppContext); + + const fetchAccounts = async ( + isFilter:boolean, pageNumber: number, query?: string) => { + const response = isFilter + ? await global.services.main.filterAccountByName(query) + : await global.services.main.listAccounts(pageNumber); + + if (response.status === 200) { + const { data } = response; + setAccountCount(data.count); + setMasterData(data.results); + + const cleanedAccounts: Array> = []; + data.results.forEach((acc: Account) => { + const currentAccount = []; + + currentAccount.push(acc.name); + currentAccount.push(acc.username); + currentAccount.push(acc.email); + currentAccount.push(acc.phone_number); + currentAccount.push(acc.area); + currentAccount.push(String(0)); + currentAccount.push(acc.id); + currentAccount.push(acc.is_admin); + currentAccount.push(acc.is_verified); + currentAccount.push(acc.is_active); + cleanedAccounts.push(currentAccount); + }); + + // Workaround, TODO need to handle empty table + if (cleanedAccounts.length === 0) { + setCleanedData([['', '', '', '', '', '']]); + } else { + setCleanedData(cleanedAccounts); + } + } + }; + + + // const changeAccountCategory = () => { + // setAccountCategory(!accountCategory); + + // const cleanedAccounts: string[][] = []; + // masterData.forEach((acc: Account) => { + // const currentAccount = []; + + // if (acc.is_verified === accountCategory) { + // currentAccount.push(acc.name); + // currentAccount.push(acc.username); + // currentAccount.push(acc.email); + // currentAccount.push(acc.phone_number); + // currentAccount.push(acc.area); + // currentAccount.push(String(0)); + // cleanedAccounts.push(currentAccount); + // } + // }); + // setCleanedData(cleanedAccounts); + // }; + + useEffect(() => { + fetchAccounts(false, page); + }, [page]); + + return ( + + + {/* + + + + + + setField('name', value)} + /> + setField('username', value)} + /> + setField('password', value)} + /> + setField('email', value)} + /> + setField('phone_number', value)} + /> + setField('area', value)} + /> + + + + + + + */} + + + Coming soon + + + + + + + + + + + + + +
+ + {value.toString()} + + + + + { + rowOnClick([...row]) + }} + mainAxis="center" + crossAxis="center" + background={colors.green} + > + + + + +
{ + setModal( + + + + ); + }} + maximumData={accountCount} + data={cleanedData} + onChange={(searchValue, pageNumber) => { + if (pageNumber !== page) { + setPage(pageNumber); + } else { + fetchAccounts(true, pageNumber, searchValue); + } + return cleanedData; + }} + /> + + + + + ); +} diff --git a/src/scenes/Home/index.tsx b/src/scenes/Home/index.tsx index 0df73ee514c68c2e313c173b914900eea1594620..a2c98230883008847559aca6ff55e7e17ae3a074 100644 --- a/src/scenes/Home/index.tsx +++ b/src/scenes/Home/index.tsx @@ -1,101 +1,61 @@ import React, { useContext } from 'react'; +import { ThemeContext } from 'styled-components'; -import { Box, Gap, Field, Text, Button, CategoryButton, Cloud } from 'components'; -import { AppContext } from 'contexts'; -import { useFormState } from 'helper'; +import { + Box, + Text, + Content, + Gap, +} from 'components'; -const formData = { - title: { - type: "text", +const DEFAULT_THEME = { + colors: { + totallyWhite: "white", + mediumGray: "gray", } } export default function Home() { - const { setModal } = useContext(AppContext) - const [form, setField] = useFormState(formData) - + const { colors } = useContext(ThemeContext) || DEFAULT_THEME; + return ( - - - - - Hellooo - - - setField("title", value)} - /> - - - - + + + + Coming soon + + + - + + + + Coming soon + + + ); } diff --git a/src/scenes/index.ts b/src/scenes/index.ts index 2b2312400a054e79228c581c59a269ae7ebab837..a1f31e98278adc1d47325f149d9b918aa7aa9ff4 100644 --- a/src/scenes/index.ts +++ b/src/scenes/index.ts @@ -1,4 +1,9 @@ import Home from './Home'; +import AccountManagement from './AccountManagement'; import Login from './Login'; -export { Home, Login }; +export { + Home, + AccountManagement, + Login, +}; diff --git a/src/services/LocalStorage/index.test.tsx b/src/services/LocalStorage/index.test.tsx index 490b5b937f694bbfe858a6ca268a8c7b33a7947a..8f64ce73495ef66d3bb9150bceeb54b3480abf34 100644 --- a/src/services/LocalStorage/index.test.tsx +++ b/src/services/LocalStorage/index.test.tsx @@ -1,49 +1,48 @@ import LocalStorage from '.'; -describe("Local storage tests", () => { - const SECRET_KEY: string = "My-super-secret-key"; - it("Should fail when there is not secret key", async () => { - await expect(LocalStorage.setItem("test", "value")).rejects.toThrow(); - await expect(LocalStorage.getItem("test")).rejects.toThrow(); - }) - - it("Should able to use after initialize the secret key", async () => { - // Initialization - LocalStorage.setSecretKey(SECRET_KEY); - - const key: string = "test"; - const value: string = "value"; - const foo = await LocalStorage.setItem(key, value); - const result = await LocalStorage.getItem(key); - expect(result).toEqual(value); - }) - - it("Should get null value if there is no value on local storage", async () => { - // Initialization - LocalStorage.setSecretKey(SECRET_KEY); - - const key: string = "key"; - const result = await LocalStorage.getItem(key); - expect(result).toEqual(null); - }) - - it("Should fail if use null key", async () => { - // Initialization - LocalStorage.setSecretKey(SECRET_KEY); - - const key: any = undefined; - const value: any = undefined; - await expect(LocalStorage.setItem(key, value)).rejects.toThrow(); - await expect(LocalStorage.getItem(key)).rejects.toThrow(); - }) - - - it("Should fail if key exist, but value is null", async () => { - // Initialization - LocalStorage.setSecretKey(SECRET_KEY); - - const key: any = "key"; - const value: any = undefined; - await expect(LocalStorage.setItem(key, value)).rejects.toThrow(); - }) -}) \ No newline at end of file +describe('Local storage tests', () => { + const SECRET_KEY: string = 'My-super-secret-key'; + it('Should fail when there is not secret key', async () => { + await expect(LocalStorage.setItem('test', 'value')).rejects.toThrow(); + await expect(LocalStorage.getItem('test')).rejects.toThrow(); + }); + + it('Should able to use after initialize the secret key', async () => { + // Initialization + LocalStorage.setSecretKey(SECRET_KEY); + + const key: string = 'test'; + const value: string = 'value'; + const foo = await LocalStorage.setItem(key, value); + const result = await LocalStorage.getItem(key); + expect(result).toEqual(value); + }); + + it('Should get null value if there is no value on local storage', async () => { + // Initialization + LocalStorage.setSecretKey(SECRET_KEY); + + const key: string = 'key'; + const result = await LocalStorage.getItem(key); + expect(result).toEqual(null); + }); + + it('Should fail if use null key', async () => { + // Initialization + LocalStorage.setSecretKey(SECRET_KEY); + + const key: any = undefined; + const value: any = undefined; + await expect(LocalStorage.setItem(key, value)).rejects.toThrow(); + await expect(LocalStorage.getItem(key)).rejects.toThrow(); + }); + + it('Should fail if key exist, but value is null', async () => { + // Initialization + LocalStorage.setSecretKey(SECRET_KEY); + + const key: any = 'key'; + const value: any = undefined; + await expect(LocalStorage.setItem(key, value)).rejects.toThrow(); + }); +}); diff --git a/src/services/LocalStorage/index.tsx b/src/services/LocalStorage/index.tsx index a2dca22fc76963bdae5f262edddf1cdbdacb2fbe..275e532d93fe30cea0b810e106fbc312945be27a 100644 --- a/src/services/LocalStorage/index.tsx +++ b/src/services/LocalStorage/index.tsx @@ -8,12 +8,12 @@ export default class LocalStorage { if (value === null) throw new Error('Value can not be empty'); if (!LocalStorage.secretKey) throw new Error( - 'Need to initialize the local storage first with a private key', + 'Need to initialize the local storage first with a private key' ); const digest = CryptoJS.AES.encrypt( JSON.stringify(value), - LocalStorage.secretKey, + LocalStorage.secretKey ); await localStorage.setItem(key, digest.toString()); } diff --git a/src/services/hooks/useMainService/index.tsx b/src/services/hooks/useMainService/index.tsx index 74451a40b2bb28d1b1339a2d6af81f8c96f12a88..360138282701779bf33938b68df00fad749013b8 100644 --- a/src/services/hooks/useMainService/index.tsx +++ b/src/services/hooks/useMainService/index.tsx @@ -70,9 +70,31 @@ export default function useMainService(token: string) { return fetchWithAuthentication(endPoint, Method.GET); } + async function listAccounts(page: number) { + if (page === 1) { + const endpoint = END_POINTS.ACCOUNTS([null]); + return fetchWithAuthentication(endpoint, Method.GET); + } + const endpointWithPaging = END_POINTS.ACCOUNTS([`?page=${page}`]); + return fetchWithAuthentication(endpointWithPaging.slice(0, -1), Method.GET); + } + + async function filterAccount(name: string = "", username: string = "", email: string = "") { + const endpoint = END_POINTS.ACCOUNTS([`?name=${name}&username=%{username}&email=${email}`]); + return fetchWithAuthentication(endpoint.slice(0, -1), Method.GET); + } + + async function editAccount(id: string, data: object) { + const endpoint = END_POINTS.ACCOUNTS([id]) + return fetchWithAuthentication(endpoint, Method.PUT, data) + } + return { // Authentication login, me, + listAccounts, + filterAccount, + editAccount, }; } diff --git a/src/services/index.ts b/src/services/index.ts index 3e9a5e753a360e2f4b94fc850cbe149c93f2d3f0..131ac3a3ad935501a65e61eeb914e3a370cae50a 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,7 +1,4 @@ import useMainService from './hooks/useMainService'; import LocalStorage from './LocalStorage'; -export { - useMainService, - LocalStorage, -}; +export { useMainService, LocalStorage };