add favicon, debounce, categories done
parent
13f332f8a5
commit
614a0630fd
Binary file not shown.
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 15 KiB |
@ -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 (
|
||||
<Box p="3" m="2" bg="white" rounded="lg">
|
||||
<Heading size="md">
|
||||
<Link to="/dashboard">
|
||||
dashboard {' '}
|
||||
</Link>
|
||||
<Link to={link}>
|
||||
/ {menu} {' '}
|
||||
</Link>
|
||||
{submenu && ` / ${submenu}`}
|
||||
</Heading>
|
||||
</Box>
|
||||
)
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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 (
|
||||
<Box px="3" mt="6">
|
||||
<InputGroup>
|
||||
<Input
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
placeholder="cari"
|
||||
focusBorderColor="red.500"
|
||||
/>
|
||||
<InputRightElement children={<FontAwesomeIcon icon="search" />} />
|
||||
</InputGroup>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<FormControl id={name} mb="3">
|
||||
<FormLabel fontWeight="bold">{name}</FormLabel>
|
||||
<Input
|
||||
focusBorderColor="red.500"
|
||||
type={type}
|
||||
value={val}
|
||||
onChange={e => setVal(e.target.value)}
|
||||
{...props}
|
||||
/>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
export function FormInputNumber(props) {
|
||||
const { data:[name, val, setVal] } = props
|
||||
return (
|
||||
<FormControl id={name} mb="3">
|
||||
<FormLabel fontWeight="bold">{name}</FormLabel>
|
||||
<NumberFormat
|
||||
customInput={Input}
|
||||
thousandSeparator="."
|
||||
decimalSeparator=","
|
||||
allowNegative={false}
|
||||
allowLeadingZeros={false}
|
||||
style={{textAlign: "right"}}
|
||||
value={val}
|
||||
onValueChange={e => setVal(e.value)}
|
||||
focusBorderColor="red.500"
|
||||
{...props}
|
||||
/>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
@ -1,17 +1,36 @@
|
||||
import { HStack, Box } from "@chakra-ui/layout"
|
||||
|
||||
export function usePaginate() {}
|
||||
export function Pagination() {
|
||||
function StyledBox(props) {
|
||||
return (
|
||||
<Box
|
||||
px="3"
|
||||
py="1"
|
||||
outlineColor="red.700"
|
||||
rounded="3xl"
|
||||
color="white"
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export function Pagination({ page = 1, setPage, totalPages = 1}) {
|
||||
return (
|
||||
<HStack spacing="2" mt="3" justifyContent="flex-end">
|
||||
<Box px="3" py="1" bg="red.200" outlineColor="red.700" rounded="3xl" color="white" _hover={{ bg: "red:700" }}>
|
||||
<StyledBox
|
||||
bg={page === 1 ? "red.200" : "red.500"}
|
||||
onClick={() => setPage(+page - 1 <= 0 ? 1 : +page - 1)}
|
||||
>
|
||||
{"<"}
|
||||
</Box>
|
||||
<Box px="3" py="1" bg="red.500" outlineColor="red.700" rounded="3xl" color="white" _hover={{ bg: "red:700" }}>
|
||||
1
|
||||
</Box>
|
||||
<Box px="3" py="1" bg="red.500" outlineColor="red.700" rounded="3xl" color="white" _hover={{ bg: "red:700" }}>
|
||||
</StyledBox>
|
||||
<StyledBox
|
||||
bg={page === (+totalPages > 0 ? +totalPages : 1) ? "red.200" : "red.500"}
|
||||
onClick={() => setPage(+page + 1 >= +totalPages ? +totalPages : +page + 1)}
|
||||
>
|
||||
>
|
||||
</Box>
|
||||
</StyledBox>
|
||||
</HStack>
|
||||
)
|
||||
}
|
@ -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"
|
@ -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;
|
@ -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
|
||||
})
|
||||
}
|
@ -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 (
|
||||
<>
|
||||
<Breadcrumb main={["/categories", "kategori", "baru"]}/>
|
||||
<Card>
|
||||
{error !== null && (
|
||||
<Alert status="error" mb="5" rounded="md">
|
||||
<AlertIcon />
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
<FormInput data={['nama', name, setName]}/>
|
||||
<FormInput data={['deskripsi', description, setDescription]}/>
|
||||
<Button
|
||||
mt="4"
|
||||
isLoading={submit}
|
||||
onClick={() => handleSubmit()}
|
||||
>
|
||||
simpan
|
||||
</Button>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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 (
|
||||
<>
|
||||
<Breadcrumb main={["/categories", "kategori", "ubah"]}/>
|
||||
<Card>
|
||||
{error !== null && (
|
||||
<Alert status="error" mb="5" rounded="md">
|
||||
<AlertIcon />
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
{loading ? (
|
||||
<Loading/>
|
||||
) : (
|
||||
<>
|
||||
<FormInput data={['nama', name, setName]}/>
|
||||
<FormInput data={['deskripsi', description, setDescription]}/>
|
||||
<Button
|
||||
mt="4"
|
||||
isLoading={submit}
|
||||
onClick={() => handleSubmit()}
|
||||
>
|
||||
simpan
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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 (
|
||||
<Alert status="error">
|
||||
<AlertIcon />
|
||||
{error.message}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb main={["/categories", "kategori"]}/>
|
||||
<Card>
|
||||
<Button as={Link} to="/categories/create" size="md">
|
||||
tambah
|
||||
</Button>
|
||||
<SearchInput setter={[search, setSearch]}/>
|
||||
{data ? (
|
||||
<>
|
||||
<Table variant="simple" mt="2" mb="4">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>nama</Th>
|
||||
<Th>deskripsi</Th>
|
||||
<Th isNumeric>{''}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{data.categories.map((category) => (
|
||||
<Tr key={category.id}>
|
||||
<Td>{ category.name }</Td>
|
||||
<Td>{ category.description }</Td>
|
||||
<Td isNumeric>
|
||||
<Menu>
|
||||
<MenuButton as={Button}>
|
||||
<FontAwesomeIcon icon="ellipsis-v"/>
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<MenuItem as={Link} to={`/categories/${category.id}/edit`}>ubah</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
toggle(category)
|
||||
}}
|
||||
>
|
||||
hapus
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
<Pagination page={page} setPage={setPage} totalPages={data.meta.totalPages}/>
|
||||
</>
|
||||
) : (
|
||||
<Loading/>
|
||||
)}
|
||||
</Card>
|
||||
<AlertDialog
|
||||
isOpen={isOpen}
|
||||
onClose={handleDelete}
|
||||
toggle={() => toggle()}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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 (
|
||||
<Switch>
|
||||
<Route path="/categories/:id/edit" exect component={Edit} />
|
||||
<Route path="/categories/create" exact component={Create} />
|
||||
<Route path="/categories" exact component={List} />
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
export default routes
|
@ -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 (
|
||||
<>
|
||||
<Breadcrumb main={["/products", "produk", "baru"]}/>
|
||||
<Card>
|
||||
<FormInput data={['nama', name, setName]}/>
|
||||
<FormInput data={['deskripsi', description, setDescription]}/>
|
||||
<FormInputNumber data={['harga beli', cost, setCost]} onClick={() => setCost('')}/>
|
||||
<FormInputNumber data={['harga jual', price, setPrice]} onClick={() => setPrice('')}/>
|
||||
<FormInputNumber data={['stok', stock, setStock]}/>
|
||||
<FormInput data={['kategori', category ? category.name : '']} readOnly={true}/>
|
||||
<Button mt="4">simpan</Button>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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 (
|
||||
<Switch>
|
||||
<Route path="/products/:id" exact component={ProductDetail} />
|
||||
<Route path="/products" exact component={ProductList} />
|
||||
<Route path="/products/create" exact component={Create} />
|
||||
<Route path="/products" exact component={List} />
|
||||
<Route path="/products/:id" exact component={Detail} />
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductRoutes
|
||||
export default routes
|
||||
|
Loading…
Reference in New Issue