useSWR, refreshToken axios intercept, update readme, fixing typo

dev
Aji Kamaludin 3 years ago
parent cecfe95901
commit 78fb71ee9b
No known key found for this signature in database
GPG Key ID: 670E1F26AD5A8099

@ -12,6 +12,7 @@ contoh react SPA POS ( point of sales ) built with react
- pembelian - pembelian
- penjualan - penjualan
- diskon penjualan - diskon penjualan
- `UI : ChakraUI (https://chakra-ui.com/)`
### start ### start
- install - install

21
package-lock.json generated

@ -29,6 +29,7 @@
"react": "^17.0.2", "react": "^17.0.2",
"react-datepicker": "^4.2.0", "react-datepicker": "^4.2.0",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-number-format": "^4.7.3",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"swr": "^0.5.6", "swr": "^0.5.6",
@ -17986,6 +17987,18 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
}, },
"node_modules/react-number-format": {
"version": "4.7.3",
"resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-4.7.3.tgz",
"integrity": "sha512-4EvcANjstypQ5anhanmdEioGc49qbnErfS+yqbhatC0vzQ1okplkWNb0DIY7ABu4RhaxzttEz6pypEy8KsqgBQ==",
"dependencies": {
"prop-types": "^15.7.2"
},
"peerDependencies": {
"react": "^0.14 || ^15.0.0-0 || ^16.0.0-0 || ^17.0.0-0",
"react-dom": "^0.14 || ^15.0.0-0 || ^16.0.0-0 || ^17.0.0-0"
}
},
"node_modules/react-onclickoutside": { "node_modules/react-onclickoutside": {
"version": "6.11.2", "version": "6.11.2",
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.11.2.tgz", "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.11.2.tgz",
@ -37117,6 +37130,14 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
}, },
"react-number-format": {
"version": "4.7.3",
"resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-4.7.3.tgz",
"integrity": "sha512-4EvcANjstypQ5anhanmdEioGc49qbnErfS+yqbhatC0vzQ1okplkWNb0DIY7ABu4RhaxzttEz6pypEy8KsqgBQ==",
"requires": {
"prop-types": "^15.7.2"
}
},
"react-onclickoutside": { "react-onclickoutside": {
"version": "6.11.2", "version": "6.11.2",
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.11.2.tgz", "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.11.2.tgz",

@ -25,6 +25,7 @@
"react": "^17.0.2", "react": "^17.0.2",
"react-datepicker": "^4.2.0", "react-datepicker": "^4.2.0",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-number-format": "^4.7.3",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"swr": "^0.5.6", "swr": "^0.5.6",

@ -16,9 +16,11 @@ import "./axiosSetup"
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { fas } from '@fortawesome/free-solid-svg-icons' import { fas } from '@fortawesome/free-solid-svg-icons'
import { AppProvider } from "./context/AppContext" import { AppProvider } from "./context/AppContext"
import ErrorBoundary from "./context/ErrorBoundary";
import NotFound from "./views/errors/404" import NotFound from "./views/errors/404"
import Loading from "./components/Common/Loading" import Loading from "./components/Common/Loading"
import AppCrash from "./views/errors/500";
const Login = React.lazy(() => import('./views/auth/Login')) const Login = React.lazy(() => import('./views/auth/Login'))
const Register = React.lazy(() => import('./views/auth/Register')) const Register = React.lazy(() => import('./views/auth/Register'))
@ -41,14 +43,17 @@ class App extends React.Component {
<AppProvider> <AppProvider>
<BrowserRouter> <BrowserRouter>
<ChakraProvider theme={customTheme}> <ChakraProvider theme={customTheme}>
<ErrorBoundary>
<React.Suspense fallback={<Loading/>}> <React.Suspense fallback={<Loading/>}>
<Switch> <Switch>
<Route path="/login" exect={true} render={(props) => <Login {...props} />}/> <Route path="/login" exect={true} render={(props) => <Login {...props} />}/>
<Route path="/register" exect={true} render={(props) => <Register {...props} />}/> <Route path="/register" exect={true} render={(props) => <Register {...props} />}/>
<Route path="/error" exect={true} render={(props) => <AppCrash {...props} />}/>
<Route path="/" render={(props) => <Dashboard {...props} />}/> <Route path="/" render={(props) => <Dashboard {...props} />}/>
<Route path="*" render={(props) => <NotFound/>}/> <Route path="*" render={(props) => <NotFound/>}/>
</Switch> </Switch>
</React.Suspense> </React.Suspense>
</ErrorBoundary>
</ChakraProvider> </ChakraProvider>
</BrowserRouter> </BrowserRouter>
</AppProvider> </AppProvider>

@ -1,36 +1,36 @@
export const navs = [ export const navs = [
{ {
name: "Dashboard", name: "dashboard",
to: "/dashboard", to: "/dashboard",
icon: "clipboard-list", icon: "clipboard-list",
role: "admin", role: "admin",
}, },
{ {
name: "Kasir", name: "kasir",
to: "/sales/create", to: "/sales/create",
icon: "cash-register", icon: "cash-register",
role: "kasir", role: "kasir",
}, },
{ {
name: "Penjualan", name: "penjualan",
to: "/sales", to: "/sales",
icon: "money-bill-wave", icon: "money-bill-wave",
role: "admin", role: "admin",
}, },
{ {
name: "Pembelian", name: "pembelian",
to: "/purchases", to: "/purchases",
icon: "money-bill-wave", icon: "money-bill-wave",
role: "admin", role: "admin",
}, },
{ {
name: "Kategori", name: "kategori",
to: "/categories", to: "/categories",
icon: "list", icon: "list",
role: "admin", role: "admin",
}, },
{ {
name: "Produk", name: "produk",
to: "/products", to: "/products",
icon: "list", icon: "list",
role: "admin", role: "admin",

@ -0,0 +1,31 @@
import axios from "axios"
import useSWR from 'swr'
import "../axiosSetup"
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()
const { data, error } = useSWR([
`/products?startDate=${formatDate(startDate)}&endDate=${formatDate(endDate)}`, user.accessToken
], fetcher)
return [
data,
error,
]
}
export function useProduct(id) {
}

@ -1,16 +1,47 @@
import axios from 'axios' import axios from 'axios'
import { API_URL } from './config' import { API_URL } from './config'
const id = x => x
axios.defaults.baseURL = API_URL axios.defaults.baseURL = API_URL
axios.interceptors.response.use(id, error => { const axiosApiInstance = axios.create();
const refreshAccessToken = (refreshToken) => {
return axios({
method: "PUT",
url: '/authentications',
data: { refreshToken }
}).then(res => res.data)
}
axios.interceptors.response.use(
(response) => {
return response
},
async (error) => {
const { status, data: { message } } = error.response const { status, data: { message } } = error.response
if (status === 401 && message === 'Unauthenticated.') { if (status === 401 && message === 'Unauthenticated.') {
window.localStorage.clear() window.localStorage.clear()
window.location.reload() window.location.reload()
return return
} }
// if expired access token lets refresh token
if(status === 401 && message === "Token maximum age exceeded") {
const originalRequest = error.config;
originalRequest._retry = true;
const user = JSON.parse(window.localStorage.getItem('KASIRAJA_USER'))
const res = await refreshAccessToken(user.refreshToken);
window.localStorage.setItem(
'KASIRAJA_USER',
JSON.stringify({
...user,
accessToken: res.data.accessToken
})
)
axios.defaults.headers.common['Authorization'] = `Bearer ${res.data.accessToken}`;
return axiosApiInstance(originalRequest);
}
if (status === 403) { if (status === 403) {
window.alert('Anda tidak mempunyai akses untuk aksi ini') window.alert('Anda tidak mempunyai akses untuk aksi ini')
} }

@ -9,12 +9,24 @@ import DatePicker from "react-datepicker"
import { useState } from "react" import { useState } from "react"
import { formatDate } from "../../utils" import { formatDate } from "../../utils"
export default function DatePickerFilter() { export function useDatePickerFilter() {
const date = new Date() const date = new Date()
const [startDate, setStartDate] = useState(date) const [startDate, setStartDate] = useState(new Date())
const [endDate, setEndDate] = useState(new Date(date.setTime(date.getTime() + (7 * 24 * 60 * 60 * 1000)))) const [endDate, setEndDate] = useState(new Date(date.setTime(date.getTime() + (7 * 24 * 60 * 60 * 1000))))
return [
startDate,
endDate,
{
setStartDate,
setEndDate
}
]
}
export function DatePickerFilter({ startDate, endDate, setter : { setStartDate, setEndDate } }) {
const [startDateOpen, setStartDateOpen] = useState(false) const [startDateOpen, setStartDateOpen] = useState(false)
const [endDateOpen, setEndDateOpen] = useState(false) const [endDateOpen, setEndDateOpen] = useState(false)
@ -31,7 +43,10 @@ export default function DatePickerFilter() {
bg="gray.200" bg="gray.200"
readOnly={true} readOnly={true}
focusBorderColor="red.500" focusBorderColor="red.500"
onClick={() => setStartDateOpen(!startDateOpen)} onClick={() => {
setStartDateOpen(!startDateOpen)
setEndDateOpen(false)
}}
/> />
<InputRightElement children={<FontAwesomeIcon icon="calendar-alt" />} /> <InputRightElement children={<FontAwesomeIcon icon="calendar-alt" />} />
</InputGroup> </InputGroup>
@ -64,7 +79,10 @@ export default function DatePickerFilter() {
bg="gray.200" bg="gray.200"
readOnly={true} readOnly={true}
focusBorderColor="red.500" focusBorderColor="red.500"
onClick={() => setEndDateOpen(!endDateOpen)} onClick={() => {
setEndDateOpen(!endDateOpen)
setStartDateOpen(false)
}}
/> />
<InputRightElement children={<FontAwesomeIcon icon="calendar-alt" />} /> <InputRightElement children={<FontAwesomeIcon icon="calendar-alt" />} />
</InputGroup> </InputGroup>

@ -1,5 +1,10 @@
import { Flex } from '@chakra-ui/react'
import { CircularProgress } from "@chakra-ui/progress" import { CircularProgress } from "@chakra-ui/progress"
export default function Loading() { export default function Loading() {
return <CircularProgress isIndeterminate color="red.300" /> return (
<Flex justifyContent="center" alignItems="center" mt="24" mb="24">
<CircularProgress isIndeterminate color="red.500" />
</Flex>
)
} }

@ -33,7 +33,7 @@ const Header = ({ showSidebarButton = true, onShowSidebar, onLogout, user }) =>
</MenuButton> </MenuButton>
<MenuList> <MenuList>
<MenuItem color="blackAlpha.900">{user?.name}</MenuItem> <MenuItem color="blackAlpha.900">{user?.name}</MenuItem>
<MenuItem color="blackAlpha.900" onClick={e => onLogout(e)}>Logout</MenuItem> <MenuItem color="blackAlpha.900" onClick={e => onLogout(e)}>logout</MenuItem>
</MenuList> </MenuList>
</Menu> </Menu>

@ -0,0 +1,30 @@
import React from 'react'
import AppCrash from '../views/errors/500';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service
// logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <AppCrash/>;
}
return this.props.children;
}
}
export default ErrorBoundary

@ -1,3 +1,7 @@
export const formatDate = (date) => { export const formatDate = (date) => {
return date.toLocaleDateString() return date.toLocaleDateString()
} }
export function formatIDR(amount) {
const idFormatter = new Intl.NumberFormat('id-ID')
return idFormatter.format(amount)
}

@ -53,12 +53,12 @@ export default function Login(props) {
<Box style={{minHeight: "100vh"}} bg="gray.200" alignItems="center"> <Box style={{minHeight: "100vh"}} bg="gray.200" alignItems="center">
<Flex py={{ base: 1, lg: 22 }} px={{ base: 1, lg: 12, xl: 52 }} flexFlow={{base: "column", lg: "row" }} justifyContent="center" style={{ minHeight: "100vh", placeItems: "center", gap: "1rem", alignItems: "center"}}> <Flex py={{ base: 1, lg: 22 }} px={{ base: 1, lg: 12, xl: 52 }} flexFlow={{base: "column", lg: "row" }} justifyContent="center" style={{ minHeight: "100vh", placeItems: "center", gap: "1rem", alignItems: "center"}}>
<Box maxW={{base: "80", lg: "container.lg"}} display={{base: "none", lg: "block"}}> <Box maxW={{base: "80", lg: "container.lg"}} display={{base: "none", lg: "block"}}>
<Heading pb="7">Hai, kasirAja</Heading> <Heading pb="7">hai, kasirAja</Heading>
<Text> <Text>
kasirAja sebuah sistem POS simple, mudah, cepat, dan modern kasirAja sebuah sistem POS simple, mudah, cepat, dan modern
</Text> </Text>
<Text> <Text>
Sistem penjualan dan pembelian yang simple dengan pengelolan produk multi user. modern dengan dibangun diatas rest api dengan menggunakan nodejs, dapat diakses melalui web maupun perangkat mobile dengan aplikasi yang tersedia dan support dengan PWA. sistem penjualan dan pembelian yang simple dengan pengelolan produk multi user. modern dengan dibangun diatas rest api dengan menggunakan nodejs, dapat diakses melalui web maupun perangkat mobile dengan aplikasi yang tersedia dan support dengan PWA.
</Text> </Text>
</Box> </Box>
<Box flexShrink="0" shadow="lg" p="8" maxW="96" w="full" bg="white" rounded="lg"> <Box flexShrink="0" shadow="lg" p="8" maxW="96" w="full" bg="white" rounded="lg">
@ -70,7 +70,7 @@ export default function Login(props) {
</Alert> </Alert>
)} )}
<FormControl id="email" pb="2"> <FormControl id="email" pb="2">
<FormLabel mb="1">Email</FormLabel> <FormLabel mb="1">email</FormLabel>
<Input <Input
focusBorderColor="red.500" focusBorderColor="red.500"
type="email" type="email"
@ -82,7 +82,7 @@ export default function Login(props) {
/> />
</FormControl> </FormControl>
<FormControl id="password" pb="4"> <FormControl id="password" pb="4">
<FormLabel mb="1">Password</FormLabel> <FormLabel mb="1">password</FormLabel>
<Input <Input
focusBorderColor="red.500" focusBorderColor="red.500"
type="password" type="password"
@ -93,7 +93,7 @@ export default function Login(props) {
</FormControl> </FormControl>
<Box mt={5} mb="1" ml="1" fontSize="sm"> <Box mt={5} mb="1" ml="1" fontSize="sm">
<Link to="/register"> <Link to="/register">
Ingin mencoba, daftar ? ingin mencoba, daftar ?
</Link> </Link>
</Box> </Box>
<Button <Button
@ -103,7 +103,7 @@ export default function Login(props) {
disabled={submit} disabled={submit}
onClick={(e) => handleSubmit(e)} onClick={(e) => handleSubmit(e)}
> >
Login login
</Button> </Button>
</Box> </Box>
</Box> </Box>

@ -67,12 +67,12 @@ export default function RegisterPage(props) {
<Box style={{minHeight: "100vh"}} bg="gray.200" alignItems="center"> <Box style={{minHeight: "100vh"}} bg="gray.200" alignItems="center">
<Flex py={{ base: 1, lg: 22 }} px={{ base: 1, lg: 12, xl: 52 }} flexFlow={{base: "column", lg: "row" }} justifyContent="center" style={{ minHeight: "100vh", placeItems: "center", gap: "1rem", alignItems: "center"}}> <Flex py={{ base: 1, lg: 22 }} px={{ base: 1, lg: 12, xl: 52 }} flexFlow={{base: "column", lg: "row" }} justifyContent="center" style={{ minHeight: "100vh", placeItems: "center", gap: "1rem", alignItems: "center"}}>
<Box maxW={{base: "80", lg: "container.lg"}} display={{base: "none", lg: "block"}}> <Box maxW={{base: "80", lg: "container.lg"}} display={{base: "none", lg: "block"}}>
<Heading pb="7">Hai, kasirAja</Heading> <Heading pb="7">hai, kasirAja</Heading>
<Text> <Text>
kasirAja sebuah sistem POS simple, mudah, cepat, dan modern kasirAja sebuah sistem POS simple, mudah, cepat, dan modern
</Text> </Text>
<Text> <Text>
Sistem penjualan dan pembelian yang simple dengan pengelolan produk multi user. modern dengan dibangun diatas rest api dengan menggunakan nodejs, dapat diakses melalui web maupun perangkat mobile dengan aplikasi yang tersedia dan support dengan PWA. sistem penjualan dan pembelian yang simple dengan pengelolan produk multi user. modern dengan dibangun diatas rest api dengan menggunakan nodejs, dapat diakses melalui web maupun perangkat mobile dengan aplikasi yang tersedia dan support dengan PWA.
</Text> </Text>
</Box> </Box>
<Box flexShrink="0" shadow="lg" p="8" maxW="96" w="full" bg="white" rounded="lg"> <Box flexShrink="0" shadow="lg" p="8" maxW="96" w="full" bg="white" rounded="lg">
@ -84,7 +84,7 @@ export default function RegisterPage(props) {
</Alert> </Alert>
)} )}
<FormControl id="name" pb="2"> <FormControl id="name" pb="2">
<FormLabel mb="1">Nama Toko</FormLabel> <FormLabel mb="1">nama toko</FormLabel>
<Input <Input
focusBorderColor="red.500" focusBorderColor="red.500"
type="text" type="text"
@ -94,7 +94,7 @@ export default function RegisterPage(props) {
/> />
</FormControl> </FormControl>
<FormControl id="email" pb="2"> <FormControl id="email" pb="2">
<FormLabel mb="1">Email</FormLabel> <FormLabel mb="1">email</FormLabel>
<Input <Input
focusBorderColor="red.500" focusBorderColor="red.500"
type="email" type="email"
@ -104,7 +104,7 @@ export default function RegisterPage(props) {
/> />
</FormControl> </FormControl>
<FormControl id="password" pb="4"> <FormControl id="password" pb="4">
<FormLabel mb="1">Password</FormLabel> <FormLabel mb="1">password</FormLabel>
<InputGroup size="md"> <InputGroup size="md">
<Input <Input
pr="4.5rem" pr="4.5rem"
@ -124,7 +124,7 @@ export default function RegisterPage(props) {
</FormControl> </FormControl>
<Box mt={5} mb="1" ml="1" fontSize="sm"> <Box mt={5} mb="1" ml="1" fontSize="sm">
<Link to="/login"> <Link to="/login">
Sudah punya akun, login ? sudah punya akun, login ?
</Link> </Link>
</Box> </Box>
<Button <Button
@ -134,7 +134,7 @@ export default function RegisterPage(props) {
disabled={submit} disabled={submit}
onClick={e => handleSubmit(e)} onClick={e => handleSubmit(e)}
> >
Daftar daftar
</Button> </Button>
</Box> </Box>
</Box> </Box>

@ -9,7 +9,7 @@ export default function Dashboard() {
<Flex flexShrink="revert" direction="row" justifyContent="flex-start"> <Flex flexShrink="revert" direction="row" justifyContent="flex-start">
<Card> <Card>
<Stat> <Stat>
<StatLabel>Penjualan</StatLabel> <StatLabel>penjualan</StatLabel>
<StatNumber>345.670</StatNumber> <StatNumber>345.670</StatNumber>
<StatHelpText> <StatHelpText>
<StatArrow type="increase" /> <StatArrow type="increase" />
@ -19,7 +19,7 @@ export default function Dashboard() {
</Card> </Card>
<Card> <Card>
<Stat> <Stat>
<StatLabel>Pembelian</StatLabel> <StatLabel>pembelian</StatLabel>
<StatNumber>145.670</StatNumber> <StatNumber>145.670</StatNumber>
<StatHelpText> <StatHelpText>
<StatArrow type="decrease" /> <StatArrow type="decrease" />

@ -0,0 +1,11 @@
import { Container, Center } from '@chakra-ui/react'
export default function AppCrash(){
return (
<Container minW="full" minH="full">
<Center bg="red.600" minH="40rem" m="10" color="white" fontSize="2.5rem">
be patient, app crash / down / in maintace
</Center>
</Container>
)
}

@ -1,44 +1,72 @@
import { import {
Button, Button,
Alert,
AlertIcon,
Box,
Table, Table,
Thead, Thead,
Tr, Tr,
Td, Td,
Th, Th,
Tbody Tbody,
Heading
} from "@chakra-ui/react" } from "@chakra-ui/react"
import { useProducts } from "../../api"
import Card from "../../components/Common/Card" import Card from "../../components/Common/Card"
import DatePickerFilter from "../../components/Common/DatePickerFilter" import Loading from "../../components/Common/Loading"
import { DatePickerFilter, useDatePickerFilter } from "../../components/Common/DatePickerFilter"
import { formatIDR } from "../../utils"
export default function List({ history }) {
const [ startDate, endDate, setter ] = useDatePickerFilter()
const [ data, error ] = useProducts({startDate, endDate})
const handleItemClick = (id) => {
history.push(`/products/${id}`)
}
if(error) {
return (
<Alert status="error">
<AlertIcon />
{error.message}
</Alert>
)
}
export default function List() {
const arr = [1,2,3,4,5,6,7,8,9,10,11,12]
return ( return (
<>
<Box p="3" m="2" bg="white" rounded="lg">
<Heading size="md">dashboard / produk</Heading>
</Box>
<Card> <Card>
{/* Tombol Create */} <Button size="md">tambah</Button>
<Button size="md">Tambah</Button> <DatePickerFilter startDate={startDate} endDate={endDate} setter={setter} />
{/* Filter Tanggal */} {data ? (
<DatePickerFilter/> <Table variant="simple" mt="2">
{/* daftar products */}
<Table variant="simple" mt="2" colorScheme="whatsapp">
<Thead> <Thead>
<Tr> <Tr>
<Th>To convert</Th> <Th>nama</Th>
<Th>into</Th> <Th isNumeric>harga beli</Th>
<Th isNumeric>multiply by</Th> <Th isNumeric>harga jual</Th>
<Th>deskripsi</Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ {data.products.map((product) => (
arr.map(() => ( <Tr onClick={() => handleItemClick(product.id)} key={product.id}>
<Tr onClick={() => {alert('Hello')}}> <Td>{ product.name }</Td>
<Td>inches</Td> <Td isNumeric>{ formatIDR(product.cost) }</Td>
<Td>millimetres (mm)</Td> <Td isNumeric>{ formatIDR(product.price) }</Td>
<Td isNumeric>25.4</Td> <Td>{ product.description }</Td>
</Tr> </Tr>
)) ))}
}
</Tbody> </Tbody>
</Table> </Table>
) : (
<Loading/>
)}
</Card> </Card>
</>
) )
} }
Loading…
Cancel
Save