diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 4dcb439..7fb53a1 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -16,5 +16,6 @@ module.exports = {
'warn',
{ allowConstantExport: true },
],
+ 'react/prop-types': 'off',
},
-}
+};
diff --git a/index.html b/index.html
index 0c589ec..5fbf71e 100644
--- a/index.html
+++ b/index.html
@@ -1,10 +1,10 @@
-
+
- Vite + React
+ BDC Shoe Dashboard
diff --git a/package-lock.json b/package-lock.json
index b23a001..9540ca3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"@mantine/core": "^6.0.18",
"@mantine/form": "^6.0.18",
"@mantine/hooks": "^6.0.18",
+ "@tabler/icons-react": "^2.30.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.14.2"
@@ -1286,6 +1287,31 @@
"node": ">=14"
}
},
+ "node_modules/@tabler/icons": {
+ "version": "2.30.0",
+ "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-2.30.0.tgz",
+ "integrity": "sha512-tvtmkI4ALjKThVVORh++sB9JnkFY7eGInKxNy+Df7WVQiF7T85tlvGADzlgX4Ic+CK5MIUzZ0jhOlQ/RRlgXpg==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/codecalm"
+ }
+ },
+ "node_modules/@tabler/icons-react": {
+ "version": "2.30.0",
+ "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-2.30.0.tgz",
+ "integrity": "sha512-aYggXusHW133L4KujJkVf4GIIrjg7tIRHgNf/n37mnoHqMjwNP+PjmVdrBM1Z8Ywx9PKFRlrwM0eUMDcG+I4HA==",
+ "dependencies": {
+ "@tabler/icons": "2.30.0",
+ "prop-types": "^15.7.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/codecalm"
+ },
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@@ -3093,7 +3119,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -3366,7 +3391,6 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
- "dev": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
diff --git a/package.json b/package.json
index 78ed3ed..05316bc 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"@mantine/core": "^6.0.18",
"@mantine/form": "^6.0.18",
"@mantine/hooks": "^6.0.18",
+ "@tabler/icons-react": "^2.30.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.14.2"
diff --git a/src/assets/images/shoe-example.jpg b/src/assets/images/shoe-example.jpg
new file mode 100644
index 0000000..679b12b
Binary files /dev/null and b/src/assets/images/shoe-example.jpg differ
diff --git a/src/components/header.jsx b/src/components/header.jsx
new file mode 100644
index 0000000..3dc6407
--- /dev/null
+++ b/src/components/header.jsx
@@ -0,0 +1,15 @@
+import { Header, Title, MediaQuery, Flex } from '@mantine/core';
+import { IconMenu2 } from '@tabler/icons-react';
+
+export function HeaderMain({ onToggle }) {
+ return (
+
+
+
+ onToggle()} />
+
+ Dasboard
+
+
+ );
+}
diff --git a/src/components/navbar/index.jsx b/src/components/navbar/index.jsx
new file mode 100644
index 0000000..784a62e
--- /dev/null
+++ b/src/components/navbar/index.jsx
@@ -0,0 +1,77 @@
+import { NavLink } from 'react-router-dom';
+import { Navbar, Group, Title, Flex, MediaQuery } from '@mantine/core';
+import {
+ IconDashboard,
+ IconShoe,
+ IconCategory,
+ IconLogout,
+ IconX,
+} from '@tabler/icons-react';
+
+import { useStyles } from './style';
+
+const links = [
+ { link: '/', label: 'Dashboard', icon: IconDashboard },
+ { link: '/shoe', label: 'Shoes', icon: IconShoe },
+ { link: '/category', label: 'Category', icon: IconCategory },
+];
+
+export default function NavbarMain({ status, onToggle }) {
+ const { classes, cx } = useStyles();
+
+ return (
+
+
+
+
+
+
+ BDC Shoe
+
+
+
+ onToggle()} />
+
+
+
+
+ {links.map((item) => (
+
+ cx(classes.link, {
+ [classes.linkActive]: isActive,
+ })
+ }
+ to={item.link}
+ key={item.label}
+ >
+
+ {item.label}
+
+ ))}
+
+
+
+ event.preventDefault()}
+ >
+
+ Logout
+
+
+
+ );
+}
diff --git a/src/components/navbar/style.js b/src/components/navbar/style.js
new file mode 100644
index 0000000..3b33481
--- /dev/null
+++ b/src/components/navbar/style.js
@@ -0,0 +1,57 @@
+import { createStyles, getStylesRef, rem } from '@mantine/core';
+
+export const useStyles = createStyles((theme) => ({
+ header: {
+ paddingBottom: theme.spacing.md,
+ marginBottom: `calc(${theme.spacing.md} * 1.5)`,
+ borderBottom: `${rem(1)} solid ${theme.colors.gray[2]}`,
+ },
+
+ footer: {
+ paddingTop: theme.spacing.md,
+ marginTop: theme.spacing.md,
+ borderTop: `${rem(1)} solid ${theme.colors.gray[2]}`,
+ },
+
+ link: {
+ ...theme.fn.focusStyles(),
+ display: 'flex',
+ alignItems: 'center',
+ textDecoration: 'none',
+ fontSize: theme.fontSizes.sm,
+ color: theme.colors.gray[7],
+ padding: `${theme.spacing.xs} ${theme.spacing.sm}`,
+ borderRadius: theme.radius.sm,
+ fontWeight: 500,
+
+ '&:hover': {
+ backgroundColor: theme.colors.gray[0],
+ color: theme.black,
+
+ [`& .${getStylesRef('icon')}`]: {
+ color: theme.black,
+ },
+ },
+ },
+
+ linkIcon: {
+ ref: getStylesRef('icon'),
+ color: theme.colors.gray[6],
+ marginRight: theme.spacing.sm,
+ },
+
+ linkActive: {
+ '&, &:hover': {
+ backgroundColor: theme.fn.variant({
+ variant: 'light',
+ color: theme.primaryColor,
+ }).background,
+ color: theme.fn.variant({ variant: 'light', color: theme.primaryColor })
+ .color,
+ [`& .${getStylesRef('icon')}`]: {
+ color: theme.fn.variant({ variant: 'light', color: theme.primaryColor })
+ .color,
+ },
+ },
+ },
+}));
diff --git a/src/layouts/main.jsx b/src/layouts/main.jsx
index 8459bd3..e621710 100644
--- a/src/layouts/main.jsx
+++ b/src/layouts/main.jsx
@@ -1,5 +1,26 @@
-import { Outlet } from "react-router-dom";
+import { useState } from 'react';
+import { Outlet } from 'react-router-dom';
+import { AppShell, Container } from '@mantine/core';
+
+import NavbarMain from '../components/navbar';
+import { HeaderMain } from '../components/header';
export default function LayoutMain() {
- return ;
+ const [opened, setOpened] = useState(false);
+
+ return (
+ setOpened(!opened)} />
+ }
+ header={ setOpened(!opened)} />}
+ >
+
+
+
+
+ );
}
diff --git a/src/pages/category/create.jsx b/src/pages/category/create.jsx
new file mode 100644
index 0000000..034d171
--- /dev/null
+++ b/src/pages/category/create.jsx
@@ -0,0 +1,57 @@
+import { Form, Link, redirect } from 'react-router-dom';
+import { Button, Flex, Group, TextInput, Title } from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+
+export async function action({ request }) {
+ const formData = await request.formData();
+ const payload = Object.fromEntries(formData);
+
+ await fetch('http://localhost:3000/category', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(payload),
+ });
+
+ return redirect('/category');
+}
+
+export default function PageCategoryCreate() {
+ return (
+ <>
+
+
+ Add Category
+
+
+ }
+ >
+ Back
+
+
+
+
+ >
+ );
+}
diff --git a/src/pages/category/edit.jsx b/src/pages/category/edit.jsx
new file mode 100644
index 0000000..11a1e48
--- /dev/null
+++ b/src/pages/category/edit.jsx
@@ -0,0 +1,69 @@
+import { Link, useLoaderData, redirect, Form } from 'react-router-dom';
+import { Button, Flex, Group, TextInput, Title } from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+
+export async function loader({ params }) {
+ const response = await fetch(`http://localhost:3000/category/${params.id}`);
+ const category = await response.json();
+
+ return {
+ category,
+ };
+}
+
+export async function action({ request, params }) {
+ const formData = await request.formData();
+ const payload = Object.fromEntries(formData);
+
+ await fetch(`http://localhost:3000/category/${params.id}`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(payload),
+ });
+
+ return redirect('/category');
+}
+
+export default function PageCategoryEdit() {
+ const data = useLoaderData();
+
+ return (
+ <>
+
+
+ Edit Category
+
+
+ }
+ >
+ Back
+
+
+
+
+ >
+ );
+}
diff --git a/src/pages/category/list.jsx b/src/pages/category/list.jsx
new file mode 100644
index 0000000..b133a83
--- /dev/null
+++ b/src/pages/category/list.jsx
@@ -0,0 +1,76 @@
+import { Form, Link, useLoaderData, redirect } from 'react-router-dom';
+import { ActionIcon, Button, Flex, Table, Title } from '@mantine/core';
+import { IconPencil, IconPlus, IconTrash } from '@tabler/icons-react';
+
+export async function loader() {
+ const response = await fetch('http://localhost:3000/category');
+ const categories = await response.json();
+
+ return {
+ categories,
+ };
+}
+
+export async function action({ request }) {
+ const formData = await request.formData();
+ const id = formData.get('id');
+ await fetch(`http://localhost:3000/category/${id}`, {
+ method: 'DELETE',
+ });
+
+ return redirect('/category');
+}
+
+export default function PageCategoryList() {
+ const data = useLoaderData();
+
+ return (
+ <>
+
+
+ Category List
+
+
+ }>
+ Add
+
+
+
+
+
+
+ | Name |
+ Action |
+
+
+
+
+ {data.categories.map((category) => (
+
+ | {category.name} |
+
+
+
+
+
+
+
+
+ |
+
+ ))}
+
+
+ >
+ );
+}
diff --git a/src/pages/home.jsx b/src/pages/home.jsx
index f98b49e..b1b0202 100644
--- a/src/pages/home.jsx
+++ b/src/pages/home.jsx
@@ -1,3 +1,3 @@
-export default function Home() {
+export default function PageHome() {
return Home Page
;
}
diff --git a/src/pages/shoe/create.jsx b/src/pages/shoe/create.jsx
index 2b8c162..4974c88 100644
--- a/src/pages/shoe/create.jsx
+++ b/src/pages/shoe/create.jsx
@@ -1,7 +1,169 @@
-export default function ShoeCreate() {
+import {
+ Link,
+ redirect,
+ Form,
+ useLoaderData,
+ useNavigation,
+ useActionData,
+} from 'react-router-dom';
+import {
+ Button,
+ Flex,
+ Group,
+ NumberInput,
+ Radio,
+ Select,
+ TextInput,
+ Textarea,
+ Title,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+
+export async function loader() {
+ const response = await fetch('http://localhost:3000/category');
+ const payload = await response.json();
+
+ const shoes = payload.map((category) => ({
+ value: category.id,
+ label: category.name,
+ }));
+
+ return { shoes };
+}
+
+export async function action({ request }) {
+ const formData = await request.formData();
+ const payload = Object.fromEntries(formData);
+
+ const errors = {};
+
+ if (Number(formData.get('qty')) < 1) {
+ errors.qty = 'Qty should be more than zero';
+ }
+
+ if (Number(formData.get('price')) < 0) {
+ errors.price = 'Price should be start from zero';
+ }
+
+ if (Object.keys(errors).length > 0) {
+ return errors;
+ }
+
+ await fetch('http://localhost:3000/shoe', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(payload),
+ });
+
+ return redirect('/shoe');
+}
+
+export default function PageShoeCreate() {
+ const navigation = useNavigation();
+ const isSubmitting = navigation.state === 'submitting';
+
+ const data = useLoaderData();
+ const errors = useActionData();
+
return (
-
+ <>
+
+
+ Add Shoe
+
+
+ }
+ >
+ Back
+
+
+
+
+ >
);
}
diff --git a/src/pages/shoe/detail.jsx b/src/pages/shoe/detail.jsx
index bb1a68e..e74dc49 100644
--- a/src/pages/shoe/detail.jsx
+++ b/src/pages/shoe/detail.jsx
@@ -1,7 +1,78 @@
-export default function ShoeDetail() {
+import { Link, useLoaderData } from 'react-router-dom';
+import { Button, Flex, Title, Image, Text, Badge, Group } from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+
+import imgShoe from '../../assets/images/shoe-example.jpg';
+
+export async function loader({ params }) {
+ const response = await fetch(`http://localhost:3000/shoe/${params.id}`);
+ const json = await response.json();
+
+ return {
+ shoe: json,
+ };
+}
+
+export default function PageShoeDetail() {
+ const data = useLoaderData();
+
return (
-
+ <>
+
+
+ Detail Shoe
+
+
+ }
+ >
+ Back
+
+
+
+
+
+
+
+
+
+ {data.shoe.category.name}
+
+ {data.shoe.available ? (
+ Available
+ ) : (
+ Unavailable
+ )}
+
+
+ {data.shoe.name}
+
+
+ Quantity: {data.shoe.qty}
+
+
+
+ Rp {data.shoe.price}
+
+
+ {data.shoe.desc}
+
+
+
+
+
+
+
+ >
);
}
diff --git a/src/pages/shoe/edit.jsx b/src/pages/shoe/edit.jsx
index b2f78d5..dbe91a2 100644
--- a/src/pages/shoe/edit.jsx
+++ b/src/pages/shoe/edit.jsx
@@ -1,7 +1,184 @@
-export default function ShoeEdit() {
+import {
+ Link,
+ useLoaderData,
+ redirect,
+ useActionData,
+ Form,
+ useNavigation,
+} from 'react-router-dom';
+import {
+ Button,
+ Flex,
+ Group,
+ NumberInput,
+ Radio,
+ Select,
+ TextInput,
+ Textarea,
+ Title,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+
+export async function loader({ params }) {
+ const [shoeResponse, categoryResponse] = await Promise.all([
+ fetch(`http://localhost:3000/shoe/${params.id}`),
+ fetch('http://localhost:3000/category'),
+ ]);
+
+ const shoe = await shoeResponse.json();
+ const categories = await categoryResponse.json();
+
+ const categoryOptions = categories.map((category) => ({
+ value: category.id,
+ label: category.name,
+ }));
+
+ return {
+ shoe,
+ categoryOptions,
+ };
+}
+
+export async function action({ request, params }) {
+ const formData = await request.formData();
+ const payload = Object.fromEntries(formData);
+
+ const errors = {};
+
+ if (Number(formData.get('qty')) < 1) {
+ errors.qty = 'Qty should be more than zero';
+ }
+
+ if (Number(formData.get('price')) < 0) {
+ errors.price = 'Price should be start from zero';
+ }
+
+ if (Object.keys(errors).length > 0) {
+ return errors;
+ }
+
+ await fetch(`http://localhost:3000/shoe/${params.id}`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(payload),
+ });
+
+ return redirect('/shoe');
+}
+
+export default function PageShoeEdit() {
+ const data = useLoaderData();
+ const errors = useActionData();
+
+ const navigation = useNavigation();
+ const isSubmitting = navigation.state === 'submitting';
+
return (
-
+ <>
+
+
+ Edit Shoe
+
+
+ }
+ >
+ Back
+
+
+
+
+ >
);
}
diff --git a/src/pages/shoe/list.jsx b/src/pages/shoe/list.jsx
index 327ce59..d7d4d71 100644
--- a/src/pages/shoe/list.jsx
+++ b/src/pages/shoe/list.jsx
@@ -1,10 +1,139 @@
-import { Button } from "@mantine/core";
+import { Link, useLoaderData, redirect, Form } from 'react-router-dom';
+import {
+ ActionIcon,
+ Badge,
+ Button,
+ Flex,
+ Group,
+ Modal,
+ Table,
+ Title,
+} from '@mantine/core';
+import { IconEye, IconPencil, IconPlus, IconTrash } from '@tabler/icons-react';
+import { useDisclosure } from '@mantine/hooks';
+import { useState } from 'react';
+
+export async function loader() {
+ const response = await fetch('http://localhost:3000/shoe');
+ const shoes = await response.json();
+
+ return {
+ shoes,
+ };
+}
+
+export async function action({ request }) {
+ const formData = await request.formData();
+ const id = formData.get('id');
+ await fetch(`http://localhost:3000/shoe/${id}`, {
+ method: 'DELETE',
+ });
+
+ return redirect('/shoe');
+}
+
+export default function PageShoeList() {
+ const data = useLoaderData();
+ const [opened, { open, close }] = useDisclosure(false);
+ const [deletedId, setDeletedId] = useState();
-export default function ShoeList() {
return (
-
-
List Page
-
-
+ <>
+
+
+ Shoe List
+
+
+ }>
+ Add
+
+
+
+
+ Are you sure to delete data?
+
+
+
+
+
+
+
+
+
+
+
+ | Name |
+ Brand |
+ Quantity |
+ Availability |
+ Action |
+
+
+
+
+ {data.shoes.map((shoe) => (
+
+ | {shoe.name} |
+ {shoe.merk} |
+
+ {shoe.qty}
+ |
+
+ {shoe.available ? (
+ Yes
+ ) : (
+ No
+ )}
+ |
+
+
+
+
+
+
+
+
+
+
+ {
+ open();
+ setDeletedId(shoe.id);
+ }}
+ >
+
+
+
+ |
+
+ ))}
+
+
+ >
);
}
diff --git a/src/route.jsx b/src/route.jsx
index 004459c..861a850 100644
--- a/src/route.jsx
+++ b/src/route.jsx
@@ -1,22 +1,102 @@
-import { createBrowserRouter } from "react-router-dom";
+import { Outlet, createBrowserRouter } from 'react-router-dom';
-import LayoutMain from "./layouts/main";
-import Home from "./pages/home";
-import ShoeList from "./pages/shoe/list";
-import ShoeDetail from "./pages/shoe/detail";
-import ShoeCreate from "./pages/shoe/create";
-import ShoeEdit from "./pages/shoe/edit";
+import LayoutMain from './layouts/main';
+import PageHome from './pages/home';
+
+import PageShoeList, {
+ loader as loaderShoeList,
+ action as actionShoeList,
+} from './pages/shoe/list';
+
+import PageShoeDetail, {
+ loader as loaderShoeDetail,
+} from './pages/shoe/detail';
+
+import PageShoeCreate, {
+ loader as loaderShoeCreate,
+ action as actionShoeCreate,
+} from './pages/shoe/create';
+
+import PageShoeEdit, {
+ loader as loaderShoeEdit,
+ action as actionShoeEdit,
+} from './pages/shoe/edit';
+
+import PageCategoryList, {
+ action as actionCategoryList,
+ loader as loaderCategoryList,
+} from './pages/category/list';
+
+import PageCategoryCreate, {
+ action as actionCategoryCreate,
+} from './pages/category/create';
+
+import PageCategoryEdit, {
+ loader as loaderCategoryEdit,
+ action as actionCategoryEdit,
+} from './pages/category/edit';
const router = createBrowserRouter([
{
- path: "/",
+ path: '/',
element: ,
children: [
- { index: true, element: },
- { path: "/shoe", element: },
- { path: "/shoe/:id/detail", element: },
- { path: "/shoe/create", element: },
- { path: "/shoe/:id/edit", element: },
+ {
+ index: true,
+ element: ,
+ },
+ {
+ path: '/shoe',
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ loader: loaderShoeList,
+ action: actionShoeList,
+ },
+ {
+ path: ':id/detail',
+ element: ,
+ loader: loaderShoeDetail,
+ },
+ {
+ path: 'create',
+ element: ,
+ action: actionShoeCreate,
+ loader: loaderShoeCreate,
+ },
+ {
+ path: ':id/edit',
+ element: ,
+ loader: loaderShoeEdit,
+ action: actionShoeEdit,
+ },
+ ],
+ },
+ {
+ path: '/category',
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ action: actionCategoryList,
+ loader: loaderCategoryList,
+ },
+ {
+ path: 'create',
+ element: ,
+ action: actionCategoryCreate,
+ },
+ {
+ path: ':id/edit',
+ element: ,
+ loader: loaderCategoryEdit,
+ action: actionCategoryEdit,
+ },
+ ],
+ },
],
},
]);