diff --git a/public/favicon.ico b/public/favicon.ico old mode 100644 new mode 100755 index a11777c..5bb6ee6 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/src/App.js b/src/App.js index 86f1a5f..f17fef5 100644 --- a/src/App.js +++ b/src/App.js @@ -9,6 +9,8 @@ import { Route, BrowserRouter, } from 'react-router-dom' +import { SWRConfig } from "swr"; +import axios from "axios"; import "react-datepicker/dist/react-datepicker.css"; import "@fontsource/raleway/400.css" import "@fontsource/open-sans/700.css" @@ -25,7 +27,10 @@ const Login = React.lazy(() => import('./views/auth/Login')) const Register = React.lazy(() => import('./views/auth/Register')) const Dashboard = React.lazy(() => import('./layouts/Dashboard')) +// font awesome library.add(fas) + +// chakra ui custom theme const customTheme = extendTheme(withDefaultColorScheme( { colorScheme: "red", @@ -36,25 +41,48 @@ const customTheme = extendTheme(withDefaultColorScheme( } )) +// config fatcher for swr with axios +const fetcher = (url, token) => axios({ + method: "GET", + url: url, + headers: { + 'Authorization': `Bearer ${token}` + } +}).then(res => { + return res.data.data +}) class App extends React.Component { + render() { return ( - - - - - }> - - }/> - }/> - }/> - }/> - }/> - - - - - + // handle global value + + {/* handle useSwr to fetch api */} + + {/* react router dom */} + + {/* handle chakra ui */} + + {/* handle app crash */} + + }> + + }/> + }/> + }/> + }/> + }/> + + + + + + ); } diff --git a/src/api/index.js b/src/api/index.js index 206de0c..a0439d5 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -1,23 +1,9 @@ -import axios from "axios" import useSWR from "swr" -import { useAuth } from "../context/AppContext" -import { formatDate } from "../utils" -const fetcher = (url, token) => axios({ - method: "GET", - url: url, - headers: { - 'Authorization': `Bearer ${token}` - } -}).then(res => { - return res.data.data -}) - -export function useProducts({startDate, endDate}) { - const { user } = useAuth() +export function useProducts(user, { page = 1, q = '' }) { const { data, error } = useSWR([ - `/products?startDate=${formatDate(startDate)}&endDate=${formatDate(endDate)}`, user.accessToken - ], fetcher) + `/products?page=${page}&q=${q}`, user.accessToken + ]) return [ data, @@ -25,6 +11,13 @@ export function useProducts({startDate, endDate}) { ] } -export function useProduct(id) { +export function useCategories(user, { page = 1, q }) { + const { data, error } = useSWR([ + `/categories?page=${page}&q=${q}`, user.accessToken + ]) -} \ No newline at end of file + return [ + data, + error + ] +} diff --git a/src/components/Common/Alert.js b/src/components/Common/Alert.js index 35935d8..728da30 100644 --- a/src/components/Common/Alert.js +++ b/src/components/Common/Alert.js @@ -1,4 +1,4 @@ -import { useRef } from "react" +import { useRef, useState } from "react" import { Button, AlertDialog, @@ -11,6 +11,7 @@ import { export default function Alert({ isOpen, onClose, toggle }) { const cancelRef = useRef() + const [loading, setLoading] = useState(false) return ( <> @@ -22,18 +23,27 @@ export default function Alert({ isOpen, onClose, toggle }) { - Delete Customer + hapus - Are you sure? You can't undo this action afterwards. + anda yakin akan menghapus ? - - diff --git a/src/components/Common/Breadcrumb.js b/src/components/Common/Breadcrumb.js new file mode 100644 index 0000000..2559f60 --- /dev/null +++ b/src/components/Common/Breadcrumb.js @@ -0,0 +1,18 @@ +import { Box, Heading } from '@chakra-ui/react' +import { Link } from 'react-router-dom' + +export default function Breadcrumb({ main: [link, menu, submenu]}) { + return ( + + + + dashboard {' '} + + + / {menu} {' '} + + {submenu && ` / ${submenu}`} + + + ) +} \ No newline at end of file diff --git a/src/components/Common/Hooks.js b/src/components/Common/Hooks.js new file mode 100644 index 0000000..ae8dfd0 --- /dev/null +++ b/src/components/Common/Hooks.js @@ -0,0 +1,17 @@ +import { useState } from "react" + +export function useModalState(init = false) { + const [isOpen, setIsOpen] = useState(init) + const [val, setVal] = useState({}) + + const toggle = (data = {}) => { + setIsOpen(!isOpen) + setVal(data) + } + + return { + isOpen, + toggle, + selected: val + } +} \ No newline at end of file diff --git a/src/components/Common/Input.js b/src/components/Common/Input.js new file mode 100644 index 0000000..403159b --- /dev/null +++ b/src/components/Common/Input.js @@ -0,0 +1,74 @@ +import { useEffect, useState } from "react"; +import { FormControl, FormLabel, Input, Box, InputGroup, InputRightElement } from "@chakra-ui/react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import NumberFormat from "react-number-format"; + +export function SearchInput({ setter: [search, setSearch] }) { + return ( + + + setSearch(e.target.value)} + placeholder="cari" + focusBorderColor="red.500" + /> + } /> + + + ) +} + +export function useDebounce(value, delay) { + const [debouncedValue, setDebouncedValue] = useState(value); + useEffect( + () => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + return () => { + clearTimeout(handler); + }; + }, + [value, delay] + ); + return debouncedValue; +} + +export function FormInput(props) { + const { data: [name, val, setVal = () => {}, type = "text"]} = props + return ( + + {name} + setVal(e.target.value)} + {...props} + /> + + ) +} + +export function FormInputNumber(props) { + const { data:[name, val, setVal] } = props + return ( + + {name} + setVal(e.value)} + focusBorderColor="red.500" + {...props} + /> + + ) +} \ No newline at end of file diff --git a/src/components/Common/Pagination.js b/src/components/Common/Pagination.js index c471547..41e38c0 100644 --- a/src/components/Common/Pagination.js +++ b/src/components/Common/Pagination.js @@ -1,17 +1,36 @@ import { HStack, Box } from "@chakra-ui/layout" + export function usePaginate() {} -export function Pagination() { + function StyledBox(props) { + return ( + + {props.children} + + ) +} + +export function Pagination({ page = 1, setPage, totalPages = 1}) { return ( - + setPage(+page - 1 <= 0 ? 1 : +page - 1)} + > {"<"} - - - 1 - - + + 0 ? +totalPages : 1) ? "red.200" : "red.500"} + onClick={() => setPage(+page + 1 >= +totalPages ? +totalPages : +page + 1)} + > > - + ) } \ No newline at end of file diff --git a/src/components/Common/index.js b/src/components/Common/index.js new file mode 100644 index 0000000..ef733c8 --- /dev/null +++ b/src/components/Common/index.js @@ -0,0 +1,8 @@ +export { default as Breadcrumb } from "./Breadcrumb" +export { default as Card } from "./Card" +export { default as Loading } from "./Loading" +export { default as AlertDialog } from "./Alert" +export * from "./DatePickerFilter" +export * from "./Pagination" +export * from "./Input" +export * from "./Hooks" diff --git a/src/layouts/_routeDashboard.js b/src/layouts/_routeDashboard.js index 7fe9e34..f31b238 100644 --- a/src/layouts/_routeDashboard.js +++ b/src/layouts/_routeDashboard.js @@ -1,11 +1,13 @@ import React from "react"; const Dashboard = React.lazy(() => import("../views/dashboard")); -const ListProduct = React.lazy(() => import("../views/products")); +const Product = React.lazy(() => import("../views/products")); +const Categories = React.lazy(() => import("../views/categories")); const routes = [ { path: '/', exact: true, name: 'Home' }, { path: "/dashboard", name: "dashboard", component: Dashboard }, - { path: "/products", name: "product", component: ListProduct } + { path: "/products", name: "produk", component: Product }, + { path: "/categories", name: "kategori", component: Categories } ] export default routes; \ No newline at end of file diff --git a/src/views/categories/Api.js b/src/views/categories/Api.js new file mode 100644 index 0000000..409d146 --- /dev/null +++ b/src/views/categories/Api.js @@ -0,0 +1,59 @@ +import axios from "axios"; + +export function createCategory(payload, token) { + return axios({ + method: 'POST', + url: '/categories', + data: payload, + headers: { + 'Authorization': `Bearer ${token}` + } + }) + .then(res => res.data) + .catch(err => { + throw err.response.data + }) +} + +export function getCategory(id, token){ + return axios({ + method: 'GET', + url: `/categories/${id}`, + headers: { + 'Authorization': `Bearer ${token}` + } + }) + .then(res => res.data.data) + .catch(err => { + throw err.response.data + }) +} + +export function updateCategory(id, payload, token) { + return axios({ + method: 'PUT', + url: `/categories/${id}`, + data: payload, + headers: { + 'Authorization': `Bearer ${token}` + } + }) + .then(res => res.data) + .catch(err => { + throw err.response.data + }) +} + +export function deleteCategory(id, token) { + return axios({ + method: 'DELETE', + url: `/categories/${id}`, + headers: { + 'Authorization': `Bearer ${token}` + } + }) + .then(res => res.data) + .catch(err => { + throw err.response.data + }) +} \ No newline at end of file diff --git a/src/views/categories/Create.js b/src/views/categories/Create.js new file mode 100644 index 0000000..26e7e3e --- /dev/null +++ b/src/views/categories/Create.js @@ -0,0 +1,66 @@ +import { + Button, + Alert, + AlertIcon, + useToast +} from "@chakra-ui/react" +import { createCategory } from "./Api" +import { useState } from "react" +import { Breadcrumb, Card, FormInput } from "../../components/Common" +import { useAuth } from "../../context/AppContext" + +export default function Create({ history }) { + const { user } = useAuth() + + const toast = useToast() + + const [name, setName] = useState('') + const [description, setDescription] = useState('') + const [submit, setSubmit] = useState(false) + const [error, setError] = useState(null) + + const handleSubmit = () => { + setSubmit(true) + createCategory({ name, description: description ? description : '' }, user.accessToken) + .then(res => { + toast({ + title: res.status, + description: "item ditambahkan", + status: "success", + duration: 4000, + isClosable: true, + position: "top-right" + }) + history.push('/categories') + }) + .catch(err => { + setError(err.message) + }) + .finally(() => { + setSubmit(false) + }) + } + + return ( + <> + + + {error !== null && ( + + + {error} + + )} + + + + + + ) +} \ No newline at end of file diff --git a/src/views/categories/Edit.js b/src/views/categories/Edit.js new file mode 100644 index 0000000..a9294f7 --- /dev/null +++ b/src/views/categories/Edit.js @@ -0,0 +1,92 @@ +import { + Button, + Alert, + AlertIcon, + useToast +} from "@chakra-ui/react" +import { getCategory, updateCategory } from "./Api" +import { useState } from "react" +import { Breadcrumb, Card, FormInput, Loading } from "../../components/Common" +import { useAuth } from "../../context/AppContext" +import { useEffect } from "react" + +export default function Edit(props) { + const id = props.match.params.id + const { user } = useAuth() + + const toast = useToast() + + const [name, setName] = useState('') + const [description, setDescription] = useState('') + + const [loading, setLoading] = useState(false) + const [submit, setSubmit] = useState(false) + const [error, setError] = useState(null) + + const handleSubmit = () => { + setSubmit(true) + updateCategory(id, { + name, + description: description ? description : '' + }, user.accessToken) + .then(res => { + toast({ + title: res.status, + description: "item diubah", + status: "success", + duration: 4000, + isClosable: true, + position: "top-right" + }) + props.history.push('/categories') + }) + .catch(err => { + setError(err.message) + }) + .finally(() => { + setSubmit(false) + }) + } + + useEffect(() => { + const { accessToken: token } = user + setLoading(true) + getCategory(id, token) + .then(res => { + setName(res.category.name) + setDescription(res.category.description) + }) + .catch(err => setError(err.message)) + .finally(() => setLoading(false)) + return () => {} + }, [id, user]) + + return ( + <> + + + {error !== null && ( + + + {error} + + )} + {loading ? ( + + ) : ( + <> + + + + + )} + + + ) +} \ No newline at end of file diff --git a/src/views/categories/List.js b/src/views/categories/List.js new file mode 100644 index 0000000..148b888 --- /dev/null +++ b/src/views/categories/List.js @@ -0,0 +1,138 @@ +import { useState } from "react" +import { + Button, + Alert, + AlertIcon, + Table, + Thead, + Tr, + Td, + Th, + Tbody, + Menu, + MenuItem, + MenuButton, + MenuList, + useToast, +} from "@chakra-ui/react" +import { Link } from "react-router-dom" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import { mutate } from 'swr' +import { + Breadcrumb, + Card, + Loading, + Pagination, + useDebounce, + AlertDialog, + SearchInput, + useModalState, +} from "../../components/Common" +import { useCategories } from "../../api" +import { deleteCategory } from "./Api" +import { useAuth } from "../../context/AppContext" + +export default function List() { + const { user } = useAuth() + const toast = useToast() + + const [page, setPage] = useState(1) + const [search, setSearch] = useState('') + const q = useDebounce(search, 300) + + const [data, error] = useCategories(user, { page, q }) + + const {isOpen, toggle, selected} = useModalState(false) + + const handleDelete = async () => { + await deleteCategory(selected.id, user.accessToken) + .then((res) => { + toast({ + title: res.status, + description: "item dihapus", + status: "success", + position: "top-right", + duration: 4000, + isClosable: true + }) + mutate([`/categories?page=${page}&q=${q}`, user.accessToken]) + }) + .catch((err) => { + toast({ + title: err.status, + description: err.message, + status: "error", + position: "top-right", + duration: 4000, + isClosable: true + }) + }) + } + + if(error) { + return ( + + + {error.message} + + ) + } + + return ( + <> + + + + + {data ? ( + <> + + + + + + + + + + {data.categories.map((category) => ( + + + + + + ))} + +
namadeskripsi{''}
{ category.name }{ category.description } + + + + + + ubah + { + toggle(category) + }} + > + hapus + + + +
+ + + ) : ( + + )} +
+ toggle()} + /> + + ) +} \ No newline at end of file diff --git a/src/views/categories/index.js b/src/views/categories/index.js new file mode 100644 index 0000000..98f7c54 --- /dev/null +++ b/src/views/categories/index.js @@ -0,0 +1,17 @@ +import { Switch, Route } from 'react-router-dom' + +import Create from './Create' +import List from './List' +import Edit from './Edit' + +function routes(props) { + return ( + + + + + + ) +} + +export default routes diff --git a/src/views/products/Create.js b/src/views/products/Create.js new file mode 100644 index 0000000..6d3a236 --- /dev/null +++ b/src/views/products/Create.js @@ -0,0 +1,29 @@ +import { + Button, +} from "@chakra-ui/react" +import { useState } from "react" +import { Breadcrumb, Card, FormInput, FormInputNumber } from "../../components/Common" + +export default function Create() { + const [name, setName] = useState('') + const [description, setDescription] = useState('') + const [cost, setCost] = useState(0) + const [price, setPrice] = useState(0) + const [stock, setStock] = useState(0) + const [category, setCategory] = useState({}) + + return ( + <> + + + + + setCost('')}/> + setPrice('')}/> + + + + + + ) +} \ No newline at end of file diff --git a/src/views/products/List.js b/src/views/products/List.js index e25cea7..1fcf076 100644 --- a/src/views/products/List.js +++ b/src/views/products/List.js @@ -1,25 +1,34 @@ +import { useState } from "react" import { Button, Alert, AlertIcon, - Box, Table, Thead, Tr, Td, Th, Tbody, - Heading } from "@chakra-ui/react" import { useProducts } from "../../api" -import Card from "../../components/Common/Card" -import Loading from "../../components/Common/Loading" -import { DatePickerFilter, useDatePickerFilter } from "../../components/Common/DatePickerFilter" +import { + Breadcrumb, + Card, + Loading, + SearchInput, + Pagination, + useDebounce +} from "../../components/Common" import { formatIDR } from "../../utils" +import { Link } from "react-router-dom" +import { useAuth } from "../../context/AppContext" export default function List({ history }) { - const [ startDate, endDate, setter ] = useDatePickerFilter() - const [ data, error ] = useProducts({startDate, endDate}) + const { user } = useAuth() + const [page, setPage] = useState(1) + const [search, setSearch] = useState('') + const q = useDebounce(search, 600) + const [ data, error ] = useProducts(user, { page, q }) const handleItemClick = (id) => { history.push(`/products/${id}`) @@ -36,13 +45,14 @@ export default function List({ history }) { return ( <> - - dashboard / produk - + - - + + {data ? ( + <> @@ -63,6 +73,8 @@ export default function List({ history }) { ))}
+ + ) : ( )} diff --git a/src/views/products/index.js b/src/views/products/index.js index d605664..089eda8 100644 --- a/src/views/products/index.js +++ b/src/views/products/index.js @@ -1,15 +1,17 @@ import { Switch, Route } from 'react-router-dom' -import ProductList from './List' -import ProductDetail from './Detail' +import Create from './Create' +import List from './List' +import Detail from './Detail' -function ProductRoutes(props) { +function routes(props) { return ( - - + + + ) } -export default ProductRoutes +export default routes