diff --git a/.babelrc b/.babelrc
index 88d5636caa8cf001e28beda10214d2590af8e0b0..733881f5a194e81daa78232ede104fc89e6ed842 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,3 +1,18 @@
 {
-  "presets": ["@babel/preset-react", "@babel/preset-env"]
+  "presets": ["@babel/preset-react", "@babel/preset-env",
+  [
+    "@emotion/babel-preset-css-prop",
+    {
+      "sourceMap": false
+    }
+  ]
+  ],
+  "plugins": [
+    [
+      "transform-inline-environment-variables"
+    ],
+    [
+      "emotion"
+    ]
+  ]
 }
diff --git a/.eslintrc.json b/.eslintrc.json
index ba6ab965317eb3fb546b0b96c73ac687dc39a9b8..eef303d6e6bdd98a064fff928795332719be6ea8 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -10,10 +10,13 @@
   "rules": {
     "react/prop-types": 0,
     "react-hooks/rules-of-hooks": "error",
-    "no-console": "warn"
+    "no-console": "warn",
+    "emotion/no-vanilla": "error",
+    "emotion/import-from-emotion": "error",
+    "emotion/styled-import": "error"
   },
   "parser": "babel-eslint",
-  "plugins": ["react", "import", "jsx-a11y", "react-hooks"],
+  "plugins": ["react", "import", "jsx-a11y", "react-hooks", "emotion"],
   "parserOptions": {
     "ecmaVersion": 2019,
     "sourceType": "module",
diff --git a/.gitignore b/.gitignore
index 0d551e1e73e854b013491aff78c98b559b4beb60..a09068287042be8c9a3236d803d49d0319fa396b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@
 .pnp.js
 /.cache
 /dist
+.env
 
 # testing
 /coverage
@@ -25,4 +26,5 @@ test-report.xml
 npm-debug.log*
 yarn-debug.log*
 yarn-error.log*
-package-lock.json
\ No newline at end of file
+package-lock.json
+/.vscode
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a5eeb839d19c3ce40e799b42ded08366481fa156..4d6afd1d03ad7665eb9800f0c97c8d431048aae2 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -39,10 +39,11 @@ staging:
   variables:
     NETLIFY_AUTH_TOKEN: $STAGING_NETLIFY_AUTH_TOKEN
     NETLIFY_SITE_ID: $STAGING_NETLIFY_SITE_ID
+    REACT_APP_BASE_URL: $STAGING_REACT_APP_BASE_URL
   script:
     - npm install
     - CI=false npm run build
-    - npm install netlify-cli -g
+    - npm install netlify-cli -g --unsafe-perm
     - netlify deploy --dir dist --prod
   only:
     - staging
@@ -54,6 +55,7 @@ production:
     AWS_ACCESS_KEY_ID: $PRODUCTION_AWS_ACCESS_KEY_ID
     AWS_SECRET_ACCESS_KEY: $PRODUCTION_AWS_SECRET_ACCESS_KEY
     AWS_STORAGE_BUCKET_NAME: $PRODUCTION_AWS_STORAGE_BUCKET_NAME
+    REACT_APP_BASE_URL: $PRODUCTION_REACT_APP_BASE_URL
   script:
     - npm install
     - CI=false npm run build
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1 @@
+{}
diff --git a/README.md b/README.md
index f7b548693144276ef5588674689a2ac8cf57069b..4a5fabb9b877030f5cc01b40a53da62850e0f5fe 100644
--- a/README.md
+++ b/README.md
@@ -2,3 +2,38 @@
 
 [![pipeline status](https://gitlab.cs.ui.ac.id/ppl-fasilkom-ui/2020/ppl-c/diskominfo-depok-tpu-online/post-rpl-web/badges/master/pipeline.svg)](https://gitlab.cs.ui.ac.id/ppl-fasilkom-ui/2020/ppl-c/diskominfo-depok-tpu-online/post-rpl-web/commits/master)
 [![coverage report](https://gitlab.cs.ui.ac.id/ppl-fasilkom-ui/2020/ppl-c/diskominfo-depok-tpu-online/post-rpl-web/badges/master/coverage.svg)](https://gitlab.cs.ui.ac.id/ppl-fasilkom-ui/2020/ppl-c/diskominfo-depok-tpu-online/post-rpl-web/commits/master)
+
+A local e-commerce application created for ease of buying and selling transactions.
+
+## Table of Contents
+- [Install](#install)
+- [Running Development Mode](#running-development-mode)
+- [References](#references)
+
+## Install
+The Admin Website's frontend uses Node.js and is developed using React. You need to install the required dependencies prior to building and contributing to the project.
+
+- [Node.js](https://nodejs.org/en/download/releases/) and npm package manager
+
+Verify that Node.js has been successfully installed. Make sure the interpreter can be invoked from the shell. For example:
+```
+npm --version
+```
+
+Now install the packages required for Node.js:
+```
+npm install
+```
+
+## Running Development Mode
+To serve the frontend:
+```
+npm run start
+```
+
+You can see the app running by going to localhost:1234 via your favourite web browser.
+
+## References
+- https://docs.gitlab.com/ee/user/markdown.html#wiki---direct-page-link
+- https://nodejs.org/en/download/package-manager/
+
diff --git a/_redirects b/_redirects
new file mode 100644
index 0000000000000000000000000000000000000000..7797f7c6a7356b0d451d11a49925df854c22e978
--- /dev/null
+++ b/_redirects
@@ -0,0 +1 @@
+/* /index.html 200
diff --git a/package.json b/package.json
index ce9b1bd9cc6ca5f434aa92e7a7260b6eabff0b6e..27922000fd05ea2ec701d04c334e8b08febb09ad 100644
--- a/package.json
+++ b/package.json
@@ -3,14 +3,28 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
-    "@reach/router": "^1.3.1",
-    "react": "^16.12.0",
-    "react-dom": "^16.12.0",
-    "react-scripts": "3.3.1"
+    "@emotion/babel-preset-css-prop": "^10.0.27",
+    "@emotion/core": "^10.0.28",
+    "@emotion/styled": "^10.0.27",
+    "@material-ui/core": "^4.9.11",
+    "@material-ui/icons": "^4.9.1",
+    "@reach/router": "^1.3.3",
+    "bootstrap": "^4.4.1",
+    "jquery": "^3.5.0",
+    "moment": "^2.24.0",
+    "moment-timezone": "^0.5.28",
+    "popper.js": "^1.16.1",
+    "react": "^16.13.1",
+    "react-bootstrap": "^1.0.1",
+    "react-dom": "^16.13.1",
+    "react-hook-form": "^5.6.0",
+    "react-moment": "^0.9.7",
+    "react-number-format": "^4.4.1",
+    "react-promise-tracker": "^2.1.0"
   },
   "scripts": {
     "start": "parcel public/index.html",
-    "build": "parcel build public/index.html",
+    "build": "parcel build public/index.html --experimental-scope-hoisting",
     "test": "jest",
     "test:coverage": "jest --coverage",
     "lint": "eslint \"src/**/*.{js,jsx}\" --quiet",
@@ -21,9 +35,9 @@
   },
   "browserslist": {
     "production": [
-      ">0.2%",
-      "not dead",
-      "not op_mini all"
+      "last 3 chrome version",
+      "last 3 firefox version",
+      "last 3 safari version"
     ],
     "development": [
       "last 1 chrome version",
@@ -32,24 +46,40 @@
     ]
   },
   "devDependencies": {
-    "@babel/core": "^7.8.4",
-    "@babel/preset-env": "^7.8.4",
-    "@babel/preset-react": "^7.8.3",
-    "@testing-library/react": "^9.4.0",
-    "@types/jest": "^25.1.2",
-    "babel-eslint": "^10.0.3",
+    "@babel/core": "^7.9.0",
+    "@babel/preset-env": "^7.9.5",
+    "@babel/preset-react": "^7.9.4",
+    "@testing-library/dom": "^7.2.2",
+    "@testing-library/react": "^10.0.3",
+    "@types/jest": "^25.2.1",
+    "babel-eslint": "^10.1.0",
+    "babel-plugin-emotion": "^10.0.33",
+    "babel-plugin-transform-inline-environment-variables": "^0.4.3",
+    "enzyme": "^3.11.0",
+    "enzyme-adapter-react-16": "^1.15.2",
     "eslint": "^6.8.0",
-    "eslint-config-prettier": "^6.10.0",
-    "eslint-plugin-import": "^2.20.1",
+    "eslint-config-prettier": "^6.11.0",
+    "eslint-plugin-emotion": "^10.0.27",
+    "eslint-plugin-import": "^2.20.2",
     "eslint-plugin-jsx-a11y": "^6.2.3",
-    "eslint-plugin-react": "^7.18.3",
-    "eslint-plugin-react-hooks": "^2.3.0",
-    "jest": "^25.1.0",
+    "eslint-plugin-react": "^7.19.0",
+    "eslint-plugin-react-hooks": "^2.5.1",
+    "jest": "^25.4.0",
+    "jest-environment-enzyme": "^7.1.2",
+    "jest-enzyme": "^7.1.2",
+    "jest-fetch-mock": "^3.0.3",
     "jest-sonar-reporter": "^2.0.0",
+    "mutationobserver-shim": "^0.3.5",
     "parcel-bundler": "^1.12.4",
-    "prettier": "^1.19.1"
+    "prettier": "^2.0.5",
+    "regenerator-runtime": "^0.13.5"
   },
   "jest": {
+    "setupFilesAfterEnv": [
+      "jest-enzyme",
+      "<rootDir>/src/__test__/setup-jest.js"
+    ],
+    "testEnvironment": "enzyme",
     "moduleNameMapper": {
       "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
       "\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js"
diff --git a/public/index.html b/public/index.html
index c9919861e4383cd10ef97e10a28edab8cfdba0af..9303b31270f5841f1b09f241bdb1d7676b874f91 100644
--- a/public/index.html
+++ b/public/index.html
@@ -3,18 +3,13 @@
   <head>
     <meta charset="utf-8" />
     <link rel="icon" href="favicon.ico" />
-    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
     <meta name="theme-color" content="#000000" />
     <meta
       name="description"
       content="Web site created using create-react-app"
     />
     <link rel="apple-touch-icon" href="logo192.png" />
-    <!--
-      manifest.json provides metadata used when your web app is installed on a
-      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-    -->
-    <link rel="manifest" href="manifest.json" />
     <!--
       Notice the use of %PUBLIC_URL% in the tags above.
       It will be replaced with the URL of the `public` folder during the build.
@@ -26,7 +21,7 @@
     -->
     <title>React App</title>
   </head>
-  <body>
+  <body style="background-color: #F5F8F9">
     <noscript>You need to enable JavaScript to run this app.</noscript>
     <div id="root"></div>
     <!--
diff --git a/public/manifest.json b/public/manifest.json
deleted file mode 100644
index 080d6c77ac21bb2ef88a6992b2b73ad93daaca92..0000000000000000000000000000000000000000
--- a/public/manifest.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
-  "short_name": "React App",
-  "name": "Create React App Sample",
-  "icons": [
-    {
-      "src": "favicon.ico",
-      "sizes": "64x64 32x32 24x24 16x16",
-      "type": "image/x-icon"
-    },
-    {
-      "src": "logo192.png",
-      "type": "image/png",
-      "sizes": "192x192"
-    },
-    {
-      "src": "logo512.png",
-      "type": "image/png",
-      "sizes": "512x512"
-    }
-  ],
-  "start_url": ".",
-  "display": "standalone",
-  "theme_color": "#000000",
-  "background_color": "#ffffff"
-}
diff --git a/public/robots.txt b/public/robots.txt
deleted file mode 100644
index e9e57dc4d41b9b46e05112e9f45b7ea6ac0ba15e..0000000000000000000000000000000000000000
--- a/public/robots.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-# https://www.robotstxt.org/robotstxt.html
-User-agent: *
-Disallow:
diff --git a/sonar-project.properties b/sonar-project.properties
index e201ce2f9ed47169bb96a05e8d4e656116e8109b..9cc2b14329d57043aed8acf21b0e5d8f45ec2d76 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -5,3 +5,5 @@ sonar.testExecutionReportPaths=test-report.xml
 sonar.scm.provider=git
 sonar.sourceEncoding=UTF-8
 sonar.sources=src
+sonar.tests=src/__test__
+sonar.exclusions=**/*-jest.js
\ No newline at end of file
diff --git a/src/App.css b/src/App.css
deleted file mode 100644
index 74b5e053450a48a6bdb4d71aad648e7af821975c..0000000000000000000000000000000000000000
--- a/src/App.css
+++ /dev/null
@@ -1,38 +0,0 @@
-.App {
-  text-align: center;
-}
-
-.App-logo {
-  height: 40vmin;
-  pointer-events: none;
-}
-
-@media (prefers-reduced-motion: no-preference) {
-  .App-logo {
-    animation: App-logo-spin infinite 20s linear;
-  }
-}
-
-.App-header {
-  background-color: #282c34;
-  min-height: 100vh;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  font-size: calc(10px + 2vmin);
-  color: white;
-}
-
-.App-link {
-  color: #61dafb;
-}
-
-@keyframes App-logo-spin {
-  from {
-    transform: rotate(0deg);
-  }
-  to {
-    transform: rotate(360deg);
-  }
-}
diff --git a/src/App.js b/src/App.js
deleted file mode 100644
index 91ff38991c8bfbee985216ab3dbe0c12012e7811..0000000000000000000000000000000000000000
--- a/src/App.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from "react";
-import "./App.css";
-
-const App = () => {
-  return (
-    <div className="App">
-      <header className="App-header">
-        <p>Hello, World!</p>
-      </header>
-    </div>
-  );
-};
-
-export default App;
diff --git a/src/App.jsx b/src/App.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..a5a6c442bd67ae20873206b6c9704f9f8f17e0de
--- /dev/null
+++ b/src/App.jsx
@@ -0,0 +1,9 @@
+import React from "react";
+import ApplicationState from "./ApplicationState";
+import "bootstrap/dist/css/bootstrap.min.css";
+
+const App = () => {
+  return <ApplicationState />;
+};
+
+export default App;
diff --git a/src/App.test.js b/src/App.test.js
deleted file mode 100644
index d4bea6c4efcba2c9242046c1a8aa0a85c76d8b56..0000000000000000000000000000000000000000
--- a/src/App.test.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import React from "react";
-import { render } from "@testing-library/react";
-import App from "./App";
-
-test("renders hello world", () => {
-  const { getByText } = render(<App />);
-  const paragraphElement = getByText(/Hello, World!/i);
-  expect(paragraphElement.textContent).toStrictEqual("Hello, World!");
-});
diff --git a/src/ApplicationState.jsx b/src/ApplicationState.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..aaea2056fe7104eadfa23e30ad1303bf73cd9575
--- /dev/null
+++ b/src/ApplicationState.jsx
@@ -0,0 +1,37 @@
+import React, { useReducer } from "react";
+import AuthReducer, { initialState } from "./store/reducers/auth_reducer";
+import * as ACTIONS from "./store/actions/actions";
+import AuthContext from "./utils/contex";
+import Routes from "./routes";
+import Loader from "./component/Loader";
+
+const ApplicationState = () => {
+  const [stateAuthReducer, dispatchAuthReducer] = useReducer(
+    AuthReducer,
+    initialState()
+  );
+
+  const handleLogin = (profile) => {
+    dispatchAuthReducer(ACTIONS.login(profile));
+  };
+
+  const handleLogout = () => {
+    dispatchAuthReducer(ACTIONS.logout());
+  };
+
+  return (
+    <div>
+      <Loader />
+      <AuthContext.Provider
+        value={{
+          ...stateAuthReducer,
+          handleLogin,
+          handleLogout,
+        }}
+      >
+        <Routes />
+      </AuthContext.Provider>
+    </div>
+  );
+};
+export default ApplicationState;
diff --git a/src/__test__/App.test.js b/src/__test__/App.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..75cedc19ef81382f8b2cb0c1b2268b1a18ddca0d
--- /dev/null
+++ b/src/__test__/App.test.js
@@ -0,0 +1,11 @@
+import React from "react";
+import { cleanup } from "@testing-library/react";
+import App from "../App";
+import { shallow } from "enzyme";
+import ApplicationState from "../ApplicationState";
+
+afterEach(cleanup);
+test("renders Application State", () => {
+  const wrapper = shallow(<App />);
+  expect(wrapper.find(ApplicationState)).toHaveLength(1);
+});
diff --git a/src/__test__/DetailPengguna.test.js b/src/__test__/DetailPengguna.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..fe6bae921a659cf6c7a89fab42542cf6eeadd06a
--- /dev/null
+++ b/src/__test__/DetailPengguna.test.js
@@ -0,0 +1,149 @@
+import React from "react";
+import { cleanup, render } from "@testing-library/react";
+import DetailPengguna from "../page/pengguna/DetailPengguna";
+import AuthContext from "../utils/contex";
+import { waitFor } from "@testing-library/dom";
+
+beforeEach(() => {
+  fetch.resetMocks();
+});
+afterEach(cleanup);
+
+test("Test detail pengguna renders", async () => {
+  fetch
+    .once(
+      JSON.stringify({
+        count: 17,
+        next:
+          "https://industripilar-staging.herokuapp.com/transactions/?page=2",
+        previous: null,
+        results: [
+          {
+            id: "0c1db3b2-48f0-4604-83ca-e8f16e8550ab",
+            transaction_number: "H793P5ZK",
+            user: "d4b98bb5-8ba4-4a41-af10-93abcf53df58",
+            user_username: "whtestest",
+            user_full_name: "Michael Wiryadinata halim",
+            user_phone_number: "+628192090199",
+            shipping_address: "ada deh test",
+            shipping_neighborhood: "002",
+            shipping_hamlet: "002",
+            shipping_urban_village: "penggilingan",
+            shipping_sub_district: "Dummy Sub-District",
+            transaction_items: [
+              {
+                id: "37f0298c-2e8a-4ab0-a094-e9177ddc60c4",
+                product: "3d403cd3-e356-4c15-9a86-8843333e2778",
+                product_code: "5VK6TY",
+                product_name: "produk barang",
+                product_price: "50000.00",
+                quantity: 7,
+              },
+            ],
+            transaction_item_subtotal: "350000.00",
+            shipping_costs: "15000.00",
+            payment_method: "TRF",
+            readable_payment_method: "Transfer",
+            donation: "5000.00",
+            transaction_status: "002",
+            readable_transaction_status: "Waiting for seller confirmation",
+            proof_of_payment: null,
+            user_bank_account_name: "test",
+            user_bank_account_number: "1232131241321",
+            created_at: "2020-04-18T10:59:42.074386+07:00",
+            updated_at: "2020-04-18T11:00:18.150633+07:00",
+            subtotal: "370000.00",
+          },
+        ],
+      })
+    )
+    .once(
+      JSON.stringify({
+        id: "663392ac-1dd6-462b-9301-a19c1287cefd",
+        username: "dummyuser",
+        full_name: "Dummy User",
+        phone_number: "+6285212345678",
+        address: "Jl. Dummy No.1",
+        neighborhood: "000",
+        hamlet: "000",
+        urban_village: "Dummy Urban Village",
+        sub_district: "Dummy Sub-District",
+        profile_picture: null,
+      })
+    );
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <DetailPengguna />
+    </AuthContext.Provider>
+  );
+  await waitFor(() => getByTestId("profile"));
+  const profile = getByTestId("profile");
+  expect(profile.textContent).toContain("dummyuser");
+  expect(profile.textContent).toContain("+6285212345678");
+  expect(profile.textContent).toContain("Jl. Dummy No.1");
+  expect(profile.textContent).toContain("000");
+  expect(profile.textContent).toContain("000");
+  expect(profile.textContent).toContain("Dummy Sub-District");
+  expect(profile.textContent).toContain("Dummy Urban Village");
+});
+
+test("Test mock return error", async () => {
+  fetch
+    .once(
+      JSON.stringify({
+        count: 17,
+        next:
+          "https://industripilar-staging.herokuapp.com/transactions/?page=2",
+        previous: null,
+        results: [
+          {
+            id: "0c1db3b2-48f0-4604-83ca-e8f16e8550ab",
+            transaction_number: "H793P5ZK",
+            user: "d4b98bb5-8ba4-4a41-af10-93abcf53df58",
+            user_username: "whtestest",
+            user_full_name: "Michael Wiryadinata halim",
+            user_phone_number: "+628192090199",
+            shipping_address: "ada deh test",
+            shipping_neighborhood: "002",
+            shipping_hamlet: "002",
+            shipping_urban_village: "penggilingan",
+            shipping_sub_district: "Dummy Sub-District",
+            transaction_items: [
+              {
+                id: "37f0298c-2e8a-4ab0-a094-e9177ddc60c4",
+                product: "3d403cd3-e356-4c15-9a86-8843333e2778",
+                product_code: "5VK6TY",
+                product_name: "produk barang",
+                product_price: "50000.00",
+                quantity: 7,
+              },
+            ],
+            transaction_item_subtotal: "350000.00",
+            shipping_costs: "15000.00",
+            payment_method: "TRF",
+            readable_payment_method: "Transfer",
+            donation: "5000.00",
+            transaction_status: "002",
+            readable_transaction_status: "Waiting for seller confirmation",
+            proof_of_payment: null,
+            user_bank_account_name: "test",
+            user_bank_account_number: "1232131241321",
+            created_at: "2020-04-18T10:59:42.074386+07:00",
+            updated_at: "2020-04-18T11:00:18.150633+07:00",
+            subtotal: "370000.00",
+          },
+        ],
+      })
+    )
+    .mockReject(new Error("fake error message"));
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <DetailPengguna />
+    </AuthContext.Provider>
+  );
+
+  const page = getByTestId("page-profile");
+  await waitFor(() =>
+    expect(page.textContent).toContain("Error !, Please relogin..")
+  );
+});
diff --git a/src/__test__/ListPengguna.test.js b/src/__test__/ListPengguna.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..05959e5fb3aaa15e7b26660f688497546b4288a8
--- /dev/null
+++ b/src/__test__/ListPengguna.test.js
@@ -0,0 +1,55 @@
+import React from "react";
+import { render, cleanup } from "@testing-library/react";
+import ListPengguna from "../page/pengguna/ListPengguna";
+import AuthContext from "../utils/contex";
+import { waitFor } from "@testing-library/dom";
+
+beforeEach(() => {
+  fetch.resetMocks();
+});
+afterEach(cleanup);
+
+test("test fetching API", async () => {
+  fetch.mockResponseOnce(
+    JSON.stringify({
+      count: 2,
+      next: null,
+      previous: null,
+      results: [
+        {
+          id: "45897cc5-968c-44cf-931d-e646b095fcaf",
+          username: "admin-staging",
+          full_name: "",
+          phone_number: "",
+          address: "",
+          neighborhood: "",
+          hamlet: "",
+          urban_village: "",
+          sub_district: "",
+          profile_picture: null,
+        },
+        {
+          id: "663392ac-1dd6-462b-9301-a19c1287cefd",
+          username: "dummyuser",
+          full_name: "Dummy User",
+          phone_number: "+6285212345678",
+          address: "Jl. Dummy No.1",
+          neighborhood: "000",
+          hamlet: "000",
+          urban_village: "Dummy Urban Village",
+          sub_district: "Dummy Sub-District",
+          profile_picture: null,
+        },
+      ],
+    })
+  );
+
+  const { getByTestId, getByText } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <ListPengguna />
+    </AuthContext.Provider>
+  );
+  await waitFor(() => getByTestId("tableList"));
+  const data = getByText("Dummy User");
+  expect(data.textContent).toContain("Dummy User");
+});
diff --git a/src/__test__/Login.test.js b/src/__test__/Login.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..e719999293c01b17476a4821f6566be0924701aa
--- /dev/null
+++ b/src/__test__/Login.test.js
@@ -0,0 +1,67 @@
+import React from "react";
+import { act, cleanup, fireEvent, render } from "@testing-library/react";
+import Login from "../page/login/Login";
+import * as AuthContext from "../utils/contex";
+
+beforeEach(() => {
+  fetch.resetMocks();
+});
+afterEach(cleanup);
+
+test("Test login page validation", async () => {
+  const handleLogin = jest.fn();
+  jest
+    .spyOn(AuthContext, "useAuthContext")
+    .mockImplementation(() => ({ is_authenticated: true, handleLogin }));
+  const { getByTestId } = render(<Login />);
+  await act(async () => {
+    await fireEvent.submit(getByTestId("login"));
+  });
+  expect(getByTestId("name-required").textContent).toEqual(
+    "Username field is required"
+  );
+  expect(getByTestId("password-required").textContent).toEqual(
+    "Password field is required"
+  );
+});
+
+test("Test login submitted", async () => {
+  const handleLogin = jest.fn();
+  fetch.mockResponseOnce(JSON.stringify({ token: "AAAAAA" }));
+  jest
+    .spyOn(AuthContext, "useAuthContext")
+    .mockImplementation(() => ({ is_authenticated: true, handleLogin }));
+
+  const { getByTestId } = render(<Login />);
+
+  const username = getByTestId("name-input");
+  const password = getByTestId("password-input");
+  await act(async () => {
+    await fireEvent.input(username, { target: { value: "test" } });
+    await fireEvent.input(password, { target: { value: "password" } });
+  });
+  await act(async () => {
+    await fireEvent.submit(getByTestId("login"));
+  });
+  expect(handleLogin).toBeCalled();
+  expect(fetch).toBeCalled();
+});
+
+test("Test login error", async () => {
+  fetch.mockResponseOnce([
+    JSON.stringify([{ token: "AAAAAA" }]),
+    { status: 404 },
+  ]);
+  const { getByTestId } = render(<Login />);
+  const username = getByTestId("name-input");
+  const password = getByTestId("password-input");
+  await act(async () => {
+    await fireEvent.input(username, { target: { value: "test" } });
+    await fireEvent.input(password, { target: { value: "password" } });
+  });
+  await act(async () => {
+    await fireEvent.submit(getByTestId("login"));
+  });
+  const login = getByTestId("login");
+  expect(login.textContent).toContain("Password salah !");
+});
diff --git a/src/__test__/ProtectedRoute.test.js b/src/__test__/ProtectedRoute.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..829664f3622de6f568226a584dcf69508fda7d68
--- /dev/null
+++ b/src/__test__/ProtectedRoute.test.js
@@ -0,0 +1,30 @@
+import React from "react";
+import { cleanup } from "@testing-library/react";
+import * as AuthContext from "../utils/contex";
+import ProtectedRoute from "../component/routes/ProtectedRoute";
+import { shallow } from "enzyme";
+import { Redirect } from "@reach/router";
+import Layout from "../layout/Layout";
+import ListProduk from "../page/produk/ListProduk";
+
+afterEach(cleanup);
+
+test("Test protected route authenticated", () => {
+  jest
+    .spyOn(AuthContext, "useAuthContext")
+    .mockImplementation(() => ({ is_authenticated: true }));
+  const wrapper = shallow(
+    <ProtectedRoute path="/product" component={ListProduk} />
+  );
+  expect(wrapper.find(Layout).dive().find(ListProduk)).toHaveLength(1);
+});
+
+test("Test protected route unauthenticated", () => {
+  jest
+    .spyOn(AuthContext, "useAuthContext")
+    .mockImplementation(() => ({ is_authenticated: false }));
+  const wrapper = shallow(
+    <ProtectedRoute path="/product" component={ListProduk} />
+  );
+  expect(wrapper.find(Redirect)).toHaveLength(1);
+});
diff --git a/src/__test__/UnauthenticatedRoute.test.js b/src/__test__/UnauthenticatedRoute.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..7f6965a0edf0ce2d608f78500c909837f7634155
--- /dev/null
+++ b/src/__test__/UnauthenticatedRoute.test.js
@@ -0,0 +1,24 @@
+import React from "react";
+import { cleanup } from "@testing-library/react";
+import * as AuthContext from "../utils/contex";
+import UnauthenticatedRoute from "../component/routes/UnauthenticatedRoute";
+import Login from "../page/login/Login";
+import { shallow } from "enzyme";
+import { Redirect } from "@reach/router";
+afterEach(cleanup);
+
+test("Test unauthenticated route authenticated", () => {
+  jest
+    .spyOn(AuthContext, "useAuthContext")
+    .mockImplementation(() => ({ is_authenticated: true }));
+  const wrapper = shallow(<UnauthenticatedRoute path="/" component={Login} />);
+  expect(wrapper.find(Redirect)).toHaveLength(1);
+});
+
+test("Test unauthenticated route unauthenticated", () => {
+  jest
+    .spyOn(AuthContext, "useAuthContext")
+    .mockImplementation(() => ({ is_authenticated: false }));
+  const wrapper = shallow(<UnauthenticatedRoute path="/" component={Login} />);
+  expect(wrapper.find(Login)).toHaveLength(1);
+});
diff --git a/src/__test__/auth_reducer.test.js b/src/__test__/auth_reducer.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..369b834d32d24d235e5480829769ed7400f8042d
--- /dev/null
+++ b/src/__test__/auth_reducer.test.js
@@ -0,0 +1,26 @@
+import { cleanup } from "@testing-library/react";
+import AuthReducer, { initialState } from "../store/reducers/auth_reducer";
+import * as ACTIONS from "../store/actions/actions";
+
+afterEach(cleanup);
+
+test("auth reducer test", () => {
+  expect(AuthReducer(initialState(), {})).toEqual(initialState());
+
+  const profile = {
+    name: "test",
+    token: "abcd",
+  };
+  const expectationLogin = {
+    is_authenticated: true,
+    profile: { ...profile },
+  };
+  const loginAction = ACTIONS.login(profile);
+  expect(AuthReducer(initialState(), loginAction)).toEqual(expectationLogin);
+
+  const logoutAction = ACTIONS.logout();
+  expect(AuthReducer(expectationLogin, logoutAction)).toEqual({
+    is_authenticated: false,
+    profile: null,
+  });
+});
diff --git a/src/__test__/context_state.test.js b/src/__test__/context_state.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..c2c988d630c100b2720d0dbca3316812637ed8f7
--- /dev/null
+++ b/src/__test__/context_state.test.js
@@ -0,0 +1,33 @@
+import React from "react";
+import { act, cleanup, fireEvent, render } from "@testing-library/react";
+import ApplicationState from "../ApplicationState";
+import { navigate } from "@reach/router";
+
+afterEach(cleanup);
+
+test("Test context state", async () => {
+  fetch.mockResponseOnce(JSON.stringify({ token: "AAAAAA" }));
+  const { getByTestId } = render(<ApplicationState />);
+  const input = getByTestId("name-input");
+  const name = "test";
+  const password = getByTestId("password-input");
+  await act(async () => {
+    await fireEvent.input(input, { target: { value: name } });
+    await fireEvent.input(password, { target: { value: "password" } });
+  });
+  await act(async () => {
+    await fireEvent.submit(getByTestId("login"));
+  });
+
+  fetch.mockResponseOnce(JSON.stringify({ results: [] }));
+  await act(async () => {
+    await navigate("/pengguna");
+  });
+  expect(getByTestId("page").textContent).toContain("Pengguna");
+
+  await act(async () => {
+    await fireEvent.click(getByTestId("logout"));
+  });
+  await navigate("/");
+  expect(getByTestId("login").tagName).toEqual("FORM");
+});
diff --git a/src/__test__/kategori/DetailKategori.test.js b/src/__test__/kategori/DetailKategori.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..dbc611cf8aa0f8409976e8de1e12a90173ee672a
--- /dev/null
+++ b/src/__test__/kategori/DetailKategori.test.js
@@ -0,0 +1,283 @@
+import { act, cleanup, fireEvent, render } from "@testing-library/react";
+import AuthContext from "../../utils/contex";
+import React from "react";
+import DetailKategori from "../../page/kategori/DetailKategori";
+import { waitFor } from "@testing-library/dom";
+
+beforeEach(() => {
+  fetch.resetMocks();
+});
+afterEach(cleanup);
+
+test("Test detail kategori renders", async () => {
+  fetch
+    .once(
+      JSON.stringify({
+        count: 3,
+        next: null,
+        previous: null,
+        results: [
+          {
+            id: "ff0473d9-be63-497e-afc9-8610f57423d8",
+            name: "Jas",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540",
+            subcategory_name: "Baju Pergi",
+            description: "Jas hitam",
+            price: "800000.00",
+            stock: 5,
+            image: null,
+          },
+          {
+            id: "f3fb1295-8420-4d00-ba5e-48579092551b",
+            name: "Kemeja",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540",
+            subcategory_name: "Baju Pergi",
+            description: "kemeja putih",
+            price: "200000.00",
+            stock: 5,
+            image: null,
+          },
+          {
+            id: "45435428-e323-4f1f-8e07-f48312605504",
+            name: "celana panjang tidur",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03",
+            subcategory_name: "Baju Tidur",
+            description: "celana panjang hitam tidur",
+            price: "90000.00",
+            stock: 15,
+            image: null,
+          },
+          {
+            id: "0eb3cefe-4b70-4817-94fc-d91273cd5132",
+            name: "celana panjang tidur 1",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03",
+            subcategory_name: "Baju Tidur",
+            description: "celana panjang hitam tidur",
+            price: "90000.00",
+            stock: 15,
+            image: null,
+          },
+        ],
+      })
+    )
+    .once(
+      JSON.stringify({
+        id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+        name: "Baju",
+        image: null,
+      })
+    );
+
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <DetailKategori />
+    </AuthContext.Provider>
+  );
+  await waitFor(() => getByTestId("page-detail-kategori"));
+  const subkategori = getByTestId("page-detail-kategori");
+  expect(subkategori.textContent).toContain("Baju");
+  expect(subkategori.textContent).toContain("celana panjang tidur 1");
+  expect(fetch.mock.calls.length).toEqual(2);
+});
+
+test("Test mock detail kategori return error", async () => {
+  fetch.mockReject(new Error("fake error message"));
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <DetailKategori />
+    </AuthContext.Provider>
+  );
+  const page = getByTestId("page-detail-kategori");
+  await waitFor(() =>
+    expect(page.textContent).toContain("Error !, Please relogin..")
+  );
+});
+
+test("Test detail kategori delete", async () => {
+  fetch
+    .once(
+      JSON.stringify({
+        count: 3,
+        next: null,
+        previous: null,
+        results: [
+          {
+            id: "ff0473d9-be63-497e-afc9-8610f57423d8",
+            name: "Jas",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540",
+            subcategory_name: "Baju Pergi",
+            description: "Jas hitam",
+            price: "800000.00",
+            stock: 5,
+            image: null,
+          },
+          {
+            id: "f3fb1295-8420-4d00-ba5e-48579092551b",
+            name: "Kemeja",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540",
+            subcategory_name: "Baju Pergi",
+            description: "kemeja putih",
+            price: "200000.00",
+            stock: 5,
+            image: null,
+          },
+          {
+            id: "45435428-e323-4f1f-8e07-f48312605504",
+            name: "celana panjang tidur",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03",
+            subcategory_name: "Baju Tidur",
+            description: "celana panjang hitam tidur",
+            price: "90000.00",
+            stock: 15,
+            image: null,
+          },
+          {
+            id: "0eb3cefe-4b70-4817-94fc-d91273cd5132",
+            name: "celana panjang tidur 1",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03",
+            subcategory_name: "Baju Tidur",
+            description: "celana panjang hitam tidur",
+            price: "90000.00",
+            stock: 15,
+            image: null,
+          },
+        ],
+      })
+    )
+    .once(
+      JSON.stringify({
+        id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+        name: "Baju",
+        image: null,
+      })
+    )
+    .once(JSON.stringify({}));
+
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <DetailKategori />
+    </AuthContext.Provider>
+  );
+  await waitFor(() => getByTestId("page-detail-kategori"));
+  const subkategori = getByTestId("page-detail-kategori");
+  expect(subkategori.textContent).toContain("Baju");
+  expect(subkategori.textContent).toContain("celana panjang tidur 1");
+  expect(fetch.mock.calls.length).toEqual(2);
+  const btnDeleteModal = getByTestId("button-delete-category-modal");
+  await act(async () => {
+    await fireEvent.click(btnDeleteModal);
+  });
+  const btnDelete = getByTestId("button-delete-category");
+  await act(async () => {
+    await fireEvent.click(btnDelete);
+  });
+  expect(fetch.mock.calls.length).toEqual(3);
+});
+
+test("Test detail kategori delete error", async () => {
+  fetch
+    .once(
+      JSON.stringify({
+        count: 3,
+        next: null,
+        previous: null,
+        results: [
+          {
+            id: "ff0473d9-be63-497e-afc9-8610f57423d8",
+            name: "Jas",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540",
+            subcategory_name: "Baju Pergi",
+            description: "Jas hitam",
+            price: "800000.00",
+            stock: 5,
+            image: null,
+          },
+          {
+            id: "f3fb1295-8420-4d00-ba5e-48579092551b",
+            name: "Kemeja",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540",
+            subcategory_name: "Baju Pergi",
+            description: "kemeja putih",
+            price: "200000.00",
+            stock: 5,
+            image: null,
+          },
+          {
+            id: "45435428-e323-4f1f-8e07-f48312605504",
+            name: "celana panjang tidur",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03",
+            subcategory_name: "Baju Tidur",
+            description: "celana panjang hitam tidur",
+            price: "90000.00",
+            stock: 15,
+            image: null,
+          },
+          {
+            id: "0eb3cefe-4b70-4817-94fc-d91273cd5132",
+            name: "celana panjang tidur 1",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03",
+            subcategory_name: "Baju Tidur",
+            description: "celana panjang hitam tidur",
+            price: "90000.00",
+            stock: 15,
+            image: null,
+          },
+        ],
+      })
+    )
+    .once(
+      JSON.stringify({
+        id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+        name: "Baju",
+        image: null,
+      })
+    )
+    .mockReject(new Error("fake error message"));
+
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <DetailKategori />
+    </AuthContext.Provider>
+  );
+  await waitFor(() => getByTestId("page-detail-kategori"));
+  const subkategori = getByTestId("page-detail-kategori");
+  expect(subkategori.textContent).toContain("Baju");
+  expect(subkategori.textContent).toContain("celana panjang tidur 1");
+  expect(fetch.mock.calls.length).toEqual(2);
+  const btnDeleteModal = getByTestId("button-delete-category-modal");
+  await act(async () => {
+    await fireEvent.click(btnDeleteModal);
+  });
+  const btnDelete = getByTestId("button-delete-category");
+  await act(async () => {
+    await fireEvent.click(btnDelete);
+  });
+  expect(subkategori.textContent).toContain(
+    "Tidak dapat menghapus kategori, mohon periksa apakah ada produk " +
+      "didalam kategori ini."
+  );
+});
diff --git a/src/__test__/kategori/EditKategori.test.js b/src/__test__/kategori/EditKategori.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..08ad8ac15d0a40c4e9066433dfa90d00b98f76d8
--- /dev/null
+++ b/src/__test__/kategori/EditKategori.test.js
@@ -0,0 +1,74 @@
+import { act, cleanup, fireEvent, render } from "@testing-library/react";
+import AuthContext from "../../utils/contex";
+import { waitFor } from "@testing-library/dom";
+import React from "react";
+import EditKategori from "../../page/kategori/EditKategori";
+
+beforeEach(() => {
+  fetch.resetMocks();
+});
+afterEach(cleanup);
+test("Test edit kategori renders", async () => {
+  fetch
+    .once(
+      JSON.stringify({
+        id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+        name: "Baju",
+        image: null,
+      })
+    )
+    .once(JSON.stringify({}));
+
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <EditKategori />
+    </AuthContext.Provider>
+  );
+  const waitKategori = getByTestId("waiting-edit-kategori");
+  expect(waitKategori.textContent).toContain("Fetching data..");
+  await waitFor(() => getByTestId("edit-kategori"));
+  const name_kategori = getByTestId("name-kategori-input");
+  expect(name_kategori.value).toEqual("Baju");
+  expect(fetch.mock.calls.length).toEqual(1);
+  await act(async () => {
+    await fireEvent.input(name_kategori, { target: { value: "test" } });
+  });
+  await act(async () => {
+    await fireEvent.submit(getByTestId("submit-category"));
+  });
+
+  expect(fetch.mock.calls.length).toEqual(2);
+});
+
+test("Test Edit kategori error", async () => {
+  fetch
+    .once(
+      JSON.stringify({
+        id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+        name: "Baju",
+        image: null,
+      })
+    )
+    .once(JSON.stringify({}), { status: 400 });
+
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <EditKategori />
+    </AuthContext.Provider>
+  );
+  const waitKategori = getByTestId("waiting-edit-kategori");
+  expect(waitKategori.textContent).toContain("Fetching data..");
+  await waitFor(() => getByTestId("edit-kategori"));
+  const name_kategori = getByTestId("name-kategori-input");
+  expect(name_kategori.value).toEqual("Baju");
+  expect(fetch.mock.calls.length).toEqual(1);
+  await act(async () => {
+    await fireEvent.input(name_kategori, { target: { value: "test" } });
+  });
+  await act(async () => {
+    await fireEvent.submit(getByTestId("submit-category"));
+  });
+  const category = getByTestId("edit-kategori");
+  expect(category.textContent).toContain("Error !, Data tidak dapat disimpan");
+  expect(fetch.mock.calls.length).toEqual(2);
+});
diff --git a/src/__test__/kategori/ListKategori.test.js b/src/__test__/kategori/ListKategori.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..18dd0f10a7b5d9dfb1439600c7d0abc2cb54516a
--- /dev/null
+++ b/src/__test__/kategori/ListKategori.test.js
@@ -0,0 +1,51 @@
+import { cleanup, render } from "@testing-library/react";
+import AuthContext from "../../utils/contex";
+import React from "react";
+import ListKategori from "../../page/kategori/ListKategori";
+import { waitFor } from "@testing-library/dom";
+
+beforeEach(() => {
+  fetch.resetMocks();
+});
+afterEach(cleanup);
+
+test(" Test List Kategori", async () => {
+  fetch.mockResponseOnce(
+    JSON.stringify({
+      count: 4,
+      next: null,
+      previous: null,
+      results: [
+        {
+          id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+          name: "Baju",
+          image: null,
+        },
+        {
+          id: "69faa710-fbe2-45f1-98e8-f80d3c4c1bbe",
+          name: "Celana",
+          image: null,
+        },
+        {
+          id: "0664247c-d9ea-4e56-bb02-4b8463f9e14c",
+          name: "Mainan",
+          image: null,
+        },
+        {
+          id: "8c2c06e6-0ead-4b9a-8de1-37237fc6bdc9",
+          name: "Makanan",
+          image: null,
+        },
+      ],
+    })
+  );
+  const { getByTestId, getByText } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <ListKategori />
+    </AuthContext.Provider>
+  );
+  await waitFor(() => getByTestId("tableList"));
+  const data = getByText("Baju");
+  expect(data.textContent).toContain("Baju");
+  expect(fetch.mock.calls.length).toEqual(1);
+});
diff --git a/src/__test__/kategori/TambahKategori.test.js b/src/__test__/kategori/TambahKategori.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..fed5dc6ffd8f427400ed0d233811e6d4672c0c6f
--- /dev/null
+++ b/src/__test__/kategori/TambahKategori.test.js
@@ -0,0 +1,60 @@
+import { act, cleanup, fireEvent, render } from "@testing-library/react";
+import AuthContext from "../../utils/contex";
+import React from "react";
+import TambahKategori from "../../page/kategori/TambahKategori";
+
+beforeEach(() => {
+  fetch.resetMocks();
+});
+afterEach(cleanup);
+test("Test tambah kategori renders", async () => {
+  fetch.once(JSON.stringify({}));
+
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <TambahKategori />
+    </AuthContext.Provider>
+  );
+  const name_kategori = getByTestId("name-kategori-input");
+  await act(async () => {
+    await fireEvent.input(name_kategori, { target: { value: "test" } });
+  });
+  await act(async () => {
+    await fireEvent.submit(getByTestId("submit-category"));
+  });
+  expect(fetch.mock.calls.length).toEqual(1);
+});
+
+test("Test tambah kategori form required", async () => {
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <TambahKategori />
+    </AuthContext.Provider>
+  );
+  await act(async () => {
+    await fireEvent.submit(getByTestId("submit-category"));
+  });
+  const formCategory = getByTestId("form-category");
+  expect(formCategory.textContent).toContain(
+    "Nama kategori tidak boleh kosong"
+  );
+});
+
+test("Test tambah kategori error", async () => {
+  fetch.once(JSON.stringify({}), { status: 400 });
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <TambahKategori />
+    </AuthContext.Provider>
+  );
+  const name_kategori = getByTestId("name-kategori-input");
+  await act(async () => {
+    await fireEvent.input(name_kategori, { target: { value: "test" } });
+  });
+  await act(async () => {
+    await fireEvent.submit(getByTestId("submit-category"));
+  });
+  const category = getByTestId("tambah-kategori");
+  expect(category.textContent).toContain("Error !, Data tidak dapat disimpan");
+  expect(fetch.mock.calls.length).toEqual(1);
+});
diff --git a/src/__test__/produk/DetailProduk.test.js b/src/__test__/produk/DetailProduk.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..16b8313ba36578dc1218874ab1eee57097c89811
--- /dev/null
+++ b/src/__test__/produk/DetailProduk.test.js
@@ -0,0 +1,151 @@
+import { act, cleanup, render, fireEvent } from "@testing-library/react";
+import AuthContext from "../../utils/contex";
+import React from "react";
+import DetailProduk from "../../page/produk/DetailProduk";
+import { waitFor } from "@testing-library/dom";
+
+beforeEach(() => {
+  fetch.resetMocks();
+});
+afterEach(cleanup);
+
+test("Test detail produk renders", async () => {
+  fetch.mockResponseOnce(
+    JSON.stringify({
+      id: "ff0473d9-be63-497e-afc9-8610f57423d8",
+      name: "Kue Nastar",
+      subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540",
+      description: "Kue Nastar enak maknyuss",
+      price: 70000,
+      stock: 5,
+      image: null,
+      pre_order: false,
+    })
+  );
+
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <DetailProduk />
+    </AuthContext.Provider>
+  );
+  await waitFor(() => getByTestId("produk-detail"));
+  const produk = getByTestId("produk-detail");
+  expect(produk.textContent).toContain("Kue Nastar");
+  expect(produk.textContent).toContain("5");
+  expect(fetch.mock.calls.length).toEqual(1);
+});
+
+test("Test detail produk renders preorder", async () => {
+  fetch.mockResponseOnce(
+    JSON.stringify({
+      id: "ff0473d9-be63-497e-afc9-8610f57423d8",
+      name: "Kue Nastar",
+      subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540",
+      description: "Kue Nastar enak maknyuss",
+      price: 70000,
+      stock: null,
+      image: null,
+      pre_order: true,
+    })
+  );
+
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <DetailProduk />
+    </AuthContext.Provider>
+  );
+  await waitFor(() => getByTestId("produk-detail"));
+  const produk = getByTestId("produk-detail");
+  expect(produk.textContent).toContain("Kue Nastar");
+  expect(produk.textContent).toContain("Preorder");
+  expect(fetch.mock.calls.length).toEqual(1);
+});
+
+test("Test mock detail produk return error", async () => {
+  fetch.mockReject(new Error("fake error message"));
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <DetailProduk />
+    </AuthContext.Provider>
+  );
+  await waitFor(() => getByTestId("page"));
+  const page = getByTestId("page");
+  expect(page.textContent).toContain("Error !, Please relogin..");
+});
+
+test("Test detail produk delete", async () => {
+  fetch.mockResponseOnce(
+    JSON.stringify({
+      id: "ff0473d9-be63-497e-afc9-8610f57423d8",
+      name: "Jas",
+      category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+      category_name: "Baju",
+      subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540",
+      subcategory_name: "Baju Pergi",
+      description: "Jas hitam",
+      price: "800000.00",
+      stock: 5,
+      image: null,
+    })
+  );
+
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <DetailProduk />
+    </AuthContext.Provider>
+  );
+  await waitFor(() => getByTestId("page"));
+  const produk = getByTestId("page");
+  expect(produk.textContent).toContain("Jas");
+  expect(fetch.mock.calls.length).toEqual(1);
+  const btnDeleteModal = getByTestId("button-delete-product-modal");
+  await act(async () => {
+    await fireEvent.click(btnDeleteModal);
+  });
+  const btnDelete = getByTestId("button-delete-product");
+  await act(async () => {
+    await fireEvent.click(btnDelete);
+  });
+  expect(fetch.mock.calls.length).toEqual(2);
+});
+
+test("Test detail produk delete error", async () => {
+  fetch
+    .mockResponseOnce(
+      JSON.stringify({
+        id: "ff0473d9-be63-497e-afc9-8610f57423d8",
+        name: "Jas",
+        category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+        category_name: "Baju",
+        subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540",
+        subcategory_name: "Baju Pergi",
+        description: "Jas hitam",
+        price: "800000.00",
+        stock: 5,
+        image: null,
+      })
+    )
+    .mockReject(new Error("fake error message"));
+
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <DetailProduk />
+    </AuthContext.Provider>
+  );
+  await waitFor(() => getByTestId("page"));
+  const produk = getByTestId("page");
+  expect(produk.textContent).toContain("Jas");
+  expect(fetch.mock.calls.length).toEqual(1);
+  const btnDeleteModal = getByTestId("button-delete-product-modal");
+  await act(async () => {
+    await fireEvent.click(btnDeleteModal);
+  });
+  const btnDelete = getByTestId("button-delete-product");
+  await act(async () => {
+    await fireEvent.click(btnDelete);
+  });
+  expect(fetch.mock.calls.length).toEqual(2);
+  expect(produk.textContent).toContain(
+    "Tidak dapat menghapus produk, mohon periksa apakah ada produk ini."
+  );
+});
diff --git a/src/__test__/produk/EditProduk.test.js b/src/__test__/produk/EditProduk.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..f2851820394a3b5dd60186671e9bc79b57fcc32b
--- /dev/null
+++ b/src/__test__/produk/EditProduk.test.js
@@ -0,0 +1,197 @@
+import { act, cleanup, fireEvent, render } from "@testing-library/react";
+import AuthContext from "../../utils/contex";
+import { waitFor } from "@testing-library/dom";
+import React from "react";
+import EditProduk from "../../page/produk/EditProduk";
+
+beforeEach(() => {
+  fetch.resetMocks();
+});
+afterEach(cleanup);
+test("Test edit produk renders", async () => {
+  fetch
+    .once(
+      JSON.stringify({
+        id: "3d403cd3-e356-4c15-9a86-8843333e2778",
+        code: "5VK6TY",
+        name: "a",
+        category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+        category_name: "Baju",
+        subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03",
+        subcategory_name: "Baju Tidur hehe",
+        description: "celana",
+        price: "50000.00",
+        stock: 17,
+        image: null,
+        pre_order: false,
+      })
+    )
+    .once(
+      JSON.stringify({
+        count: 4,
+        next: null,
+        previous: null,
+        results: [
+          {
+            id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            name: "Baju",
+            image: null,
+          },
+          {
+            id: "0664247c-d9ea-4e56-bb02-4b8463f9e14c",
+            name: "Mainan",
+            image: null,
+          },
+          {
+            id: "8c2c06e6-0ead-4b9a-8de1-37237fc6bdc9",
+            name: "Makanan",
+            image: null,
+          },
+          {
+            id: "b8a9909b-9131-4c2b-bcc5-6bceb18f702c",
+            name: "Minuman",
+            image: null,
+          },
+        ],
+      })
+    )
+    .once(
+      JSON.stringify({
+        count: 7,
+        next: null,
+        previous: null,
+        results: [
+          {
+            id: "626aa022-50a7-4d3a-b658-79cb0f059b03",
+            name: "Baju Tidur hehe",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            image: null,
+          },
+          {
+            id: "ab222bb8-46e4-40bb-bd47-7f546d356de1",
+            name: "joker baru",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            image: null,
+          },
+        ],
+      })
+    )
+    .once(JSON.stringify({}), { statusCode: 200 });
+
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <EditProduk />
+    </AuthContext.Provider>
+  );
+  const waitProduk = getByTestId("waiting-edit-produk");
+  expect(waitProduk.textContent).toContain("Fetching data..");
+  await waitFor(() => getByTestId("edit-produk"));
+  const name_produk = getByTestId("name-produk-input");
+  expect(name_produk.value).toEqual("a");
+  await act(async () => {
+    await fireEvent.input(name_produk, { target: { value: "test" } });
+  });
+  await act(async () => {
+    await fireEvent.submit(getByTestId("submit-produk"));
+  });
+
+  expect(fetch.mock.calls.length).toEqual(5);
+});
+
+test("Test edit produk renders error", async () => {
+  fetch
+    .once(
+      JSON.stringify({
+        id: "3d403cd3-e356-4c15-9a86-8843333e2778",
+        code: "5VK6TY",
+        name: "a",
+        category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+        category_name: "Baju",
+        subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03",
+        subcategory_name: "Baju Tidur hehe",
+        description: "celana",
+        price: "50000.00",
+        stock: 17,
+        image: null,
+        pre_order: false,
+      })
+    )
+    .once(
+      JSON.stringify({
+        count: 4,
+        next: null,
+        previous: null,
+        results: [
+          {
+            id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            name: "Baju",
+            image: null,
+          },
+          {
+            id: "0664247c-d9ea-4e56-bb02-4b8463f9e14c",
+            name: "Mainan",
+            image: null,
+          },
+          {
+            id: "8c2c06e6-0ead-4b9a-8de1-37237fc6bdc9",
+            name: "Makanan",
+            image: null,
+          },
+          {
+            id: "b8a9909b-9131-4c2b-bcc5-6bceb18f702c",
+            name: "Minuman",
+            image: null,
+          },
+        ],
+      })
+    )
+    .once(
+      JSON.stringify({
+        count: 7,
+        next: null,
+        previous: null,
+        results: [
+          {
+            id: "626aa022-50a7-4d3a-b658-79cb0f059b03",
+            name: "Baju Tidur hehe",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            image: null,
+          },
+          {
+            id: "ab222bb8-46e4-40bb-bd47-7f546d356de1",
+            name: "joker baru",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            image: null,
+          },
+        ],
+      })
+    )
+    .once(JSON.stringify({}), { status: 400 });
+
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <EditProduk />
+    </AuthContext.Provider>
+  );
+  const waitProduk = getByTestId("waiting-edit-produk");
+  expect(waitProduk.textContent).toContain("Fetching data..");
+  await waitFor(() => getByTestId("edit-produk"));
+  const name_produk = getByTestId("name-produk-input");
+  expect(name_produk.value).toEqual("a");
+  const stock_produk = getByTestId("stock-produk-input");
+  await act(async () => {
+    await fireEvent.input(stock_produk, { target: { value: "-1" } });
+  });
+  await act(async () => {
+    await fireEvent.submit(getByTestId("submit-produk"));
+  });
+  const produk = getByTestId("edit-produk");
+  expect(produk.textContent).toContain(
+    "Error loading form !, Please relogin.."
+  );
+  expect(fetch.mock.calls.length).toEqual(5);
+});
diff --git a/src/__test__/produk/ListProduk.test.js b/src/__test__/produk/ListProduk.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..5a5094b884e594a6310d51361aa8f7ee66659e35
--- /dev/null
+++ b/src/__test__/produk/ListProduk.test.js
@@ -0,0 +1,57 @@
+import { cleanup, render } from "@testing-library/react";
+import AuthContext from "../../utils/contex";
+import React from "react";
+import { waitFor } from "@testing-library/dom";
+import ListProduk from "../../page/produk/ListProduk";
+
+beforeEach(() => {
+  fetch.resetMocks();
+});
+afterEach(cleanup);
+
+test(" Test List produk", async () => {
+  fetch.mockResponseOnce(
+    JSON.stringify({
+      count: 23,
+      next: "https://industripilar-staging.herokuapp.com/products/?page=2",
+      previous: null,
+      results: [
+        {
+          id: "3d403cd3-e356-4c15-9a86-8843333e2778",
+          code: "5VK6TY",
+          name: "a",
+          category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+          category_name: "Baju",
+          subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03",
+          subcategory_name: "Baju Tidur hehe",
+          description: "celana",
+          price: "50000.00",
+          stock: 9,
+          image: null,
+        },
+        {
+          id: "9a0bccaa-70f6-48a8-89fc-5c5994684729",
+          code: "4QKSBC",
+          name: "Piyama",
+          category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+          category_name: "Baju",
+          subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03",
+          subcategory_name: "Baju Tidur hehe",
+          description: "piyama",
+          price: "50000.00",
+          stock: 14,
+          image: null,
+        },
+      ],
+    })
+  );
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "tester" } }}>
+      <ListProduk />
+    </AuthContext.Provider>
+  );
+  await waitFor(() => getByTestId("tableList"));
+  const data = getByTestId("tableList");
+  expect(data.textContent).toContain("Piyama");
+  expect(fetch.mock.calls.length).toEqual(1);
+});
diff --git a/src/__test__/produk/TambahProduk.test.js b/src/__test__/produk/TambahProduk.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..69d0828fdf638be530f5e34380ab6b653ec80f34
--- /dev/null
+++ b/src/__test__/produk/TambahProduk.test.js
@@ -0,0 +1,257 @@
+import { act, cleanup, fireEvent, render } from "@testing-library/react";
+import AuthContext from "../../utils/contex";
+import React from "react";
+import TambahProduk from "../../page/produk/TambahProduk";
+
+beforeEach(() => {
+  fetch.resetMocks();
+});
+afterEach(cleanup);
+test("Test tambah produk renders", async () => {
+  fetch
+    .once(
+      JSON.stringify({
+        count: 4,
+        next: null,
+        previous: null,
+        results: [
+          {
+            id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            name: "Baju",
+            image: null,
+          },
+          {
+            id: "0664247c-d9ea-4e56-bb02-4b8463f9e14c",
+            name: "Mainan",
+            image: null,
+          },
+          {
+            id: "8c2c06e6-0ead-4b9a-8de1-37237fc6bdc9",
+            name: "Makanan",
+            image: null,
+          },
+          {
+            id: "b8a9909b-9131-4c2b-bcc5-6bceb18f702c",
+            name: "Minuman",
+            image: null,
+          },
+        ],
+      })
+    )
+    .once(
+      JSON.stringify({
+        count: 7,
+        next: null,
+        previous: null,
+        results: [
+          {
+            id: "626aa022-50a7-4d3a-b658-79cb0f059b03",
+            name: "Baju Tidur hehe",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            image: null,
+          },
+          {
+            id: "ab222bb8-46e4-40bb-bd47-7f546d356de1",
+            name: "joker baru",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            image: null,
+          },
+        ],
+      })
+    )
+    .once(JSON.stringify({}), { statusCode: 200 });
+
+  const { getByTestId, getByLabelText } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <TambahProduk />
+    </AuthContext.Provider>
+  );
+  const name_produk = getByTestId("name-produk-input");
+  await act(async () => {
+    await fireEvent.input(name_produk, { target: { value: "test" } });
+  });
+  const desc_produk = getByTestId("desc-produk-input");
+  await act(async () => {
+    await fireEvent.input(desc_produk, { target: { value: "test" } });
+  });
+  const price_produk = getByTestId("price-produk-input");
+  await act(async () => {
+    await fireEvent.input(price_produk, { target: { value: "1" } });
+  });
+  const stock_produk = getByTestId("stock-produk-input");
+  await act(async () => {
+    await fireEvent.input(stock_produk, { target: { value: "1" } });
+  });
+  await act(async () => {
+    await fireEvent.click(getByLabelText("Biasa"), {
+      target: { value: "false" },
+    });
+  });
+  await act(async () => {
+    await fireEvent.submit(getByTestId("submit-produk"));
+  });
+  expect(fetch.mock.calls.length).toEqual(4);
+});
+
+test("Test tambah produk form required", async () => {
+  fetch
+    .once(
+      JSON.stringify({
+        count: 4,
+        next: null,
+        previous: null,
+        results: [
+          {
+            id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            name: "Baju",
+            image: null,
+          },
+          {
+            id: "0664247c-d9ea-4e56-bb02-4b8463f9e14c",
+            name: "Mainan",
+            image: null,
+          },
+          {
+            id: "8c2c06e6-0ead-4b9a-8de1-37237fc6bdc9",
+            name: "Makanan",
+            image: null,
+          },
+          {
+            id: "b8a9909b-9131-4c2b-bcc5-6bceb18f702c",
+            name: "Minuman",
+            image: null,
+          },
+        ],
+      })
+    )
+    .once(
+      JSON.stringify({
+        count: 7,
+        next: null,
+        previous: null,
+        results: [
+          {
+            id: "626aa022-50a7-4d3a-b658-79cb0f059b03",
+            name: "Baju Tidur hehe",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            image: null,
+          },
+          {
+            id: "ab222bb8-46e4-40bb-bd47-7f546d356de1",
+            name: "joker baru",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            image: null,
+          },
+        ],
+      })
+    )
+    .once(JSON.stringify({}));
+
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <TambahProduk />
+    </AuthContext.Provider>
+  );
+  await act(async () => {
+    await fireEvent.submit(getByTestId("submit-produk"));
+  });
+  const formCategory = getByTestId("form-produk");
+  expect(formCategory.textContent).toContain("Nama Produk tidak boleh kosong");
+  expect(fetch.mock.calls.length).toEqual(3);
+});
+
+test("Test tambah produk error", async () => {
+  fetch
+    .once(
+      JSON.stringify({
+        count: 4,
+        next: null,
+        previous: null,
+        results: [
+          {
+            id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            name: "Baju",
+            image: null,
+          },
+          {
+            id: "0664247c-d9ea-4e56-bb02-4b8463f9e14c",
+            name: "Mainan",
+            image: null,
+          },
+          {
+            id: "8c2c06e6-0ead-4b9a-8de1-37237fc6bdc9",
+            name: "Makanan",
+            image: null,
+          },
+          {
+            id: "b8a9909b-9131-4c2b-bcc5-6bceb18f702c",
+            name: "Minuman",
+            image: null,
+          },
+        ],
+      })
+    )
+    .once(
+      JSON.stringify({
+        count: 7,
+        next: null,
+        previous: null,
+        results: [
+          {
+            id: "626aa022-50a7-4d3a-b658-79cb0f059b03",
+            name: "Baju Tidur hehe",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            image: null,
+          },
+          {
+            id: "ab222bb8-46e4-40bb-bd47-7f546d356de1",
+            name: "joker baru",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            image: null,
+          },
+        ],
+      })
+    )
+    .once(JSON.stringify({}), { status: 400 });
+
+  const { getByTestId, getByLabelText } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <TambahProduk />
+    </AuthContext.Provider>
+  );
+  const name_produk = getByTestId("name-produk-input");
+  await act(async () => {
+    await fireEvent.input(name_produk, { target: { value: "test" } });
+  });
+  const desc_produk = getByTestId("desc-produk-input");
+  await act(async () => {
+    await fireEvent.input(desc_produk, { target: { value: "test" } });
+  });
+  const price_produk = getByTestId("price-produk-input");
+  await act(async () => {
+    await fireEvent.input(price_produk, { target: { value: "1" } });
+  });
+  const stock_produk = getByTestId("stock-produk-input");
+  await act(async () => {
+    await fireEvent.input(stock_produk, { target: { value: "1" } });
+  });
+  await act(async () => {
+    await fireEvent.click(getByLabelText("Biasa"), {
+      target: { value: "false" },
+    });
+  });
+  await act(async () => {
+    await fireEvent.submit(getByTestId("submit-produk"));
+  });
+  const produk = getByTestId("tambah-produk");
+  expect(produk.textContent).toContain(
+    "Error loading form !, Please relogin.."
+  );
+  expect(fetch.mock.calls.length).toEqual(4);
+});
diff --git a/src/__test__/program/ListProgram.test.js b/src/__test__/program/ListProgram.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..90a69fe1dad5bd906bba84c8525ee6086298d21e
--- /dev/null
+++ b/src/__test__/program/ListProgram.test.js
@@ -0,0 +1,41 @@
+import { cleanup, render } from "@testing-library/react";
+import AuthContext from "../../utils/contex";
+import React from "react";
+import ListProgram from "../../page/program/ListProgram";
+import { waitFor } from "@testing-library/dom";
+
+beforeEach(() => {
+  fetch.resetMocks();
+});
+afterEach(cleanup);
+
+test(" Test List Program", async () => {
+  fetch.mockResponseOnce(
+    JSON.stringify({
+      count: 1,
+      next: null,
+      previous: null,
+      results: [
+        {
+          id: "282007ff-d0be-4d68-823e-03bde086ee79",
+          name: "Pilar Belajar",
+          description: "Pilar Belajar adalah program belajar untuk anak-anak.",
+          start_date_time: "2020-04-08T10:00:00+07:00",
+          end_date_time: "2020-04-09T10:00:00+07:00",
+          location: "Balai Warga Jatinegara Baru",
+          speaker: "Sherlock Holmes",
+          poster_image: null,
+        },
+      ],
+    })
+  );
+  const { getByTestId, getByText } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <ListProgram />
+    </AuthContext.Provider>
+  );
+  await waitFor(() => getByTestId("tableList"));
+  const data = getByText("Pilar Belajar");
+  expect(data.textContent).toContain("Pilar Belajar");
+  expect(fetch.mock.calls.length).toEqual(1);
+});
diff --git a/src/__test__/routes.test.js b/src/__test__/routes.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..1707c167461eb65b475ea253938d8fa3a6c30089
--- /dev/null
+++ b/src/__test__/routes.test.js
@@ -0,0 +1,12 @@
+import React from "react";
+import { cleanup } from "@testing-library/react";
+import { shallow } from "enzyme";
+import { Router } from "@reach/router";
+import Routes from "../routes";
+
+afterEach(cleanup);
+
+test("renders routes", () => {
+  const wrapper = shallow(<Routes />);
+  expect(wrapper.find(Router)).toHaveLength(1);
+});
diff --git a/src/__test__/setup-jest.js b/src/__test__/setup-jest.js
new file mode 100644
index 0000000000000000000000000000000000000000..4a8fa00624bec7ab04d56ce5e133b158c545b943
--- /dev/null
+++ b/src/__test__/setup-jest.js
@@ -0,0 +1,3 @@
+import "mutationobserver-shim";
+import "regenerator-runtime";
+global.fetch = require("jest-fetch-mock");
diff --git a/src/__test__/subkategori/DetailSubCategory.test.js b/src/__test__/subkategori/DetailSubCategory.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..3621acc8bc60ba397ea9f8f27adfe879c41321c8
--- /dev/null
+++ b/src/__test__/subkategori/DetailSubCategory.test.js
@@ -0,0 +1,289 @@
+import { act, cleanup, fireEvent, render } from "@testing-library/react";
+import AuthContext from "../../utils/contex";
+import React from "react";
+import DetailSubkategori from "../../page/subkategori/DetailSubkategori";
+import { waitFor } from "@testing-library/dom";
+
+beforeEach(() => {
+  fetch.resetMocks();
+});
+afterEach(cleanup);
+
+test("Test detail subkategori renders", async () => {
+  fetch
+    .once(
+      JSON.stringify({
+        count: 3,
+        next: null,
+        previous: null,
+        results: [
+          {
+            id: "ff0473d9-be63-497e-afc9-8610f57423d8",
+            name: "Jas",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540",
+            subcategory_name: "Baju Pergi",
+            description: "Jas hitam",
+            price: "800000.00",
+            stock: 5,
+            image: null,
+          },
+          {
+            id: "f3fb1295-8420-4d00-ba5e-48579092551b",
+            name: "Kemeja",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540",
+            subcategory_name: "Baju Pergi",
+            description: "kemeja putih",
+            price: "200000.00",
+            stock: 5,
+            image: null,
+          },
+          {
+            id: "45435428-e323-4f1f-8e07-f48312605504",
+            name: "celana panjang tidur",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03",
+            subcategory_name: "Baju Tidur",
+            description: "celana panjang hitam tidur",
+            price: "90000.00",
+            stock: 15,
+            image: null,
+          },
+          {
+            id: "0eb3cefe-4b70-4817-94fc-d91273cd5132",
+            name: "celana panjang tidur 1",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03",
+            subcategory_name: "Baju Tidur",
+            description: "celana panjang hitam tidur",
+            price: "90000.00",
+            stock: 15,
+            image: null,
+          },
+        ],
+      })
+    )
+    .once(
+      JSON.stringify({
+        id: "1fac049f-592c-4c15-afe6-9e05a2ce1540",
+        name: "Baju Pergi",
+        category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+        category_name: "Baju Pergi",
+        image: null,
+      })
+    );
+
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <DetailSubkategori />
+    </AuthContext.Provider>
+  );
+  await waitFor(() => getByTestId("page-detail-subkategori"));
+  const subkategori = getByTestId("page-detail-subkategori");
+  expect(subkategori.textContent).toContain("Baju Pergi");
+  expect(subkategori.textContent).toContain("celana panjang tidur 1");
+  expect(fetch.mock.calls.length).toEqual(2);
+});
+
+test("Test mock detail subkategori return error", async () => {
+  fetch.mockReject(new Error("fake error message"));
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <DetailSubkategori />
+    </AuthContext.Provider>
+  );
+  const page = getByTestId("page-detail-subkategori");
+  await waitFor(() =>
+    expect(page.textContent).toContain("Error !, Please relogin..")
+  );
+});
+
+test("Test detail subkategori delete", async () => {
+  fetch
+    .once(
+      JSON.stringify({
+        count: 3,
+        next: null,
+        previous: null,
+        results: [
+          {
+            id: "ff0473d9-be63-497e-afc9-8610f57423d8",
+            name: "Jas",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540",
+            subcategory_name: "Baju Pergi",
+            description: "Jas hitam",
+            price: "800000.00",
+            stock: 5,
+            image: null,
+          },
+          {
+            id: "f3fb1295-8420-4d00-ba5e-48579092551b",
+            name: "Kemeja",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540",
+            subcategory_name: "Baju Pergi",
+            description: "kemeja putih",
+            price: "200000.00",
+            stock: 5,
+            image: null,
+          },
+          {
+            id: "45435428-e323-4f1f-8e07-f48312605504",
+            name: "celana panjang tidur",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03",
+            subcategory_name: "Baju Tidur",
+            description: "celana panjang hitam tidur",
+            price: "90000.00",
+            stock: 15,
+            image: null,
+          },
+          {
+            id: "0eb3cefe-4b70-4817-94fc-d91273cd5132",
+            name: "celana panjang tidur 1",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03",
+            subcategory_name: "Baju Tidur",
+            description: "celana panjang hitam tidur",
+            price: "90000.00",
+            stock: 15,
+            image: null,
+          },
+        ],
+      })
+    )
+    .once(
+      JSON.stringify({
+        id: "1fac049f-592c-4c15-afe6-9e05a2ce1540",
+        name: "Baju Pergi",
+        category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+        category_name: "Baju Pergi",
+        image: null,
+      })
+    )
+    .once(JSON.stringify({}));
+
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <DetailSubkategori />
+    </AuthContext.Provider>
+  );
+  await waitFor(() => getByTestId("page-detail-subkategori"));
+  const subkategori = getByTestId("page-detail-subkategori");
+  expect(subkategori.textContent).toContain("Baju Pergi");
+  expect(subkategori.textContent).toContain("celana panjang tidur 1");
+  expect(fetch.mock.calls.length).toEqual(2);
+  const btnDeleteModal = getByTestId("button-delete-subcategory-modal");
+  await act(async () => {
+    await fireEvent.click(btnDeleteModal);
+  });
+  const btnDelete = getByTestId("button-delete-subcategory");
+  await act(async () => {
+    await fireEvent.click(btnDelete);
+  });
+  expect(fetch.mock.calls.length).toEqual(3);
+});
+
+test("Test detail subkategori delete error", async () => {
+  fetch
+    .once(
+      JSON.stringify({
+        count: 3,
+        next: null,
+        previous: null,
+        results: [
+          {
+            id: "ff0473d9-be63-497e-afc9-8610f57423d8",
+            name: "Jas",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540",
+            subcategory_name: "Baju Pergi",
+            description: "Jas hitam",
+            price: "800000.00",
+            stock: 5,
+            image: null,
+          },
+          {
+            id: "f3fb1295-8420-4d00-ba5e-48579092551b",
+            name: "Kemeja",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540",
+            subcategory_name: "Baju Pergi",
+            description: "kemeja putih",
+            price: "200000.00",
+            stock: 5,
+            image: null,
+          },
+          {
+            id: "45435428-e323-4f1f-8e07-f48312605504",
+            name: "celana panjang tidur",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03",
+            subcategory_name: "Baju Tidur",
+            description: "celana panjang hitam tidur",
+            price: "90000.00",
+            stock: 15,
+            image: null,
+          },
+          {
+            id: "0eb3cefe-4b70-4817-94fc-d91273cd5132",
+            name: "celana panjang tidur 1",
+            category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            category_name: "Baju",
+            subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03",
+            subcategory_name: "Baju Tidur",
+            description: "celana panjang hitam tidur",
+            price: "90000.00",
+            stock: 15,
+            image: null,
+          },
+        ],
+      })
+    )
+    .once(
+      JSON.stringify({
+        id: "1fac049f-592c-4c15-afe6-9e05a2ce1540",
+        name: "Baju Pergi",
+        category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+        category_name: "Baju Pergi",
+        image: null,
+      })
+    )
+    .mockReject(new Error("fake error message"));
+
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <DetailSubkategori />
+    </AuthContext.Provider>
+  );
+  await waitFor(() => getByTestId("page-detail-subkategori"));
+  const subkategori = getByTestId("page-detail-subkategori");
+  expect(subkategori.textContent).toContain("Baju Pergi");
+  expect(subkategori.textContent).toContain("celana panjang tidur 1");
+  expect(fetch.mock.calls.length).toEqual(2);
+  const btnDeleteModal = getByTestId("button-delete-subcategory-modal");
+  await act(async () => {
+    await fireEvent.click(btnDeleteModal);
+  });
+  const btnDelete = getByTestId("button-delete-subcategory");
+  await act(async () => {
+    await fireEvent.click(btnDelete);
+  });
+  expect(subkategori.textContent).toContain(
+    "Tidak dapat menghapus subkategori, mohon periksa apakah ada produk " +
+      "didalam kategori ini."
+  );
+});
diff --git a/src/__test__/subkategori/EditSubCategory.test.js b/src/__test__/subkategori/EditSubCategory.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..e6b63a724b88ef13b0cf5ee1bed4b8e00525d2f8
--- /dev/null
+++ b/src/__test__/subkategori/EditSubCategory.test.js
@@ -0,0 +1,149 @@
+import { act, cleanup, fireEvent, render } from "@testing-library/react";
+import AuthContext from "../../utils/contex";
+
+import { waitFor } from "@testing-library/dom";
+import React from "react";
+import EditSubkategori from "../../page/subkategori/EditSubkategori";
+
+beforeEach(() => {
+  fetch.resetMocks();
+});
+afterEach(cleanup);
+test("Test edit subkategori renders", async () => {
+  fetch
+    .once(
+      JSON.stringify({
+        id: "cbc3dcdd-98e2-459f-b244-6f07819dda8a",
+        name: "avengers endgame",
+        category: "0664247c-d9ea-4e56-bb02-4b8463f9e14c",
+        category_name: "Mainan",
+        image:
+          "https://industripilar-api-staging.s3.amazonaws.com/media/uploads/subcategories/1_txtcYocQEGtOFN33ZCTDbw.png",
+      })
+    )
+    .once(
+      JSON.stringify({
+        count: 3,
+        next: null,
+        previous: null,
+        results: [
+          {
+            id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            name: "Baju",
+            image: null,
+          },
+          {
+            id: "0664247c-d9ea-4e56-bb02-4b8463f9e14c",
+            name: "Mainan",
+            image: null,
+          },
+          {
+            id: "8c2c06e6-0ead-4b9a-8de1-37237fc6bdc9",
+            name: "Makanan",
+            image: null,
+          },
+        ],
+      })
+    )
+    .once(JSON.stringify({}));
+
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <EditSubkategori />
+    </AuthContext.Provider>
+  );
+  const waitSubkategori = getByTestId("waiting-edit-subkategori");
+  expect(waitSubkategori.textContent).toContain("Fetching data..");
+  await waitFor(() => getByTestId("edit-subkategori"));
+  const name_subkategori = getByTestId("name-subkategori-input");
+  expect(name_subkategori.value).toEqual("avengers endgame");
+  await waitFor(() => getByTestId("category-subcategory-input"));
+  const category_subkategori = getByTestId("category-subcategory-input");
+  expect(category_subkategori.children.length).toEqual(3);
+  expect(category_subkategori.value).toEqual(
+    "0664247c-d9ea-4e56-bb02-4b8463f9e14c"
+  );
+  expect(fetch.mock.calls.length).toEqual(2);
+  await act(async () => {
+    await fireEvent.input(name_subkategori, { target: { value: "test" } });
+    await fireEvent.input(category_subkategori, {
+      target: { value: "password" },
+    });
+  });
+  await act(async () => {
+    await fireEvent.submit(getByTestId("submit-subcategory"));
+  });
+
+  expect(fetch.mock.calls.length).toEqual(3);
+});
+
+test("Test Edit subkategori error", async () => {
+  fetch
+    .once(
+      JSON.stringify({
+        id: "cbc3dcdd-98e2-459f-b244-6f07819dda8a",
+        name: "avengers endgame",
+        category: "0664247c-d9ea-4e56-bb02-4b8463f9e14c",
+        category_name: "Mainan",
+        image:
+          "https://industripilar-api-staging.s3.amazonaws.com/media/uploads/subcategories/1_txtcYocQEGtOFN33ZCTDbw.png",
+      })
+    )
+    .once(
+      JSON.stringify({
+        count: 3,
+        next: null,
+        previous: null,
+        results: [
+          {
+            id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            name: "Baju",
+            image: null,
+          },
+          {
+            id: "0664247c-d9ea-4e56-bb02-4b8463f9e14c",
+            name: "Mainan",
+            image: null,
+          },
+          {
+            id: "8c2c06e6-0ead-4b9a-8de1-37237fc6bdc9",
+            name: "Makanan",
+            image: null,
+          },
+        ],
+      })
+    )
+    .once(JSON.stringify({}), { status: 400 });
+
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <EditSubkategori />
+    </AuthContext.Provider>
+  );
+  const waitSubkategori = getByTestId("waiting-edit-subkategori");
+  expect(waitSubkategori.textContent).toContain("Fetching data..");
+  await waitFor(() => getByTestId("edit-subkategori"));
+  const name_subkategori = getByTestId("name-subkategori-input");
+  expect(name_subkategori.value).toEqual("avengers endgame");
+  await waitFor(() => getByTestId("category-subcategory-input"));
+  const category_subkategori = getByTestId("category-subcategory-input");
+  expect(category_subkategori.children.length).toEqual(3);
+  expect(category_subkategori.value).toEqual(
+    "0664247c-d9ea-4e56-bb02-4b8463f9e14c"
+  );
+  expect(fetch.mock.calls.length).toEqual(2);
+  await act(async () => {
+    await fireEvent.input(name_subkategori, { target: { value: "test" } });
+    await fireEvent.input(category_subkategori, {
+      target: { value: "password" },
+    });
+  });
+  await act(async () => {
+    await fireEvent.submit(getByTestId("submit-subcategory"));
+  });
+  const subcategory = getByTestId("edit-subkategori");
+  expect(subcategory.textContent).toContain(
+    "Error !, Data tidak dapat disimpan"
+  );
+  expect(fetch.mock.calls.length).toEqual(3);
+});
diff --git a/src/__test__/subkategori/ListSubkategori.test.js b/src/__test__/subkategori/ListSubkategori.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..c76a0228cfa82d07e1f25811a98f7ebb1669e6a2
--- /dev/null
+++ b/src/__test__/subkategori/ListSubkategori.test.js
@@ -0,0 +1,52 @@
+import { cleanup, render } from "@testing-library/react";
+import AuthContext from "../../utils/contex";
+import React from "react";
+import ListSubkategori from "../../page/subkategori/ListSubkategori";
+import { waitFor } from "@testing-library/dom";
+
+beforeEach(() => {
+  fetch.resetMocks();
+});
+afterEach(cleanup);
+
+test(" Test List subkategori", async () => {
+  fetch.mockResponseOnce(
+    JSON.stringify({
+      count: 2,
+      next: null,
+      previous: null,
+      results: [
+        {
+          id: "1fac049f-592c-4c15-afe6-9e05a2ce1540",
+          name: "Baju Pergi",
+          category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+          category_name: "Baju",
+          image: null,
+        },
+        {
+          id: "626aa022-50a7-4d3a-b658-79cb0f059b03",
+          name: "Baju Tidur",
+          category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+          category_name: "Baju",
+          image: null,
+        },
+        {
+          id: "e44da96a-44e3-4e69-8346-bd1da7b8e41f",
+          name: "Kue",
+          category: "8c2c06e6-0ead-4b9a-8de1-37237fc6bdc9",
+          category_name: "Makanan",
+          image: null,
+        },
+      ],
+    })
+  );
+  const { getByTestId, getByText } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <ListSubkategori />
+    </AuthContext.Provider>
+  );
+  await waitFor(() => getByTestId("tableList"));
+  const data = getByText("Baju Pergi");
+  expect(data.textContent).toContain("Baju Pergi");
+  expect(fetch.mock.calls.length).toEqual(1);
+});
diff --git a/src/__test__/subkategori/TambahSubCategory.test.js b/src/__test__/subkategori/TambahSubCategory.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..e8a888e1cb57fd04f2d4e9e3569a711044d5e46e
--- /dev/null
+++ b/src/__test__/subkategori/TambahSubCategory.test.js
@@ -0,0 +1,176 @@
+import { act, cleanup, fireEvent, render } from "@testing-library/react";
+import AuthContext from "../../utils/contex";
+import { waitFor } from "@testing-library/dom";
+import React from "react";
+import TambahSubkategori from "../../page/subkategori/TambahSubkategori";
+
+beforeEach(() => {
+  fetch.resetMocks();
+});
+afterEach(cleanup);
+test("Test tambah subkategori renders", async () => {
+  fetch
+    .once(
+      JSON.stringify({
+        count: 3,
+        next: null,
+        previous: null,
+        results: [
+          {
+            id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            name: "Baju",
+            image: null,
+          },
+          {
+            id: "0664247c-d9ea-4e56-bb02-4b8463f9e14c",
+            name: "Mainan",
+            image: null,
+          },
+          {
+            id: "8c2c06e6-0ead-4b9a-8de1-37237fc6bdc9",
+            name: "Makanan",
+            image: null,
+          },
+        ],
+      })
+    )
+    .once(JSON.stringify({}));
+
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <TambahSubkategori />
+    </AuthContext.Provider>
+  );
+  const name_subkategori = getByTestId("name-subkategori-input");
+  await waitFor(() => getByTestId("category-subcategory-input"));
+  const category_subkategori = getByTestId("category-subcategory-input");
+  expect(category_subkategori.children.length).toEqual(3);
+  expect(fetch.mock.calls.length).toEqual(1);
+  await act(async () => {
+    await fireEvent.input(name_subkategori, { target: { value: "test" } });
+    await fireEvent.input(category_subkategori, {
+      target: { value: "password" },
+    });
+  });
+  await act(async () => {
+    await fireEvent.submit(getByTestId("submit-subcategory"));
+  });
+
+  expect(fetch.mock.calls.length).toEqual(2);
+});
+
+test("Test tambah subkategori form required", async () => {
+  fetch
+    .once(
+      JSON.stringify({
+        count: 3,
+        next: null,
+        previous: null,
+        results: [
+          {
+            id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            name: "Baju",
+            image: null,
+          },
+          {
+            id: "0664247c-d9ea-4e56-bb02-4b8463f9e14c",
+            name: "Mainan",
+            image: null,
+          },
+          {
+            id: "8c2c06e6-0ead-4b9a-8de1-37237fc6bdc9",
+            name: "Makanan",
+            image: null,
+          },
+        ],
+      })
+    )
+    .once(JSON.stringify({}));
+
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <TambahSubkategori />
+    </AuthContext.Provider>
+  );
+  await waitFor(() => getByTestId("category-subcategory-input"));
+  const category_subkategori = getByTestId("category-subcategory-input");
+  expect(category_subkategori.children.length).toEqual(3);
+  expect(fetch.mock.calls.length).toEqual(1);
+  await act(async () => {
+    await fireEvent.submit(getByTestId("submit-subcategory"));
+  });
+  const formCategory = getByTestId("form-subcategory");
+  expect(formCategory.textContent).toContain(
+    "Nama subkategori tidak boleh kosong"
+  );
+  expect(fetch.mock.calls.length).toEqual(1);
+});
+
+test("Test tambah subkategori error", async () => {
+  fetch
+    .once(
+      JSON.stringify({
+        count: 3,
+        next: null,
+        previous: null,
+        results: [
+          {
+            id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4",
+            name: "Baju",
+            image: null,
+          },
+          {
+            id: "0664247c-d9ea-4e56-bb02-4b8463f9e14c",
+            name: "Mainan",
+            image: null,
+          },
+          {
+            id: "8c2c06e6-0ead-4b9a-8de1-37237fc6bdc9",
+            name: "Makanan",
+            image: null,
+          },
+        ],
+      })
+    )
+    .once(JSON.stringify({}), { status: 400 });
+
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <TambahSubkategori />
+    </AuthContext.Provider>
+  );
+  const name_subkategori = getByTestId("name-subkategori-input");
+  await waitFor(() => getByTestId("category-subcategory-input"));
+  const category_subkategori = getByTestId("category-subcategory-input");
+  expect(category_subkategori.children.length).toEqual(3);
+  expect(fetch.mock.calls.length).toEqual(1);
+  await act(async () => {
+    await fireEvent.input(name_subkategori, { target: { value: "test" } });
+    await fireEvent.input(category_subkategori, {
+      target: { value: "password" },
+    });
+  });
+  await act(async () => {
+    await fireEvent.submit(getByTestId("submit-subcategory"));
+  });
+  const subcategory = getByTestId("tambah-subkategori");
+  expect(subcategory.textContent).toContain(
+    "Error !, Data tidak dapat disimpan"
+  );
+  expect(fetch.mock.calls.length).toEqual(2);
+});
+
+test("Test tambah subkategori form error", async () => {
+  fetch.mockResponseOnce([JSON.stringify([{}]), { status: 404 }]);
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <TambahSubkategori />
+    </AuthContext.Provider>
+  );
+  await waitFor(() => getByTestId("form-subcategory"));
+  const formCategory = getByTestId("form-subcategory");
+  expect(fetch.mock.calls.length).toEqual(1);
+  expect(formCategory.textContent).toContain(
+    "Error loading form !, Please relogin.."
+  );
+});
diff --git a/src/__test__/table.test.js b/src/__test__/table.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..d69bcc599db91bab7caaa3cc8b956fb4109e45d3
--- /dev/null
+++ b/src/__test__/table.test.js
@@ -0,0 +1,122 @@
+import React from "react";
+import { act, cleanup, fireEvent, render } from "@testing-library/react";
+import TableComponent from "../component/TableComponent";
+import AuthContext from "../utils/contex";
+
+beforeEach(() => {
+  fetch.resetMocks();
+});
+afterEach(cleanup);
+
+test("use search bar", async () => {
+  const data = {
+    url: `${process.env.REACT_APP_BASE_URL}/users/`,
+    pageDefault: 1,
+    searchDefault: "",
+    title: "Pengguna",
+    keyValuePairs: [
+      ["id", "id"],
+      ["full_name", "Nama Lengkap"],
+      ["username", "Username"],
+      ["dummy", "dummy"],
+      ["dummy", "dummy"],
+    ],
+    link: "/pengguna/details/",
+  };
+  fetch.mockResponseOnce(
+    JSON.stringify({
+      count: 6,
+      next: null,
+      previous: null,
+      results: [
+        {
+          id: "45897cc5-968c-44cf-931d-e646b095fcaf",
+          username: "admin-staging",
+          full_name: "",
+          phone_number: "",
+          address: "",
+          neighborhood: "",
+          hamlet: "",
+          urban_village: "",
+          sub_district: "",
+          profile_picture: null,
+        },
+        {
+          id: "663392ac-1dd6-462b-9301-a19c1287cefd",
+          username: "dummyuser",
+          full_name: "Dummy User",
+          phone_number: "+6285212345678",
+          address: "Jl. Dummy No.1",
+          neighborhood: "000",
+          hamlet: "000",
+          urban_village: "Dummy Urban Village",
+          sub_district: "Dummy Sub-District",
+          profile_picture: null,
+        },
+        {
+          id: "eafaa6d5-cc28-42bd-8fa3-e6eeecbfb2e0",
+          username: "mpit",
+          full_name: "annisaa fitri shabrina",
+          phone_number: "+6282190772106",
+          address: "jalan taman ayun vi",
+          neighborhood: "004",
+          hamlet: "016",
+          urban_village: "pengglingan",
+          sub_district: "kecamatan",
+          profile_picture: null,
+        },
+        {
+          id: "50d94bea-c164-4722-b5c0-cc3fda9b6e9c",
+          username: "pidyo",
+          full_name: "pidypidy",
+          phone_number: "+628219864572",
+          address: "buaran",
+          neighborhood: "004",
+          hamlet: "006",
+          urban_village: "michael",
+          sub_district: "halim",
+          profile_picture: null,
+        },
+        {
+          id: "d4b98bb5-8ba4-4a41-af10-93abcf53df58",
+          username: "whtestest",
+          full_name: "Michael Wiryadinata",
+          phone_number: "+628192090199",
+          address: "Ada deh 123",
+          neighborhood: "001",
+          hamlet: "002",
+          urban_village: "ada deh",
+          sub_district: "mau tau",
+          profile_picture:
+            "https://industripilar-api-staging.s3.amazonaws.com/media/uploads/profile/cf3456e1-e4b2-4da9-a6c6-35c3a3a47cbc6842089823804677314.jpg",
+        },
+        {
+          id: "1bcf4409-4894-4f80-97af-b9f9ce47f01e",
+          username: "whtestest2",
+          full_name: "michaeleh",
+          phone_number: "+628192090198",
+          address: "zjsjsn",
+          neighborhood: "012",
+          hamlet: "003",
+          urban_village: "nsjsns",
+          sub_district: "bhjsns",
+          profile_picture: null,
+        },
+      ],
+    })
+  );
+  const { getByPlaceholderText, getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}>
+      <TableComponent {...data} />
+    </AuthContext.Provider>
+  );
+  const search = getByPlaceholderText("Search...");
+  await act(async () => {
+    await fireEvent.input(search, { target: { value: "Dummy User" } });
+  });
+  await act(async () => {
+    await fireEvent.submit(getByPlaceholderText("Search..."));
+  });
+  const table = getByTestId("table");
+  expect(table.textContent).toContain("Dummy User");
+});
diff --git a/src/__test__/transaksi/DetailTransaksi.test.js b/src/__test__/transaksi/DetailTransaksi.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..2a04ca61bcb165aba192b1a5945b1e75011a3a47
--- /dev/null
+++ b/src/__test__/transaksi/DetailTransaksi.test.js
@@ -0,0 +1,195 @@
+import { act, cleanup, fireEvent, render } from "@testing-library/react";
+import AuthContext from "../../utils/contex";
+import React from "react";
+import DetailTransaksi from "../../page/transaksi/DetailTransaksi";
+import { waitFor } from "@testing-library/dom";
+
+beforeEach(() => {
+  fetch.resetMocks();
+});
+afterEach(cleanup);
+
+test("Test detail loaded TRANSFER", async () => {
+  fetch.mockResponseOnce(
+    JSON.stringify({
+      id: "0c1db3b2-48f0-4604-83ca-e8f16e8550ab",
+      transaction_number: "H793P5ZK",
+      user: "d4b98bb5-8ba4-4a41-af10-93abcf53df58",
+      user_username: "whtestest",
+      user_full_name: "Michael Wiryadinata halim",
+      user_phone_number: "+628192090199",
+      shipping_address: "ada deh test",
+      shipping_neighborhood: "002",
+      shipping_hamlet: "002",
+      shipping_urban_village: "penggilingan",
+      shipping_sub_district: "Dummy Sub-District",
+      transaction_items: [
+        {
+          id: "37f0298c-2e8a-4ab0-a094-e9177ddc60c4",
+          product: "3d403cd3-e356-4c15-9a86-8843333e2778",
+          product_code: "5VK6TY",
+          product_name: "produk barang",
+          product_price: "50000.00",
+          quantity: 7,
+        },
+      ],
+      transaction_item_subtotal: "350000.00",
+      shipping_costs: "15000.00",
+      payment_method: "TRF",
+      readable_payment_method: "Transfer",
+      donation: "5000.00",
+      transaction_status: "002",
+      readable_transaction_status: "Waiting for seller confirmation",
+      proof_of_payment: "a",
+      user_bank_account_name: "test",
+      user_bank_account_number: "1232131241321",
+      created_at: "2020-04-18T10:59:42.074386+07:00",
+      updated_at: "2020-04-18T11:00:18.150633+07:00",
+      subtotal: "370000.00",
+    })
+  );
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "tester" } }}>
+      <DetailTransaksi idTransaksi={"0c1db3b2-48f0-4604-83ca-e8f16e8550ab"} />
+    </AuthContext.Provider>
+  );
+  const wait = getByTestId("waiting-detail-transaksi");
+  expect(wait.textContent).toContain("Fetching data..");
+  await waitFor(() => getByTestId("page-detail-transaksi"));
+  const data = getByTestId("page-detail-transaksi");
+  expect(data.textContent).toContain("Transfer");
+  await act(async () => {
+    await fireEvent.click(getByTestId("button-see-proof"));
+  });
+  const close = getByTestId("button-close-proof");
+  expect(close.textContent).toContain("Close");
+  await act(async () => {
+    await fireEvent.click(close);
+  });
+  const dropdown = getByTestId("dropdown-status");
+  expect(dropdown.children.length).toEqual(6);
+});
+
+test("Test detail loaded COD", async () => {
+  fetch.mockResponseOnce(
+    JSON.stringify({
+      id: "0c1db3b2-48f0-4604-83ca-e8f16e8550ab",
+      transaction_number: "H793P5ZK",
+      user: "d4b98bb5-8ba4-4a41-af10-93abcf53df58",
+      user_username: "whtestest",
+      user_full_name: "Michael Wiryadinata halim",
+      user_phone_number: "+628192090199",
+      shipping_address: "ada deh test",
+      shipping_neighborhood: "002",
+      shipping_hamlet: "002",
+      shipping_urban_village: "penggilingan",
+      shipping_sub_district: "Dummy Sub-District",
+      transaction_items: [
+        {
+          id: "37f0298c-2e8a-4ab0-a094-e9177ddc60c4",
+          product: "3d403cd3-e356-4c15-9a86-8843333e2778",
+          product_code: "5VK6TY",
+          product_name: "produk barang",
+          product_price: "50000.00",
+          quantity: 7,
+        },
+      ],
+      transaction_item_subtotal: "350000.00",
+      shipping_costs: "15000.00",
+      payment_method: "COD",
+      readable_payment_method: "Pembayaran di tempat",
+      donation: "5000.00",
+      transaction_status: "002",
+      readable_transaction_status: "Waiting for seller confirmation",
+      proof_of_payment: "a",
+      user_bank_account_name: "test",
+      user_bank_account_number: "1232131241321",
+      created_at: "2020-04-18T10:59:42.074386+07:00",
+      updated_at: "2020-04-18T11:00:18.150633+07:00",
+      subtotal: "370000.00",
+    })
+  );
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "tester" } }}>
+      <DetailTransaksi idTransaksi={"0c1db3b2-48f0-4604-83ca-e8f16e8550ab"} />
+    </AuthContext.Provider>
+  );
+  const wait = getByTestId("waiting-detail-transaksi");
+  expect(wait.textContent).toContain("Fetching data..");
+  await waitFor(() => getByTestId("page-detail-transaksi"));
+  const data = getByTestId("page-detail-transaksi");
+  expect(data.textContent).toContain("Cash On Delivery");
+  const dropdown = getByTestId("dropdown-status");
+  expect(dropdown.children.length).toEqual(5);
+});
+
+test("Test detail loaded error", async () => {
+  fetch.mockResponseOnce(JSON.stringify({}), { status: 400 });
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "tester" } }}>
+      <DetailTransaksi idTransaksi={"0c1db3b2-48f0-4604-83ca-e8f16e8550ab"} />
+    </AuthContext.Provider>
+  );
+  const wait = getByTestId("waiting-detail-transaksi");
+  await waitFor(() => wait);
+  expect(wait.textContent).toContain("Something error");
+});
+
+test("Test detail loaded submit", async () => {
+  fetch
+    .once(
+      JSON.stringify({
+        id: "0c1db3b2-48f0-4604-83ca-e8f16e8550ab",
+        transaction_number: "H793P5ZK",
+        user: "d4b98bb5-8ba4-4a41-af10-93abcf53df58",
+        user_username: "whtestest",
+        user_full_name: "Michael Wiryadinata halim",
+        user_phone_number: "+628192090199",
+        shipping_address: "ada deh test",
+        shipping_neighborhood: "002",
+        shipping_hamlet: "002",
+        shipping_urban_village: "penggilingan",
+        shipping_sub_district: "Dummy Sub-District",
+        transaction_items: [
+          {
+            id: "37f0298c-2e8a-4ab0-a094-e9177ddc60c4",
+            product: "3d403cd3-e356-4c15-9a86-8843333e2778",
+            product_code: "5VK6TY",
+            product_name: "produk barang",
+            product_price: "50000.00",
+            quantity: 7,
+          },
+        ],
+        transaction_item_subtotal: "350000.00",
+        shipping_costs: "15000.00",
+        payment_method: "TRF",
+        readable_payment_method: "Transfer",
+        donation: "5000.00",
+        transaction_status: "002",
+        readable_transaction_status: "Waiting for seller confirmation",
+        proof_of_payment: "a",
+        user_bank_account_name: "test",
+        user_bank_account_number: "1232131241321",
+        created_at: "2020-04-18T10:59:42.074386+07:00",
+        updated_at: "2020-04-18T11:00:18.150633+07:00",
+        subtotal: "370000.00",
+      })
+    )
+    .once(JSON.stringify({}), { statusCode: 204 });
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "tester" } }}>
+      <DetailTransaksi idTransaksi={"0c1db3b2-48f0-4604-83ca-e8f16e8550ab"} />
+    </AuthContext.Provider>
+  );
+  const wait = getByTestId("waiting-detail-transaksi");
+  expect(wait.textContent).toContain("Fetching data..");
+  await waitFor(() => getByTestId("page-detail-transaksi"));
+  const data = getByTestId("page-detail-transaksi");
+  expect(data.textContent).toContain("Transfer");
+  const dropdown = getByTestId("dropdown-status");
+  expect(dropdown.children.length).toEqual(6);
+  await act(async () => {
+    await fireEvent.click(getByTestId("button-submit-status"));
+  });
+  expect(fetch.mock.calls.length).toEqual(2);
+});
diff --git a/src/__test__/transaksi/ListTransaksi.test.js b/src/__test__/transaksi/ListTransaksi.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..f9317b6e299407512fc5bbff8cbf7dfc33103ed1
--- /dev/null
+++ b/src/__test__/transaksi/ListTransaksi.test.js
@@ -0,0 +1,187 @@
+import { act, cleanup, fireEvent, render } from "@testing-library/react";
+import AuthContext from "../../utils/contex";
+import React from "react";
+import ListTransaksi from "../../page/transaksi/ListTransaksi";
+import { waitFor } from "@testing-library/dom";
+
+beforeEach(() => {
+  fetch.resetMocks();
+});
+afterEach(cleanup);
+
+test(" Test List transaksi", async () => {
+  fetch.mockResponseOnce(
+    JSON.stringify({
+      count: 17,
+      next: "https://industripilar-staging.herokuapp.com/transactions/?page=2",
+      previous: null,
+      results: [
+        {
+          id: "0c1db3b2-48f0-4604-83ca-e8f16e8550ab",
+          transaction_number: "H793P5ZK",
+          user: "d4b98bb5-8ba4-4a41-af10-93abcf53df58",
+          user_username: "whtestest",
+          user_full_name: "Michael Wiryadinata halim",
+          user_phone_number: "+628192090199",
+          shipping_address: "ada deh test",
+          shipping_neighborhood: "002",
+          shipping_hamlet: "002",
+          shipping_urban_village: "penggilingan",
+          shipping_sub_district: "Dummy Sub-District",
+          transaction_items: [
+            {
+              id: "37f0298c-2e8a-4ab0-a094-e9177ddc60c4",
+              product: "3d403cd3-e356-4c15-9a86-8843333e2778",
+              product_code: "5VK6TY",
+              product_name: "produk barang",
+              product_price: "50000.00",
+              quantity: 7,
+            },
+          ],
+          transaction_item_subtotal: "350000.00",
+          shipping_costs: "15000.00",
+          payment_method: "TRF",
+          readable_payment_method: "Transfer",
+          donation: "5000.00",
+          transaction_status: "002",
+          readable_transaction_status: "Waiting for seller confirmation",
+          proof_of_payment: null,
+          user_bank_account_name: "test",
+          user_bank_account_number: "1232131241321",
+          created_at: "2020-04-18T10:59:42.074386+07:00",
+          updated_at: "2020-04-18T11:00:18.150633+07:00",
+          subtotal: "370000.00",
+        },
+      ],
+    })
+  );
+  const { getByTestId } = render(
+    <AuthContext.Provider value={{ profile: { token: "tester" } }}>
+      <ListTransaksi />
+    </AuthContext.Provider>
+  );
+  await waitFor(() => getByTestId("tableList"));
+  const data = getByTestId("tableList");
+  expect(data.textContent).toContain("whtestest");
+  expect(data.textContent).toContain("Rp370.000");
+  expect(data.textContent).toContain("18 April 2020 10:59");
+  expect(fetch.mock.calls.length).toEqual(1);
+});
+
+test(" Test List transaksi filter", async () => {
+  fetch
+    .once(
+      JSON.stringify({
+        count: 17,
+        next:
+          "https://industripilar-staging.herokuapp.com/transactions/?page=2",
+        previous: null,
+        results: [
+          {
+            id: "0c1db3b2-48f0-4604-83ca-e8f16e8550ab",
+            transaction_number: "H793P5ZK",
+            user: "d4b98bb5-8ba4-4a41-af10-93abcf53df58",
+            user_username: "whtestest",
+            user_full_name: "Michael Wiryadinata halim",
+            user_phone_number: "+628192090199",
+            shipping_address: "ada deh test",
+            shipping_neighborhood: "002",
+            shipping_hamlet: "002",
+            shipping_urban_village: "penggilingan",
+            shipping_sub_district: "Dummy Sub-District",
+            transaction_items: [
+              {
+                id: "37f0298c-2e8a-4ab0-a094-e9177ddc60c4",
+                product: "3d403cd3-e356-4c15-9a86-8843333e2778",
+                product_code: "5VK6TY",
+                product_name: "produk barang",
+                product_price: "50000.00",
+                quantity: 7,
+              },
+            ],
+            transaction_item_subtotal: "350000.00",
+            shipping_costs: "15000.00",
+            payment_method: "TRF",
+            readable_payment_method: "Transfer",
+            donation: "5000.00",
+            transaction_status: "002",
+            readable_transaction_status: "Waiting for seller confirmation",
+            proof_of_payment: null,
+            user_bank_account_name: "test",
+            user_bank_account_number: "1232131241321",
+            created_at: "2020-04-18T10:59:42.074386+07:00",
+            updated_at: "2020-04-18T11:00:18.150633+07:00",
+            subtotal: "370000.00",
+          },
+        ],
+      })
+    )
+    .once(
+      JSON.stringify({
+        count: 17,
+        next:
+          "https://industripilar-staging.herokuapp.com/transactions/?page=2",
+        previous: null,
+        results: [
+          {
+            id: "0c1db3b2-48f0-4604-83ca-e8f16e8550ab",
+            transaction_number: "H793P5ZK",
+            user: "d4b98bb5-8ba4-4a41-af10-93abcf53df58",
+            user_username: "filterwhtest",
+            user_full_name: "Michael Wiryadinata halim",
+            user_phone_number: "+628192090199",
+            shipping_address: "ada deh test",
+            shipping_neighborhood: "002",
+            shipping_hamlet: "002",
+            shipping_urban_village: "penggilingan",
+            shipping_sub_district: "Dummy Sub-District",
+            transaction_items: [
+              {
+                id: "37f0298c-2e8a-4ab0-a094-e9177ddc60c4",
+                product: "3d403cd3-e356-4c15-9a86-8843333e2778",
+                product_code: "5VK6TY",
+                product_name: "produk barang",
+                product_price: "50000.00",
+                quantity: 7,
+              },
+            ],
+            transaction_item_subtotal: "350000.00",
+            shipping_costs: "15000.00",
+            payment_method: "TRF",
+            readable_payment_method: "Transfer",
+            donation: "5000.00",
+            transaction_status: "002",
+            readable_transaction_status: "Waiting for seller confirmation",
+            proof_of_payment: null,
+            user_bank_account_name: "test",
+            user_bank_account_number: "1232131241321",
+            created_at: "2020-04-18T10:59:42.074386+07:00",
+            updated_at: "2020-04-18T11:00:18.150633+07:00",
+            subtotal: "370000.00",
+          },
+        ],
+      })
+    );
+  const { getByTestId, getByLabelText } = render(
+    <AuthContext.Provider value={{ profile: { token: "tester" } }}>
+      <ListTransaksi />
+    </AuthContext.Provider>
+  );
+  await waitFor(() => getByTestId("tableList"));
+  const data = getByTestId("tableList");
+  expect(data.textContent).toContain("whtestest");
+  expect(data.textContent).toContain("Rp370.000");
+  expect(data.textContent).toContain("18 April 2020 10:59");
+  expect(fetch.mock.calls.length).toEqual(1);
+  await act(async () => {
+    await fireEvent.click(getByTestId("filter-button"));
+  });
+  await act(async () => {
+    await fireEvent.input(
+      getByLabelText("Status transaksi:", { target: { value: "002" } })
+    );
+    await fireEvent.click(getByTestId("submit-filter"));
+  });
+  expect(data.textContent).toContain("filterwhtest");
+  expect(fetch.mock.calls.length).toEqual(2);
+});
diff --git a/src/component/LinkSidebar.jsx b/src/component/LinkSidebar.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..50dc2d8e6d388b94e5e366d66c30d330d482635f
--- /dev/null
+++ b/src/component/LinkSidebar.jsx
@@ -0,0 +1,40 @@
+import React from "react";
+import { css } from "@emotion/core";
+import { Link, Location } from "@reach/router";
+import styled from "@emotion/styled";
+import { Center } from "./html/html";
+
+const LinkSidebar = (props) => (
+  <Location>
+    {({ location }) => {
+      const isActive = location.pathname === props.to;
+      const divColor = isActive ? "#3C8DBC" : "transparent";
+      const textColor = isActive ? "white" : "black";
+      return (
+        <Center
+          css={css`
+            background-color: ${divColor};
+            width: 100%;
+            text-align: center;
+            height: 2.5rem;
+            color: ${textColor};
+          `}
+        >
+          <StyledLink {...props} />
+        </Center>
+      );
+    }}
+  </Location>
+);
+
+export default LinkSidebar;
+
+const StyledLink = styled(Link)`
+  color: inherit;
+  text-decoration: none;
+  font-style: normal;
+  font-weight: normal;
+  font-size: 14px;
+  line-height: 17px;
+  letter-spacing: 0.2em;
+`;
diff --git a/src/component/LinkYellow.jsx b/src/component/LinkYellow.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..576ebd3288315f995203a2f3a5590b79c6940673
--- /dev/null
+++ b/src/component/LinkYellow.jsx
@@ -0,0 +1,38 @@
+import React from "react";
+import { css } from "@emotion/core";
+import { Link, Location } from "@reach/router";
+
+const LinkYellow = (props) => (
+  <Location>
+    {({ location }) => {
+      const isActive = location.pathname === props.to;
+      const divColor = isActive ? "transparent" : "#FFC80A";
+      const textColor = isActive ? "grey" : "black";
+      return (
+        <Link
+          {...props}
+          css={css`
+            display: flex;
+            border: 3px solid ${divColor};
+            width: 100%;
+            text-align: center;
+            justify-content: center;
+            align-items: center;
+            height: 2rem;
+            color: ${textColor};
+            box-sizing: border-box;
+            border-radius: 50px;
+            text-decoration: none;
+            font-style: normal;
+            font-weight: normal;
+            font-size: 14px;
+            line-height: 17px;
+            letter-spacing: 0.2em;
+          `}
+        />
+      );
+    }}
+  </Location>
+);
+
+export default LinkYellow;
diff --git a/src/component/Loader.jsx b/src/component/Loader.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..bebc58f06145cf3c8d657b7840f181d209dd8e4d
--- /dev/null
+++ b/src/component/Loader.jsx
@@ -0,0 +1,21 @@
+import React from "react";
+import { usePromiseTracker } from "react-promise-tracker";
+import { Backdrop, CircularProgress } from "@material-ui/core";
+import { makeStyles } from "@material-ui/core/styles";
+const useStyles = makeStyles((theme) => ({
+  backdrop: {
+    zIndex: theme.zIndex.drawer + 1,
+    color: "#6200EE",
+  },
+}));
+const Loader = () => {
+  const classes = useStyles();
+  const { promiseInProgress } = usePromiseTracker();
+  return (
+    <Backdrop className={classes.backdrop} open={promiseInProgress}>
+      <CircularProgress color="inherit" />
+    </Backdrop>
+  );
+};
+
+export default Loader;
diff --git a/src/component/Logo.jsx b/src/component/Logo.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..af4eb52952a034ee1ed01996be975d3169283dbe
--- /dev/null
+++ b/src/component/Logo.jsx
@@ -0,0 +1,24 @@
+import React from "react";
+import { css } from "@emotion/core";
+
+const Logo = () => {
+  return (
+    <div
+      css={css`
+        display: flex;
+        background-color: black;
+        color: white;
+        align-items: center;
+        justify-content: center;
+        width: 10rem;
+        height: 2.5rem;
+        border-radius: 3px;
+        font-size: 18px;
+      `}
+    >
+      HOME INDUSTRY
+    </div>
+  );
+};
+
+export default Logo;
diff --git a/src/component/Sidebar.jsx b/src/component/Sidebar.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..4c9841d9bcbcdedb5405508ca5fc6b59e69f425f
--- /dev/null
+++ b/src/component/Sidebar.jsx
@@ -0,0 +1,90 @@
+import React from "react";
+import { css } from "@emotion/core";
+import { Center } from "./html/html";
+import Logo from "./Logo";
+import LinkSidebar from "./LinkSidebar";
+import { useAuthContext } from "../utils/contex";
+import styled from "@emotion/styled";
+
+const logout = (token) => {
+  fetch(`${process.env.REACT_APP_BASE_URL}/auth/logout/`, {
+    method: "POST",
+    headers: {
+      Authorization: `Token ${token}`,
+    },
+  });
+};
+
+const Sidebar = () => {
+  const { handleLogout, profile } = useAuthContext();
+  return (
+    <Center
+      css={css`
+        height: 100%;
+        border: 1px solid #e0e0e0;
+        box-sizing: border-box;
+        box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+        background-color: white;
+        align-items: center;
+        justify-content: stretch;
+      `}
+    >
+      <div
+        css={css`
+          margin-top: 12vh;
+        `}
+      />
+      <Logo />
+      <div
+        css={css`
+          margin-top: 2rem;
+          font-style: normal;
+          font-weight: 600;
+          font-size: 24px;
+          line-height: 29px;
+          text-align: center;
+          letter-spacing: 0.2em;
+          margin-bottom: 4rem;
+        `}
+      >
+        ADMIN DASHBOARD
+      </div>
+      <LinkSidebar to="/produk">PRODUK</LinkSidebar>
+      <LinkSidebar to="/transaksi">TRANSAKSI</LinkSidebar>
+      <LinkSidebar to="/program">PROGRAM</LinkSidebar>
+      <LinkSidebar to="/product1">DONASI</LinkSidebar>
+      <LinkSidebar to="/pengguna">PENGGUNA</LinkSidebar>
+      <Center
+        css={css`
+          width: 100%;
+          text-align: center;
+          height: 2.5rem;
+        `}
+      >
+        <StyledA
+          data-testid="logout"
+          onClick={() => {
+            logout(profile.token.repeat(1));
+            handleLogout();
+          }}
+        >
+          LOGOUT
+        </StyledA>
+      </Center>
+    </Center>
+  );
+};
+
+export default Sidebar;
+
+const StyledA = styled.a`
+  text-decoration: none;
+  font-style: normal;
+  font-weight: normal;
+  font-size: 14px;
+  line-height: 17px;
+  letter-spacing: 0.2em;
+  text-align: center;
+  color: #3c8dbc;
+  cursor: pointer;
+`;
diff --git a/src/component/Status.jsx b/src/component/Status.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..09f32d11cc9c8066576fff08cb0b9391d4bc59ed
--- /dev/null
+++ b/src/component/Status.jsx
@@ -0,0 +1,42 @@
+import React from "react";
+import { css } from "@emotion/core";
+const Status = ({ status, label }) => {
+  let color;
+  switch (status) {
+    case "001":
+      color = "#EAC435";
+      break;
+    case "002":
+      color = "#FB4D3D";
+      break;
+    case "003":
+      color = "#FC766AFF";
+      break;
+    case "004":
+      color = "#03CEA4";
+      break;
+    case "005":
+      color = "#27496d";
+      break;
+    case "006":
+      color = "#CA1551";
+      break;
+    case "007":
+      color = "#CA1551";
+      break;
+    default:
+      color = "black";
+      break;
+  }
+  return (
+    <div
+      css={css`
+        color: ${color};
+      `}
+    >
+      {label}
+    </div>
+  );
+};
+
+export default Status;
diff --git a/src/component/TableComponent.jsx b/src/component/TableComponent.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..86f97461ba8675126e31b222bc0e02d52262fc1b
--- /dev/null
+++ b/src/component/TableComponent.jsx
@@ -0,0 +1,302 @@
+import React, { useState } from "react";
+import useFetchList from "../utils/useFetchList";
+import { useForm } from "react-hook-form";
+import { css } from "@emotion/core";
+import Table from "@material-ui/core/Table";
+import TableBody from "@material-ui/core/TableBody";
+import TableCell from "@material-ui/core/TableCell";
+import TableContainer from "@material-ui/core/TableContainer";
+import Toolbar from "@material-ui/core/Toolbar";
+import TableFooter from "@material-ui/core/TableFooter";
+import TablePagination from "@material-ui/core/TablePagination";
+import TableRow from "@material-ui/core/TableRow";
+import Paper from "@material-ui/core/Paper";
+import FilterListIcon from "@material-ui/icons/FilterList";
+import Collapse from "@material-ui/core/Collapse";
+import {
+  ButtonSearch,
+  ErrorDiv,
+  InputForm,
+  InputSearch,
+  InputSelectForm,
+  InputSubmitForm,
+  LabelInput,
+  RowInput,
+} from "./html/html";
+import { Search } from "@material-ui/icons";
+import { Link } from "@reach/router";
+import ArrowDropDownCircleIcon from "@material-ui/icons/ArrowDropDownCircle";
+import TableHead from "@material-ui/core/TableHead";
+import Tooltip from "@material-ui/core/Tooltip";
+import IconButton from "@material-ui/core/IconButton";
+import Button from "@material-ui/core/Button";
+
+const TableComponent = ({
+  url,
+  pageDefault,
+  searchDefault,
+  title,
+  keyValuePairs,
+  link,
+  argument,
+  filter,
+}) => {
+  const [
+    results,
+    errorState,
+    count,
+    statePage,
+    stateSize,
+    setPage,
+    setStateSearch,
+    setPageSize,
+    setFilter,
+  ] = useFetchList([url, pageDefault, searchDefault, 5, argument, null]);
+  const { register, handleSubmit } = useForm();
+  const {
+    register: registerFilter,
+    handleSubmit: handleSubmitFilter,
+    reset: resetFilter,
+  } = useForm();
+  const [filterTab, setFilterTab] = useState(false);
+  return (
+    <div
+      css={css`
+        display: flex;
+        margin: 0 3rem 3rem 3rem;
+        flex-direction: column;
+      `}
+    >
+      {errorState && <ErrorDiv>ERROR !! Please relogin.</ErrorDiv>}
+      <div
+        data-testid="table"
+        css={css`
+          margin-top: 1.5rem;
+        `}
+      >
+        <Paper>
+          <Toolbar>
+            <div
+              data-testid="page"
+              css={css`
+                font-style: normal;
+                font-weight: normal;
+                font-size: 36px;
+                line-height: 44px;
+                color: #292929;
+              `}
+            >
+              {title}
+            </div>
+            <div
+              css={css`
+                width: 100%;
+                display: flex;
+                justify-content: flex-end;
+              `}
+            >
+              <form
+                css={css`
+                  display: flex;
+                `}
+                onSubmit={handleSubmit((data) => {
+                  setStateSearch(data["search"]);
+                })}
+              >
+                <InputSearch
+                  name="search"
+                  ref={register}
+                  placeholder="Search..."
+                />
+                <ButtonSearch className="border-0">
+                  <Search />
+                </ButtonSearch>
+              </form>
+              {filter !== null && filter !== undefined && (
+                <Tooltip title="Filter list">
+                  <IconButton
+                    data-testid="filter-button"
+                    aria-label="filter list"
+                    onClick={() => setFilterTab(!filterTab)}
+                  >
+                    <FilterListIcon />
+                  </IconButton>
+                </Tooltip>
+              )}
+            </div>
+          </Toolbar>
+          {filter !== null && filter !== undefined && (
+            <Collapse in={filterTab} addEndListener={() => {}}>
+              <Toolbar>
+                <form
+                  data-testid="filter-form"
+                  onSubmit={handleSubmitFilter((data) => {
+                    const filter = Object.entries(data)
+                      .filter(([, val]) => val !== "")
+                      .map(([key, val]) => `${key}=${val}`)
+                      .join("&");
+                    setFilter(filter);
+                  })}
+                  css={css`
+                    width: 100%;
+                    display: flex;
+                    flex-direction: column;
+                  `}
+                >
+                  {filter.map((field) => {
+                    if (Array.isArray(field)) {
+                      return (
+                        <RowInput key={field[0]}>
+                          <LabelInput htmlFor={field[0]}>
+                            {field[1]}:
+                          </LabelInput>
+                          <InputForm
+                            id={field[0]}
+                            type="date"
+                            name={field[0]}
+                            ref={registerFilter}
+                          />
+                        </RowInput>
+                      );
+                    } else if (typeof field === "object") {
+                      const k = Object.keys(field);
+                      return (
+                        <RowInput key={k[0]}>
+                          <LabelInput htmlFor={k[0]}>
+                            {field[k].label}:
+                          </LabelInput>
+                          <InputSelectForm
+                            id={k[0]}
+                            name={k[0]}
+                            ref={registerFilter}
+                          >
+                            <option value="" />
+                            {field[k].choices.map((c) => {
+                              const choice = Object.keys(c);
+                              return (
+                                <option key={choice} value={choice}>
+                                  {c[choice]}
+                                </option>
+                              );
+                            })}
+                          </InputSelectForm>
+                        </RowInput>
+                      );
+                    }
+                  })}
+                  <div
+                    css={css`
+                      display: flex;
+                      width: 100%;
+                      align-items: center;
+                      align-content: space-between;
+                    `}
+                  >
+                    <InputSubmitForm
+                      css={css`
+                        width: 50%;
+                        margin-right: 2rem;
+                      `}
+                      data-testid="submit-filter"
+                      type="submit"
+                      value="Filter"
+                    />
+                    <Button
+                      variant="outlined"
+                      color="secondary"
+                      onClick={() => {
+                        resetFilter();
+                        setFilter(null);
+                      }}
+                    >
+                      Hapus Filter
+                    </Button>
+                  </div>
+                </form>
+              </Toolbar>
+            </Collapse>
+          )}
+          <TableContainer>
+            <Table>
+              <TableHead>
+                <TableRow>
+                  {keyValuePairs.slice(1).map((pairs, index) => {
+                    return (
+                      <TableCell
+                        key={`${pairs[0]}${index}`}
+                        css={css`
+                          font-weight: bold;
+                        `}
+                      >
+                        {pairs[1]}
+                      </TableCell>
+                    );
+                  })}
+                  {link !== undefined ? (
+                    <TableCell
+                      css={css`
+                        font-weight: bold;
+                      `}
+                    >
+                      Action
+                    </TableCell>
+                  ) : null}
+                </TableRow>
+              </TableHead>
+              <TableBody data-testid="tableList">
+                {results.map((u, indexR) => {
+                  return (
+                    <TableRow key={`row${indexR}-${u[keyValuePairs[0][0]]}`}>
+                      {keyValuePairs.slice(1).map((pairs, indexC) => {
+                        return (
+                          <TableCell key={`${pairs[0]}r${indexR}c${indexC}`}>
+                            {pairs[2] !== undefined && pairs[2] !== null
+                              ? pairs[0] !== ""
+                                ? pairs[2](u[pairs[0]])
+                                : pairs[2](u)
+                              : u[pairs[0]]}
+                          </TableCell>
+                        );
+                      })}
+                      {link !== undefined ? (
+                        <TableCell key={`link_r${indexR}`}>
+                          <Link to={`${link}${u.id}`}>
+                            <ArrowDropDownCircleIcon color="action" />
+                          </Link>
+                        </TableCell>
+                      ) : null}
+                    </TableRow>
+                  );
+                })}
+              </TableBody>
+              <TableFooter>
+                <TableRow>
+                  <TablePagination
+                    rowsPerPageOptions={[5, 7, 10]}
+                    count={count || 0}
+                    rowsPerPage={stateSize}
+                    page={statePage - 1}
+                    SelectProps={{
+                      inputProps: { "aria-label": "rows per page" },
+                      native: true,
+                    }}
+                    onChangePage={(event, newPage) => {
+                      setPage(newPage + 1);
+                    }}
+                    onChangeRowsPerPage={(event) => {
+                      setPageSize(parseInt(event.target.value, 10));
+                      setPage(1);
+                    }}
+                  />
+                </TableRow>
+              </TableFooter>
+            </Table>
+          </TableContainer>
+        </Paper>
+      </div>
+      {/*<Paginator />*/}
+    </div>
+  );
+};
+
+export default TableComponent;
diff --git a/src/component/TableUtils.jsx b/src/component/TableUtils.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..a12dcdbc62fdd3e879b49d0acdc8b8b3395abfda
--- /dev/null
+++ b/src/component/TableUtils.jsx
@@ -0,0 +1,30 @@
+import React from "react";
+import Moment from "react-moment";
+import NumberFormat from "react-number-format";
+import "moment-timezone";
+import Status from "./Status";
+export const stringToDate = (date) => {
+  const dateFormat = new Date(date);
+  return (
+    <Moment format={"DD MMMM YYYY HH:mm"} tz="Asia/Jakarta" date={dateFormat} />
+  );
+};
+
+export const stringToCurrency = (currency) => (
+  <NumberFormat
+    value={currency}
+    isNumericString={true}
+    displayType={"text"}
+    thousandSeparator="."
+    decimalSeparator=","
+    decimalScale={0}
+    prefix={"Rp"}
+  />
+);
+
+export const transactionToColoredStatus = (transaction) => (
+  <Status
+    status={transaction.transaction_status}
+    label={transaction.readable_transaction_status}
+  />
+);
diff --git a/src/component/html/html.js b/src/component/html/html.js
new file mode 100644
index 0000000000000000000000000000000000000000..446dc1cccb61ac6d939eb17c77e3b010c1586a7c
--- /dev/null
+++ b/src/component/html/html.js
@@ -0,0 +1,118 @@
+import styled from "@emotion/styled";
+
+export const Center = styled.div`
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+`;
+
+export const InputSearch = styled.input`
+  background: #ffffff;
+  border: 1px solid #e0e1e2;
+  box-sizing: border-box;
+  border-radius: 4px;
+  width: 15rem;
+  height: 2.3rem;
+  font-size: 1rem;
+  padding-left: 1rem;
+`;
+
+export const ButtonSearch = styled.button`
+  border: 1px solid #e0e1e2;
+  border-left: none; /* Prevent double borders */
+  cursor: pointer;
+  background-color: white;
+`;
+
+export const ErrorDiv = styled.div`
+  color: red;
+`;
+
+export const ButtonRed = styled.button`
+  border: 2.5px solid #cc4b4c;
+  background-color: white;
+  color: #cc4b4c;
+  border-radius: 50px;
+  width: 100%;
+  padding: 8px 24px;
+`;
+
+export const ButtonSubmit = styled.button`
+  border: 2.5px solid #3c8dbc;
+  background-color: #3c8dbc;
+  color: white;
+  border-radius: 50px;
+  width: 100%;
+  padding: 8px 24px;
+`;
+
+export const InputForm = styled.input`
+  flex-grow: 4;
+  border: 1px solid #e0e1e2;
+  box-sizing: border-box;
+  border-radius: 4px;
+  font-size: 1rem;
+  padding-left: 1rem;
+`;
+
+export const InputTextArea = styled.textarea`
+  flex-grow: 4;
+  border: 1px solid #e0e1e2;
+  box-sizing: border-box;
+  border-radius: 4px;
+  font-size: 1rem;
+  padding-left: 1rem;
+`;
+
+export const InputSelectForm = styled.select`
+  flex-grow: 4;
+  border: 1px solid #e0e1e2;
+  box-sizing: border-box;
+  border-radius: 4px;
+  font-size: 1rem;
+  padding-left: 1rem;
+`;
+
+export const RowInput = styled.div`
+  height: 2rem;
+  margin: 0.5rem;
+  display: flex;
+  align-content: space-between;
+`;
+
+export const LabelInput = styled.label`
+  width: 30%;
+  font-size: 18px;
+  line-height: 22px;
+`;
+
+export const InputSubmitForm = styled.input`
+  width: 100%;
+  background: #3c8dbc;
+  border: 1px solid #3c8dbc;
+  box-sizing: border-box;
+  border-radius: 50px;
+  font-size: 14px;
+  line-height: 17px;
+  text-align: center;
+  letter-spacing: 0.2em;
+  color: white;
+  height: 2.3rem;
+`;
+
+export const ButtonDeleteStyled = styled.button`
+  border: 4px solid #cc4b4c;
+  box-sizing: border-box;
+  border-radius: 50px;
+  color: #cc4b4c;
+  width: 100%;
+  height: 2rem;
+  margin-left: 0.5rem;
+  cursor: pointer;
+  background-color: white;
+  font-size: 14px;
+  :disabled {
+    opacity: 30%;
+  }
+`;
diff --git a/src/component/routes/ProtectedRoute.jsx b/src/component/routes/ProtectedRoute.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..595547b43ee9d29ec54f659c411e6555c66c075f
--- /dev/null
+++ b/src/component/routes/ProtectedRoute.jsx
@@ -0,0 +1,16 @@
+import React from "react";
+import { useAuthContext } from "../../utils/contex";
+import { Redirect } from "@reach/router";
+import Layout from "../../layout/Layout";
+
+const ProtectedRoute = (props) => {
+  const { is_authenticated } = useAuthContext();
+
+  return is_authenticated ? (
+    <Layout component={props} />
+  ) : (
+    <Redirect from="" to="/" noThrow />
+  );
+};
+
+export default ProtectedRoute;
diff --git a/src/component/routes/UnauthenticatedRoute.jsx b/src/component/routes/UnauthenticatedRoute.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..2fedfb4cfdf79002a95a8f5a45db013f3a2000b6
--- /dev/null
+++ b/src/component/routes/UnauthenticatedRoute.jsx
@@ -0,0 +1,15 @@
+import { useAuthContext } from "../../utils/contex";
+import { Redirect } from "@reach/router";
+import React from "react";
+
+const UnauthenticatedRoute = ({ component: Component, ...rest }) => {
+  const { is_authenticated } = useAuthContext();
+
+  return !is_authenticated ? (
+    <Component {...rest} />
+  ) : (
+    <Redirect from="" to="/produk" noThrow />
+  );
+};
+
+export default UnauthenticatedRoute;
diff --git a/src/index.css b/src/index.css
index ec2585e8c0bb8188184ed1e0703c4c8f2a8419b0..15320741a80ebb181131e23f6d0be4ac3989d6c0 100644
--- a/src/index.css
+++ b/src/index.css
@@ -6,8 +6,3 @@ body {
   -webkit-font-smoothing: antialiased;
   -moz-osx-font-smoothing: grayscale;
 }
-
-code {
-  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
-    monospace;
-}
diff --git a/src/layout/Layout.jsx b/src/layout/Layout.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..85851d57b240cfdfd9d83f3866a907bc8583ea63
--- /dev/null
+++ b/src/layout/Layout.jsx
@@ -0,0 +1,76 @@
+import React from "react";
+import { css } from "@emotion/core";
+import Sidebar from "../component/Sidebar";
+import { Center } from "../component/html/html";
+
+const Layout = (props) => {
+  const { component: Component, ...rest } = props["component"];
+  return (
+    <div
+      css={css`
+        display: flex;
+        height: 100vh;
+      `}
+    >
+      <div
+        css={css`
+          width: 60vh;
+        `}
+      >
+        <Sidebar />
+      </div>
+      <Center
+        css={css`
+          width: 100%;
+          height: 100%;
+        `}
+      >
+        <div
+          css={css`
+            background-color: white;
+            width: 90%;
+            height: 85%;
+            border: 1px solid #e0e0e0;
+            box-sizing: border-box;
+            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+            overflow: scroll;
+          `}
+        >
+          <Component {...rest} />
+        </div>
+      </Center>
+      <div
+        css={css`
+          background-color: black;
+          position: absolute;
+          bottom: 0;
+          left: 0;
+          right: 0;
+          height: 5px;
+        `}
+      />
+      <div
+        css={css`
+          background-color: #75a9c7;
+          position: absolute;
+          bottom: 0;
+          left: 0;
+          right: 0;
+          height: 18px;
+        `}
+      />
+      <div
+        css={css`
+          background-color: #3c8dbc;
+          position: absolute;
+          bottom: 0;
+          left: 0;
+          right: 0;
+          height: 10px;
+        `}
+      />
+    </div>
+  );
+};
+
+export default Layout;
diff --git a/src/logo.svg b/src/logo.svg
deleted file mode 100644
index 6b60c1042f58d9fabb75485aa3624dddcf633b5c..0000000000000000000000000000000000000000
--- a/src/logo.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
-    <g fill="#61DAFB">
-        <path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
-        <circle cx="420.9" cy="296.5" r="45.7"/>
-        <path d="M520.5 78.1z"/>
-    </g>
-</svg>
diff --git a/src/page/kategori/DetailKategori.jsx b/src/page/kategori/DetailKategori.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..19edb2411b7ee663fdb24281f46ccfc4e1a7fef7
--- /dev/null
+++ b/src/page/kategori/DetailKategori.jsx
@@ -0,0 +1,140 @@
+import React, { useState } from "react";
+import useFetchSingleData from "../../utils/useFetchSingleData";
+import { css } from "@emotion/core";
+import { ButtonDeleteStyled, ErrorDiv } from "../../component/html/html";
+import TableComponent from "../../component/TableComponent";
+import LinkYellow from "../../component/LinkYellow";
+import { navigate } from "@reach/router";
+import { ArrowBack } from "@material-ui/icons";
+import useDelete from "../../utils/useDelete";
+import {
+  Dialog,
+  DialogActions,
+  DialogContent,
+  DialogContentText,
+  DialogTitle,
+  Button,
+} from "@material-ui/core";
+
+const DetailKategori = ({ idKategori }) => {
+  const url = `${process.env.REACT_APP_BASE_URL}/categories/${idKategori}/`;
+  const [category, error] = useFetchSingleData(url);
+  const [deleteProduct, errorDelete] = useDelete(url);
+  const [openModal, setOpenModal] = useState(false);
+  const data = {
+    url: `${process.env.REACT_APP_BASE_URL}/products/`,
+    pageDefault: 1,
+    searchDefault: "",
+    title: "",
+    keyValuePairs: [
+      ["id", "id"],
+      ["name", "Nama"],
+      ["price", "Harga"],
+      ["stock", "Stok"],
+      ["subcategory_name", "Subcategory"],
+    ],
+    argument: `subcategory__category=${idKategori}`,
+  };
+  const handleClose = () => setOpenModal(false);
+  return (
+    <div
+      data-testid="page-detail-kategori"
+      css={css`
+        display: flex;
+        margin: 2rem 3rem 3rem 3rem;
+        flex-direction: column;
+      `}
+    >
+      <Dialog open={openModal} onClose={handleClose}>
+        <DialogTitle>{`Hapus kategori ${category.name}`}</DialogTitle>
+        <DialogContent>
+          <DialogContentText>
+            Apakah yakin untuk menghapus kategori {category.name}?
+          </DialogContentText>
+          <DialogActions>
+            <Button onClick={handleClose} color="primary">
+              TIDAK
+            </Button>
+            <Button
+              data-testid="button-delete-category"
+              color="secondary"
+              onClick={() => {
+                deleteProduct();
+                handleClose();
+              }}
+            >
+              HAPUS
+            </Button>
+          </DialogActions>
+        </DialogContent>
+      </Dialog>
+      {error && <ErrorDiv>Error !, Please relogin..</ErrorDiv>}
+      {errorDelete && (
+        <ErrorDiv>
+          Tidak dapat menghapus kategori, mohon periksa apakah ada produk
+          didalam kategori ini.
+        </ErrorDiv>
+      )}
+      <div
+        css={css`
+          display: flex;
+          flex-direction: column;
+        `}
+      >
+        <button
+          css={css`
+            align-self: start;
+            background-color: Transparent;
+            background-repeat: no-repeat;
+            border: none;
+            cursor: pointer;
+            overflow: hidden;
+            outline: none;
+          `}
+          onClick={() => navigate("/kategori")}
+        >
+          <ArrowBack fontSize="large" />
+        </button>
+        <div
+          css={css`
+            font-size: 2rem;
+            display: flex;
+            flex-direction: row;
+            align-items: baseline;
+            margin-bottom: 1rem;
+          `}
+        >
+          <div
+            css={css`
+              flex-grow: 2;
+            `}
+          >
+            Kategori {category.name}
+          </div>
+          <div
+            css={css`
+              flex-grow: 1;
+            `}
+          >
+            <LinkYellow to="ubah">EDIT</LinkYellow>
+          </div>
+          <div
+            css={css`
+              flex-grow: 1;
+            `}
+          >
+            <ButtonDeleteStyled
+              data-testid="button-delete-category-modal"
+              onClick={() => setOpenModal(true)}
+            >
+              HAPUS
+            </ButtonDeleteStyled>
+          </div>
+        </div>
+      </div>
+      <TableComponent {...data} />
+    </div>
+  );
+};
+
+export default DetailKategori;
diff --git a/src/page/kategori/EditKategori.jsx b/src/page/kategori/EditKategori.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..bd5a22627bbfed9853adeafff1edd749df63dbc3
--- /dev/null
+++ b/src/page/kategori/EditKategori.jsx
@@ -0,0 +1,93 @@
+import React from "react";
+import useFetchSingleData from "../../utils/useFetchSingleData";
+import { css } from "@emotion/core";
+import { ErrorDiv } from "../../component/html/html";
+import FormKategori from "./FormKategori";
+import { ArrowBack, ErrorOutline } from "@material-ui/icons";
+import { navigate } from "@reach/router";
+import useSendData from "../../utils/useSendData";
+
+const EditKategori = ({ idKategori }) => {
+  const url = `${process.env.REACT_APP_BASE_URL}/categories/${idKategori}/`;
+  const [initialData, errorState] = useFetchSingleData(url);
+  const [send, error] = useSendData({ url, method: "PATCH", redirect: -1 });
+  const onSubmit = (data) => {
+    const formData = new FormData();
+    formData.append("name", data["name"]);
+    if (data["image"].length !== 0) formData.append("image", data["image"][0]);
+    send(formData);
+  };
+  if (errorState || Object.keys(initialData).length === 0)
+    return (
+      <div
+        data-testid="waiting-edit-kategori"
+        css={css`
+          display: flex;
+          margin: 2rem 3rem 3rem 3rem;
+          flex-direction: column;
+          font-size: 25px;
+        `}
+      >
+        Fetching data..
+      </div>
+    );
+  return (
+    <div
+      data-testid="edit-kategori"
+      css={css`
+        display: flex;
+        margin: 2rem 3rem 3rem 3rem;
+        flex-direction: column;
+      `}
+    >
+      {error && <ErrorDiv>Error !, Data tidak dapat disimpan</ErrorDiv>}
+      <div
+        css={css`
+          display: flex;
+          flex-direction: row;
+        `}
+      >
+        <button
+          css={css`
+            background-color: Transparent;
+            background-repeat: no-repeat;
+            border: none;
+            cursor: pointer;
+            overflow: hidden;
+            outline: none;
+          `}
+          onClick={() => navigate(-1)}
+        >
+          <ArrowBack fontSize="large" />
+        </button>
+        <div
+          css={css`
+            font-size: 36px;
+          `}
+        >
+          Edit {initialData.name}
+        </div>
+      </div>
+      <div
+        css={css`
+          margin-top: 2.5rem;
+          display: flex;
+        `}
+      >
+        <ErrorOutline style={{ fontSize: 28, color: "FFC80A" }} />
+        <div
+          css={css`
+            font-weight: 600;
+            font-size: 24px;
+            line-height: 29px;
+          `}
+        >
+          Informasi Kategori
+        </div>
+      </div>
+      <FormKategori {...{ onSubmit, initialData }} />
+    </div>
+  );
+};
+
+export default EditKategori;
diff --git a/src/page/kategori/FormKategori.jsx b/src/page/kategori/FormKategori.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..58b15eef49f0c91355516a22339909ae74add6c9
--- /dev/null
+++ b/src/page/kategori/FormKategori.jsx
@@ -0,0 +1,55 @@
+import React from "react";
+import { useForm } from "react-hook-form";
+import {
+  ErrorDiv,
+  RowInput,
+  InputForm,
+  LabelInput,
+  InputSubmitForm,
+} from "../../component/html/html";
+import { css } from "@emotion/core";
+
+const FormKategori = ({ onSubmit, initialData = null }) => {
+  const { register, handleSubmit, errors } = useForm({
+    defaultValues: initialData !== null ? { name: initialData["name"] } : {},
+  });
+  return (
+    <form
+      data-testid="form-category"
+      onSubmit={handleSubmit(onSubmit)}
+      css={css`
+        display: flex;
+        flex-direction: column;
+      `}
+    >
+      <RowInput>
+        <LabelInput htmlFor="name">Nama kategori </LabelInput>
+        <InputForm
+          data-testid="name-kategori-input"
+          name="name"
+          ref={register({ required: true })}
+        />
+        {errors.name && <ErrorDiv>Nama kategori tidak boleh kosong</ErrorDiv>}
+      </RowInput>
+      {initialData !== null && initialData["image"] != null ? (
+        <img
+          css={css`
+            height: 10rem;
+            object-fit: contain;
+          `}
+          alt={initialData["name"]}
+          src={initialData["image"]}
+        />
+      ) : null}
+      <RowInput>
+        <LabelInput htmlFor="gambar">Foto kategori </LabelInput>
+        <InputForm type="file" name="image" ref={register} />
+      </RowInput>
+      <RowInput>
+        <InputSubmitForm type="submit" data-testid="submit-category" />
+      </RowInput>
+    </form>
+  );
+};
+
+export default FormKategori;
diff --git a/src/page/kategori/ListKategori.jsx b/src/page/kategori/ListKategori.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..7482c1b21b98630f613b3175f7dd104edd3ff0d7
--- /dev/null
+++ b/src/page/kategori/ListKategori.jsx
@@ -0,0 +1,50 @@
+import React from "react";
+import TableComponent from "../../component/TableComponent";
+import { css } from "@emotion/core";
+import LinkYellow from "../../component/LinkYellow";
+
+const ListKategori = () => {
+  const data = {
+    url: `${process.env.REACT_APP_BASE_URL}/categories/`,
+    pageDefault: 1,
+    searchDefault: "",
+    title: "",
+    keyValuePairs: [
+      ["id", "id"],
+      ["name", "Nama"],
+    ],
+    link: "",
+  };
+  return (
+    <div
+      css={css`
+        display: flex;
+        flex-direction: column;
+        margin: 2rem 3rem 3rem 3rem;
+      `}
+    >
+      <div
+        css={css`
+          font-size: 35px;
+        `}
+      >
+        KELOLA KATEGORI
+      </div>
+      <div
+        css={css`
+          width: 35%;
+          display: flex;
+          flex-direction: row;
+          margin-bottom: 2rem;
+          margin-top: 1rem;
+        `}
+      >
+        <LinkYellow to="/kategori/tambah">TAMBAH</LinkYellow>
+        <LinkYellow to="/kategori">LIHAT</LinkYellow>
+      </div>
+      <TableComponent {...data} />
+    </div>
+  );
+};
+
+export default ListKategori;
diff --git a/src/page/kategori/TambahKategori.jsx b/src/page/kategori/TambahKategori.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..64c3b8611a00745daf1a649fa34eeaeee879f2c6
--- /dev/null
+++ b/src/page/kategori/TambahKategori.jsx
@@ -0,0 +1,58 @@
+import React from "react";
+import { css } from "@emotion/core";
+import { ErrorDiv } from "../../component/html/html";
+import FormKategori from "./FormKategori";
+import LinkYellow from "../../component/LinkYellow";
+import useSendData from "../../utils/useSendData";
+
+const TambahKategori = () => {
+  const url = `${process.env.REACT_APP_BASE_URL}/categories/`;
+  const [send, error] = useSendData({
+    url,
+    method: "POST",
+    redirect: "/kategori",
+  });
+  const onSubmit = (data) => {
+    const formData = new FormData();
+    formData.append("name", data["name"]);
+    if (data["image"].length !== 0) formData.append("image", data["image"][0]);
+    send(formData);
+  };
+  return (
+    <div
+      data-testid="tambah-kategori"
+      css={css`
+        display: flex;
+        margin: 2rem 3rem 3rem 3rem;
+        flex-direction: column;
+      `}
+    >
+      <div>
+        {error && <ErrorDiv>Error !, Data tidak dapat disimpan</ErrorDiv>}
+      </div>
+
+      <div
+        css={css`
+          font-size: 35px;
+        `}
+      >
+        TAMBAH KATEGORI
+      </div>
+      <div
+        css={css`
+          width: 35%;
+          display: flex;
+          flex-direction: row;
+          margin-bottom: 2rem;
+          margin-top: 1rem;
+        `}
+      >
+        <LinkYellow to="/kategori/tambah">TAMBAH</LinkYellow>
+        <LinkYellow to="/kategori">LIHAT</LinkYellow>
+      </div>
+      <FormKategori {...{ onSubmit }} />
+    </div>
+  );
+};
+
+export default TambahKategori;
diff --git a/src/page/login/Login.jsx b/src/page/login/Login.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..8f68fa7e1086a0e6340b5afbdf5a5bdd8a44fe14
--- /dev/null
+++ b/src/page/login/Login.jsx
@@ -0,0 +1,147 @@
+import React, { useState } from "react";
+import { useForm } from "react-hook-form";
+import { useAuthContext } from "../../utils/contex";
+import { css } from "@emotion/core";
+import Logo from "../../component/Logo";
+import styled from "@emotion/styled";
+import { Center, ErrorDiv } from "../../component/html/html";
+import { trackPromise } from "react-promise-tracker";
+const Login = () => {
+  const { register, handleSubmit, errors } = useForm();
+  const { handleLogin } = useAuthContext();
+  const [errorState, setErrorState] = useState(false);
+  const onSubmit = (data) => {
+    trackPromise(
+      fetch(`${process.env.REACT_APP_BASE_URL}/auth/cred-login/`, {
+        method: "POST",
+        headers: {
+          Accept: "application/json",
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify({ ...data }),
+      })
+        .then((response) => {
+          if (response.ok) {
+            return response.json();
+          } else {
+            throw new Error("Error");
+          }
+        })
+        .then((result) => {
+          handleLogin({ ...result });
+        })
+        .catch(() => {
+          setErrorState(true);
+        })
+    );
+  };
+  return (
+    <Center
+      css={css`
+        height: 100vh;
+      `}
+    >
+      <Logo />
+      <Center
+        css={css`
+          margin-top: 1rem;
+          height: 75vh;
+          background-color: white;
+          width: 60vh;
+          border: 0.5px solid #eeeeee;
+          box-sizing: border-box;
+          box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+          border-radius: 10px;
+        `}
+      >
+        <div
+          css={css`
+            display: flex;
+            font-weight: 600;
+            font-size: 36px;
+            line-height: 44px;
+            text-align: center;
+          `}
+        >
+          Sign in
+        </div>
+        <form
+          css={css`
+            display: flex;
+            flex-direction: column;
+            width: 80%;
+            margin-top: 3rem;
+          `}
+          data-testid="login"
+          onSubmit={handleSubmit(onSubmit)}
+        >
+          {/* register your input into the hook by invoking the "register" function */}
+
+          {/* include validation with required or other standard HTML validation rules */}
+          <FieldLogin
+            name="username"
+            placeholder="Username"
+            data-testid="name-input"
+            ref={register({ required: true })}
+          />
+          {/* errors will return when field validation fails  */}
+          {errors.username && (
+            <span
+              css={css`
+                color: darkred;
+              `}
+              data-testid="name-required"
+            >
+              Username field is required
+            </span>
+          )}
+          <FieldLogin
+            css={css`
+              margin-top: 2rem;
+            `}
+            name="password"
+            placeholder="password"
+            type="password"
+            data-testid="password-input"
+            ref={register({ required: true })}
+          />
+          {errors.password && (
+            <span
+              css={css`
+                color: darkred;
+              `}
+              data-testid="password-required"
+            >
+              Password field is required
+            </span>
+          )}
+          {errorState && <ErrorDiv>Password salah !</ErrorDiv>}
+          <LoginButton
+            css={css`
+              margin-top: 2rem;
+            `}
+            type="submit"
+            value="LOGIN"
+          />
+        </form>
+      </Center>
+    </Center>
+  );
+};
+
+export default Login;
+
+const FieldLogin = styled.input`
+  background: #f6f6f6;
+  border: 1px solid #dedede;
+  box-sizing: border-box;
+  border-radius: 4px;
+  height: 2.5rem;
+`;
+
+const LoginButton = styled.input`
+  background: #4e7891;
+  border-radius: 50px;
+  color: white;
+  height: 2.5rem;
+`;
diff --git a/src/page/pengguna/DetailPengguna.jsx b/src/page/pengguna/DetailPengguna.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..69445b4d17fcfe59daaa7977714f30fc6cc4cb7e
--- /dev/null
+++ b/src/page/pengguna/DetailPengguna.jsx
@@ -0,0 +1,231 @@
+import React from "react";
+import { css } from "@emotion/core";
+import { ArrowBack } from "@material-ui/icons";
+import { navigate } from "@reach/router";
+import PersonIcon from "@material-ui/icons/Person";
+import PhoneIcon from "@material-ui/icons/Phone";
+import LocationOnIcon from "@material-ui/icons/LocationOn";
+import ScheduleIcon from "@material-ui/icons/Schedule";
+import useFetchSingleData from "../../utils/useFetchSingleData";
+import { ErrorDiv } from "../../component/html/html";
+import { stringToCurrency, stringToDate } from "../../component/TableUtils";
+import TableComponent from "../../component/TableComponent";
+
+const DetailPengguna = ({ userId }) => {
+  const url = `${process.env.REACT_APP_BASE_URL}/users/${userId}/`;
+  const [user, error] = useFetchSingleData(url);
+  const data = {
+    url: `${process.env.REACT_APP_BASE_URL}/transactions/`,
+    pageDefault: 1,
+    argument: `user=${userId}`,
+    title: "Riwayat Transaksi",
+    keyValuePairs: [
+      ["id", "id"],
+      ["transaction_number", "ID Transaksi"],
+      ["created_at", "Tanggal Pembuatan", stringToDate],
+      ["updated_at", "Tanggal Update", stringToDate],
+      ["readable_transaction_status", "Status"],
+      ["subtotal", "Total", stringToCurrency],
+    ],
+    link: "/transaksi/",
+    filter: [
+      ["updated_at_before", "Updated At Before"],
+      ["updated_at_after", "Updated At After"],
+      {
+        transaction_status: {
+          label: "Status transaksi",
+          choices: [
+            { "001": "Waiting for proof of payment" },
+            { "002": "Waiting for seller confirmation" },
+            { "003": "In process" },
+            { "004": "Being shipped" },
+            { "005": "Completed" },
+            { "006": "Canceled" },
+          ],
+        },
+      },
+    ],
+  };
+  return (
+    <div
+      data-testid="page-profile"
+      css={css`
+        width: 100%;
+        height: 100%;
+      `}
+    >
+      {error && <ErrorDiv>Error !, Please relogin..</ErrorDiv>}
+      <div
+        css={css`
+          margin: 2rem 3rem 3rem 3rem;
+          display: flex;
+          flex-direction: column;
+        `}
+      >
+        <div
+          css={css`
+            font-style: normal;
+            font-weight: 300;
+            font-size: 2.5rem;
+            line-height: 3.4rem;
+          `}
+        >
+          <button
+            css={css`
+              background-color: Transparent;
+              background-repeat: no-repeat;
+              border: none;
+              cursor: pointer;
+              overflow: hidden;
+              outline: none;
+            `}
+            onClick={() => navigate(-1)}
+          >
+            <ArrowBack fontSize="large" />
+          </button>
+          <div
+            css={css`
+              display: inline;
+              margin-left: 2rem;
+            `}
+          >
+            {user.username}
+          </div>
+        </div>
+        <div
+          className="profile"
+          css={css`
+            font-style: normal;
+            font-weight: 300;
+            font-size: 36px;
+            line-height: 44px;
+            margin-top: 1rem;
+          `}
+        >
+          Profil
+        </div>
+        <div
+          css={css`
+            margin-top: 1rem;
+            margin-bottom: 1rem;
+          `}
+        >
+          <div data-testid="profile">
+            <div
+              css={css`
+                margin-top: 1rem;
+              `}
+            >
+              <PersonIcon style={{ fontSize: 20, color: "FFC80A" }} />
+              <div
+                css={css`
+                  display: inline;
+                  margin-left: 2rem;
+                  font-style: normal;
+                  font-weight: normal;
+                  font-size: 18px;
+                  line-height: 22px;
+                `}
+              >
+                {user.username}
+              </div>
+            </div>
+            <div
+              css={css`
+                margin-top: 1rem;
+              `}
+            >
+              <PhoneIcon style={{ fontSize: 20, color: "FFC80A" }} />
+              <div
+                css={css`
+                  display: inline;
+                  margin-left: 2rem;
+                  font-style: normal;
+                  font-weight: normal;
+                  font-size: 18px;
+                  line-height: 22px;
+                `}
+              >
+                {user.phone_number}
+              </div>
+            </div>
+            <div
+              css={css`
+                margin-top: 1rem;
+              `}
+            >
+              <LocationOnIcon style={{ fontSize: 20, color: "FFC80A" }} />
+              <div
+                css={css`
+                  display: inline;
+                  margin-left: 2rem;
+                  font-style: normal;
+                  font-weight: normal;
+                  font-size: 18px;
+                  line-height: 22px;
+                `}
+              >
+                {user.address}, RT {user.neighborhood}, RW {user.hamlet},
+                Kelurahan {user.urban_village}, Kecamatan {user.sub_district}
+              </div>
+            </div>
+          </div>
+        </div>
+        <div
+          css={css`
+            font-style: normal;
+            font-weight: 300;
+            font-size: 30px;
+            line-height: 44px;
+          `}
+        >
+          Riwayat
+        </div>
+        <div
+          css={css`
+            font-style: normal;
+            font-weight: normal;
+            font-size: 15px;
+            line-height: 25px;
+            margin-top: 1rem;
+          `}
+        >
+          <div
+            css={css`
+              display: flex;
+              flex-direction: column;
+            `}
+          >
+            <TableComponent {...data} />
+          </div>
+        </div>
+        <div
+          css={css`
+            font-style: normal;
+            font-weight: normal;
+            font-size: 15px;
+            line-height: 15px;
+            margin-top: 1rem;
+          `}
+        >
+          <div
+            css={css`
+              display: flex;
+            `}
+          >
+            <ScheduleIcon style={{ fontSize: 25, color: "FFC80A" }} />
+            <div
+              css={css`
+                margin-left: 2rem;
+              `}
+            >
+              Riwayat Donasi
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default DetailPengguna;
diff --git a/src/page/pengguna/ListPengguna.jsx b/src/page/pengguna/ListPengguna.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..3ec37588d8bc70c0033c483b3fdba0f31d35b8c4
--- /dev/null
+++ b/src/page/pengguna/ListPengguna.jsx
@@ -0,0 +1,31 @@
+import React from "react";
+import { css } from "@emotion/core";
+import TableComponent from "../../component/TableComponent";
+
+const ListPengguna = () => {
+  const data = {
+    url: `${process.env.REACT_APP_BASE_URL}/users/`,
+    pageDefault: 1,
+    searchDefault: "",
+    title: "Pengguna",
+    keyValuePairs: [
+      ["id", "id"],
+      ["full_name", "Nama Lengkap"],
+      ["username", "Username"],
+      ["dummy", "dummy"],
+      ["dummy", "dummy"],
+    ],
+    link: "",
+  };
+  return (
+    <div
+      css={css`
+        margin-top: 2rem;
+      `}
+    >
+      <TableComponent {...data} />
+    </div>
+  );
+};
+
+export default ListPengguna;
diff --git a/src/page/produk/DetailProduk.jsx b/src/page/produk/DetailProduk.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..e732aed56195bfb2da9bd971f4c80cb0610ef8d0
--- /dev/null
+++ b/src/page/produk/DetailProduk.jsx
@@ -0,0 +1,309 @@
+import React, { useState } from "react";
+import { css } from "@emotion/core";
+import { ButtonDeleteStyled } from "../../component/html/html";
+import LinkYellow from "../../component/LinkYellow";
+import { ErrorDiv } from "../../component/html/html";
+import {
+  Search,
+  ArrowBack,
+  Grade,
+  Category,
+  AttachMoney,
+  LocalShipping,
+} from "@material-ui/icons";
+import { Link, navigate } from "@reach/router";
+import useFetchSingleData from "../../utils/useFetchSingleData";
+import useDelete from "../../utils/useDelete";
+import {
+  Dialog,
+  DialogActions,
+  DialogContent,
+  DialogContentText,
+  DialogTitle,
+  Button,
+} from "@material-ui/core";
+import { stringToCurrency } from "../../component/TableUtils";
+
+const DetailProduk = ({ productId }) => {
+  const url = `${process.env.REACT_APP_BASE_URL}/products/${productId}/`;
+  const [product, error] = useFetchSingleData(url);
+  const [openModal, setOpenModal] = useState(false);
+  const [deleteProduct, errorDelete] = useDelete(url);
+  const handleClose = () => setOpenModal(false);
+  return (
+    <div
+      data-testid="page"
+      css={css`
+        width: 100%;
+        height: 100%;
+      `}
+    >
+      <Dialog open={openModal} onClose={handleClose}>
+        <DialogTitle>{`Hapus produk ${product.name}`}</DialogTitle>
+        <DialogContent>
+          <DialogContentText>
+            Apakah yakin untuk menghapus produk {product.name}?
+          </DialogContentText>
+          <DialogActions>
+            <Button onClick={handleClose} color="primary">
+              TIDAK
+            </Button>
+            <Button
+              data-testid="button-delete-product"
+              color="secondary"
+              onClick={() => {
+                deleteProduct();
+                handleClose();
+              }}
+            >
+              HAPUS
+            </Button>
+          </DialogActions>
+        </DialogContent>
+      </Dialog>
+      {error && <ErrorDiv>Error !, Please relogin..</ErrorDiv>}
+      {errorDelete && (
+        <ErrorDiv>
+          Tidak dapat menghapus produk, mohon periksa apakah ada produk ini.
+        </ErrorDiv>
+      )}
+      <div
+        css={css`
+          margin: 2rem 3rem 3rem 3rem;
+          display: flex;
+          flex-direction: column;
+        `}
+      >
+        <div
+          css={css`
+            font-style: normal;
+            font-weight: 300;
+            font-size: 2.5rem;
+            line-height: 3.4rem;
+          `}
+        >
+          <button
+            css={css`
+              background-color: Transparent;
+              background-repeat: no-repeat;
+              border: none;
+              cursor: pointer;
+              overflow: hidden;
+              outline: none;
+            `}
+            onClick={() => navigate(-1)}
+          >
+            <ArrowBack fontSize="large" />
+          </button>
+          <div
+            css={css`
+              display: inline;
+              margin-left: 2rem;
+            `}
+          >
+            KELOLA PRODUK
+          </div>
+        </div>
+        <div
+          css={css`
+            width: 35%;
+            display: flex;
+            flex-direction: row;
+            margin-bottom: 1.8rem;
+            margin-top: 1rem;
+          `}
+        >
+          <LinkYellow to="/produk/tambah">TAMBAH</LinkYellow>
+          <LinkYellow className="ml-2" to="/produk">
+            LIHAT
+          </LinkYellow>
+        </div>
+        <div
+          css={css`
+            margin-top: 3%;
+          `}
+        >
+          <span>
+            <span
+              className="icon"
+              css={css`
+                vertical-align: middle;
+                display: inline-block;
+              `}
+            >
+              <Search style={{ color: "#FFC80A" }} />
+            </span>
+            <span
+              className="text"
+              css={css`
+                margin-top: 0.5rem;
+                margin-left: 1%;
+                font-weight: bold;
+              `}
+            >
+              Lihat Detail Produk
+            </span>
+          </span>
+        </div>
+        <div className="container-fluid px-0">
+          <div className="row mt-2">
+            <div className="col-sm-4 mt-2">
+              <img
+                alt={product.name}
+                className="img-fluid"
+                src={product.image}
+              />
+            </div>
+            <div data-testid="produk-detail" className="col-sm-8">
+              <div>
+                <p>
+                  <span
+                    css={css`
+                      font-size: 150%;
+                    `}
+                  >
+                    {product.name}
+                  </span>
+                </p>
+                <span>
+                  <span
+                    className="icon"
+                    css={css`
+                      vertical-align: middle;
+                      display: inline-block;
+                    `}
+                  >
+                    <Grade style={{ color: "#FFC80A" }} />
+                  </span>
+                  <span
+                    className="text"
+                    css={css`
+                      margin-top: 1rem;
+                      margin-left: 1%;
+                    `}
+                  >
+                    <Link to={`/kategori/${product.category}`}>
+                      {product.category_name}
+                    </Link>
+                  </span>
+                </span>
+                <p>
+                  <span>
+                    <span
+                      className="icon"
+                      css={css`
+                        vertical-align: middle;
+                        display: inline-block;
+                      `}
+                    >
+                      <Category style={{ color: "#FFC80A" }} />
+                    </span>
+                    <span
+                      className="text"
+                      css={css`
+                        margin-top: 1rem;
+                        margin-left: 1%;
+                      `}
+                    >
+                      <Link to={`/subkategori/${product.subcategory}`}>
+                        {product.subcategory_name}
+                      </Link>
+                    </span>
+                  </span>
+                </p>
+                <p>
+                  <span>
+                    <span
+                      className="icon"
+                      css={css`
+                        vertical-align: middle;
+                        display: inline-block;
+                      `}
+                    >
+                      <AttachMoney style={{ color: "#FFC80A" }} />
+                    </span>
+                    <span
+                      className="text"
+                      css={css`
+                        margin-top: 1rem;
+                        margin-left: 1%;
+                      `}
+                    >
+                      {stringToCurrency(product.price)}
+                    </span>
+                  </span>
+                </p>
+                <p>
+                  <span>
+                    <span
+                      className="icon"
+                      css={css`
+                        vertical-align: middle;
+                        display: inline-block;
+                      `}
+                    >
+                      <LocalShipping style={{ color: "#FFC80A" }} />
+                    </span>
+                    <span
+                      className="text"
+                      css={css`
+                        margin-top: 1rem;
+                        margin-left: 1%;
+                      `}
+                    >
+                      {product.pre_order ? "Preorder" : product.stock}
+                    </span>
+                  </span>
+                </p>
+                <span
+                  css={css`
+                    font-weight: bold;
+                  `}
+                >
+                  Deskripsi
+                </span>
+                <p>
+                  <span>{product.description}</span>
+                </p>
+              </div>
+              <div className="container-fluid row">
+                <div className="col-6">
+                  <div
+                    css={css`
+                      flex-grow: 1;
+                    `}
+                  >
+                    <LinkYellow
+                      css={css`
+                        border: 3px solid #3c8dbc;
+                      `}
+                      to="ubah"
+                    >
+                      UBAH
+                    </LinkYellow>
+                  </div>
+                </div>
+                <div className="col-6">
+                  <div
+                    css={css`
+                      flex-grow: 1;
+                    `}
+                  >
+                    <ButtonDeleteStyled
+                      data-testid="button-delete-product-modal"
+                      onClick={() => setOpenModal(true)}
+                    >
+                      HAPUS
+                    </ButtonDeleteStyled>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default DetailProduk;
diff --git a/src/page/produk/EditProduk.jsx b/src/page/produk/EditProduk.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..365c310a30735ff51ac626d3663168eb5de3f646
--- /dev/null
+++ b/src/page/produk/EditProduk.jsx
@@ -0,0 +1,91 @@
+import React from "react";
+import { css } from "@emotion/core";
+import { ErrorDiv } from "../../component/html/html";
+import FormProduk from "./FormProduk";
+import { navigate } from "@reach/router";
+import { ArrowBack, ErrorOutline } from "@material-ui/icons";
+import useFetchSingleData from "../../utils/useFetchSingleData";
+import useSendData from "../../utils/useSendData";
+
+const EditProduk = ({ productId }) => {
+  const url = `${process.env.REACT_APP_BASE_URL}/products/${productId}/`;
+  const [initialData, errorState] = useFetchSingleData(url);
+  const [send, error] = useSendData({ url, method: "PATCH", redirect: -1 });
+  const onSubmit = (data) => {
+    send(data);
+  };
+  if (errorState || Object.keys(initialData).length === 0)
+    return (
+      <div
+        data-testid="waiting-edit-produk"
+        css={css`
+          display: flex;
+          margin: 2rem 3rem 3rem 3rem;
+          flex-direction: column;
+          font-size: 25px;
+        `}
+      >
+        Fetching data..
+      </div>
+    );
+  return (
+    <div
+      data-testid="edit-produk"
+      css={css`
+        display: flex;
+        margin: 2rem 3rem 3rem 3rem;
+        flex-direction: column;
+      `}
+    >
+      {error && <ErrorDiv>Error !, Data tidak dapat disimpan</ErrorDiv>}
+      <div
+        css={css`
+          display: flex;
+          flex-direction: row;
+        `}
+      >
+        <button
+          data-testid="back"
+          css={css`
+            background-color: Transparent;
+            background-repeat: no-repeat;
+            border: none;
+            cursor: pointer;
+            overflow: hidden;
+            outline: none;
+          `}
+          onClick={() => navigate(-1)}
+        >
+          <ArrowBack fontSize="large" />
+        </button>
+        <div
+          css={css`
+            font-size: 36px;
+          `}
+        >
+          Edit {initialData.name}
+        </div>
+      </div>
+      <div
+        css={css`
+          margin-top: 2.5rem;
+          display: flex;
+        `}
+      >
+        <ErrorOutline style={{ fontSize: 28, color: "FFC80A" }} />
+        <div
+          css={css`
+            font-weight: 600;
+            font-size: 24px;
+            line-height: 29px;
+          `}
+        >
+          Informasi Produk
+        </div>
+      </div>
+      <FormProduk {...{ onSubmit, initialData }} />
+    </div>
+  );
+};
+
+export default EditProduk;
diff --git a/src/page/produk/FormProduk.jsx b/src/page/produk/FormProduk.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..267adc119f4ef8a808916497148685c9d8102565
--- /dev/null
+++ b/src/page/produk/FormProduk.jsx
@@ -0,0 +1,210 @@
+import React from "react";
+import { useForm } from "react-hook-form";
+import useFetchList from "../../utils/useFetchList";
+import {
+  ErrorDiv,
+  RowInput,
+  InputForm,
+  LabelInput,
+  InputSelectForm,
+  InputSubmitForm,
+  InputTextArea,
+} from "../../component/html/html";
+import { css } from "@emotion/core";
+
+const FormProduk = ({ onSubmit, initialData = null }) => {
+  const { register, handleSubmit, errors, watch } = useForm({
+    defaultValues:
+      initialData !== null
+        ? {
+            name: initialData["name"],
+            category: initialData["category"],
+            subcategory: initialData["subcategory"],
+            description: initialData["description"],
+            price: initialData["price"],
+            stock: initialData["stock"],
+            pre_order: initialData["pre_order"].toString(),
+          }
+        : {},
+  });
+  const url = `${process.env.REACT_APP_BASE_URL}/categories/`;
+  const urlSubcategory = `${process.env.REACT_APP_BASE_URL}/subcategories/`;
+  const [results, errorState, , , , , ,] = useFetchList([
+    url,
+    null,
+    null,
+    null,
+    null,
+  ]);
+  const watchCategory = watch("category", "");
+  const watchPreorder = watch("pre_order");
+  const [resultsSubcategory, errorStateSubcategory, , , , , ,] = useFetchList([
+    urlSubcategory,
+    null,
+    null,
+    null,
+    `category=${watchCategory}`,
+    null,
+  ]);
+  const filterSubmit = (data) => {
+    const formData = new FormData();
+    if (watchPreorder === "false") {
+      data.pre_order = false;
+      formData.append("stock", data["stock"]);
+    } else {
+      data.pre_order = true;
+    }
+    formData.append("name", data["name"]);
+    formData.append("subcategory", data["subcategory"]);
+    formData.append("description", data["description"]);
+    formData.append("price", data["price"]);
+    formData.append("pre_order", data["pre_order"]);
+    if (data["image"].length !== 0) formData.append("image", data["image"][0]);
+    onSubmit(formData);
+  };
+  return (
+    <form
+      data-testid="form-produk"
+      onSubmit={handleSubmit(filterSubmit)}
+      css={css`
+        display: flex;
+        flex-direction: column;
+      `}
+    >
+      <div>
+        {errorState ||
+          (errorStateSubcategory && (
+            <ErrorDiv>Error loading form !, Please relogin..</ErrorDiv>
+          ))}
+      </div>
+      <RowInput>
+        <LabelInput htmlFor="name">Nama Produk </LabelInput>
+        <InputForm
+          data-testid="name-produk-input"
+          name="name"
+          ref={register({ required: true })}
+        />
+        {errors.name && <ErrorDiv>Nama Produk tidak boleh kosong</ErrorDiv>}
+      </RowInput>
+      {results === undefined || Object.keys(results).length === 0 ? null : (
+        <RowInput>
+          <LabelInput htmlFor="name">Kategori: </LabelInput>
+          <InputSelectForm
+            data-testid="category-produk-input"
+            name="category"
+            ref={register({ required: true })}
+          >
+            {results.map((item) => (
+              <option key={item.id} value={item.id}>
+                {item.name}
+              </option>
+            ))}
+          </InputSelectForm>
+        </RowInput>
+      )}
+      {resultsSubcategory === undefined ||
+      results === undefined ||
+      Object.keys(resultsSubcategory).length === 0 ||
+      Object.keys(results).length === 0 ? null : (
+        <RowInput>
+          <LabelInput htmlFor="name">Subkategori </LabelInput>
+          <InputSelectForm
+            data-testid="subcategory-produk-input"
+            name="subcategory"
+            ref={register({ required: true })}
+          >
+            {resultsSubcategory.map((item) => (
+              <option key={item.id} value={item.id}>
+                {item.name}
+              </option>
+            ))}
+          </InputSelectForm>
+        </RowInput>
+      )}
+      <RowInput>
+        <LabelInput htmlFor="deskripsi">Deskripsi Produk </LabelInput>
+        <InputTextArea
+          data-testid="desc-produk-input"
+          name="description"
+          ref={register({ required: true })}
+        />
+        {errors.description && (
+          <ErrorDiv>Deskripsi Produk tidak boleh kosong</ErrorDiv>
+        )}
+      </RowInput>
+      <RowInput>
+        <LabelInput htmlFor="is-preorder">Tipe Barang:</LabelInput>
+        <div
+          css={css`
+            display: flex;
+            flex-grow: 4;
+            align-items: baseline;
+            align-content: flex-start;
+          `}
+        >
+          <input
+            data-testid="is-preorder-produk-input"
+            type="radio"
+            name="pre_order"
+            value="false"
+            id="no-preorder"
+            ref={register({ required: true })}
+          />
+          <label htmlFor="no-preorder">Biasa</label>
+        </div>
+        <div
+          css={css`
+            display: flex;
+            flex-grow: 4;
+            align-items: baseline;
+            align-content: flex-start;
+          `}
+        >
+          <input
+            data-testid="is-preorder-produk-input-2"
+            type="radio"
+            name="pre_order"
+            value="true"
+            id="preorder"
+            ref={register({ required: true })}
+          />
+          <label htmlFor="preorder">Preorder</label>
+        </div>
+        {errors.pre_order && (
+          <ErrorDiv>Tipe Produk tidak boleh kosong</ErrorDiv>
+        )}
+      </RowInput>
+      <RowInput>
+        <LabelInput htmlFor="harga">Harga/kuantitas </LabelInput>
+        <InputForm
+          data-testid="price-produk-input"
+          type="number"
+          name="price"
+          ref={register({ required: true })}
+        />
+        {errors.price && <ErrorDiv>Harga Produk tidak boleh kosong</ErrorDiv>}
+      </RowInput>
+      <RowInput>
+        <LabelInput htmlFor="stock">Stok</LabelInput>
+        <InputForm
+          data-testid="stock-produk-input"
+          type="number"
+          name="stock"
+          disabled={watchPreorder === "true"}
+          ref={register({ required: watchPreorder === "false" })}
+        />
+        {errors.stock && <ErrorDiv>Stok Produk tidak boleh kosong</ErrorDiv>}
+      </RowInput>
+
+      <RowInput>
+        <LabelInput htmlFor="gambar">Foto Produk </LabelInput>
+        <InputForm type="file" name="image" ref={register} />
+      </RowInput>
+      <RowInput>
+        <InputSubmitForm type="submit" data-testid="submit-produk" />
+      </RowInput>
+    </form>
+  );
+};
+
+export default FormProduk;
diff --git a/src/page/produk/ListProduk.jsx b/src/page/produk/ListProduk.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..021f6bd253f8e45f178062ce8e82494f9e12d30c
--- /dev/null
+++ b/src/page/produk/ListProduk.jsx
@@ -0,0 +1,73 @@
+import React from "react";
+import TableComponent from "../../component/TableComponent";
+import { css } from "@emotion/core";
+import LinkYellow from "../../component/LinkYellow";
+import { stringToCurrency } from "../../component/TableUtils";
+
+const ListProduk = () => {
+  const data = {
+    url: `${process.env.REACT_APP_BASE_URL}/products/`,
+    pageDefault: 1,
+    searchDefault: "",
+    title: "",
+    keyValuePairs: [
+      ["id", "id"],
+      ["name", "Nama Produk"],
+      ["price", "Harga", stringToCurrency],
+      ["stock", "Stok"],
+    ],
+    link: "",
+  };
+  return (
+    <div
+      css={css`
+        display: flex;
+        flex-direction: column;
+        margin: 2rem 3rem 3rem 3rem;
+      `}
+    >
+      <div
+        css={css`
+          font-size: 35px;
+        `}
+      >
+        KELOLA PRODUK
+      </div>
+      <div
+        css={css`
+          width: 100%;
+          justify-content: space-between;
+          display: flex;
+          flex-direction: row;
+          margin-bottom: 1.8rem;
+          margin-top: 1rem;
+        `}
+      >
+        <div
+          css={css`
+            display: flex;
+            width: 25%;
+          `}
+        >
+          <LinkYellow to="tambah">TAMBAH</LinkYellow>
+          <LinkYellow to="/produk">LIHAT</LinkYellow>
+        </div>
+
+        <div
+          css={css`
+            display: flex;
+            width: 35%;
+          `}
+        >
+          <LinkYellow to="/subkategori">SUBKATEGORI</LinkYellow>
+          <LinkYellow className="ml-2" to="/kategori">
+            KATEGORI
+          </LinkYellow>
+        </div>
+      </div>
+      <TableComponent {...data} />
+    </div>
+  );
+};
+
+export default ListProduk;
diff --git a/src/page/produk/TambahProduk.jsx b/src/page/produk/TambahProduk.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..9ca23f57dad1449533b50612d00936cb09a4c66d
--- /dev/null
+++ b/src/page/produk/TambahProduk.jsx
@@ -0,0 +1,55 @@
+import React from "react";
+import { css } from "@emotion/core";
+import { ErrorDiv } from "../../component/html/html";
+import FormProduk from "./FormProduk";
+import LinkYellow from "../../component/LinkYellow";
+import useSendData from "../../utils/useSendData";
+
+const TambahProduk = () => {
+  const url = `${process.env.REACT_APP_BASE_URL}/products/`;
+  const [send, error] = useSendData({
+    url,
+    method: "POST",
+    redirect: "/produk",
+  });
+  const onSubmit = (data) => {
+    send(data);
+  };
+  return (
+    <div
+      data-testid="tambah-produk"
+      css={css`
+        display: flex;
+        margin: 2rem 3rem 3rem 3rem;
+        flex-direction: column;
+      `}
+    >
+      <div>
+        {error && <ErrorDiv>Error !, Data tidak dapat disimpan</ErrorDiv>}
+      </div>
+
+      <div
+        css={css`
+          font-size: 35px;
+        `}
+      >
+        TAMBAH PRODUK
+      </div>
+      <div
+        css={css`
+          width: 35%;
+          display: flex;
+          flex-direction: row;
+          margin-bottom: 2rem;
+          margin-top: 1rem;
+        `}
+      >
+        <LinkYellow to="/produk/tambah">TAMBAH</LinkYellow>
+        <LinkYellow to="/produk">LIHAT</LinkYellow>
+      </div>
+      <FormProduk {...{ onSubmit }} />
+    </div>
+  );
+};
+
+export default TambahProduk;
diff --git a/src/page/program/DetailProgram.jsx b/src/page/program/DetailProgram.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..e5da33269c1c9d4650f8d13000410e9b643c7ef3
--- /dev/null
+++ b/src/page/program/DetailProgram.jsx
@@ -0,0 +1,211 @@
+import React from "react";
+import { css } from "@emotion/core";
+import { ButtonDeleteStyled, ErrorDiv } from "../../component/html/html";
+import { ArrowBack } from "@material-ui/icons";
+import { Link } from "@reach/router";
+import PersonIcon from "@material-ui/icons/Person";
+import LocationOnIcon from "@material-ui/icons/LocationOn";
+import EventIcon from "@material-ui/icons/Event";
+import useFetchSingleData from "../../utils/useFetchSingleData";
+import LinkYellow from "../../component/LinkYellow";
+import useDelete from "../../utils/useDelete";
+
+const DetailPengguna = ({ idProgram }) => {
+  const url = `${process.env.REACT_APP_BASE_URL}/programs/${idProgram}/`;
+  const [program, error] = useFetchSingleData(url);
+  const [deleteProgram, errorDelete] = useDelete(url);
+  const start_date_time = new Date(program.start_date_time).toLocaleString();
+  const end_date_time = new Date(program.end_date_time).toLocaleString();
+  return (
+    <div
+      data-testid="page"
+      css={css`
+        width: 100%;
+        height: 100%;
+        overflow: scroll;
+      `}
+    >
+      {error && <ErrorDiv>Error !, Please relogin..</ErrorDiv>}
+      {errorDelete}
+      <div
+        css={css`
+          margin: 2rem 3rem 3rem 3rem;
+          display: flex;
+          flex-direction: column;
+        `}
+      >
+        <div
+          css={css`
+            font-style: normal;
+            font-weight: 300;
+            font-size: 2.5rem;
+            line-height: 3.4rem;
+          `}
+        >
+          <Link to="/program" style={{ color: "#000000" }}>
+            <ArrowBack fontSize="large" />
+          </Link>
+          <div
+            css={css`
+              display: inline;
+              margin-left: 2rem;
+            `}
+          >
+            KELOLA PROGRAM
+          </div>
+        </div>
+        <div
+          css={css`
+            width: 35%;
+            display: flex;
+            flex-direction: row;
+            margin-bottom: 1.8rem;
+            margin-top: 1rem;
+          `}
+        >
+          <LinkYellow to="/program/tambah">TAMBAH</LinkYellow>
+          <LinkYellow className="ml-2" to="/program">
+            LIHAT
+          </LinkYellow>
+        </div>
+        <div className="container-fluid px-0">
+          <div className="row mt-2">
+            <div className="col-sm-4 mt-2">
+              <img
+                alt={program.name}
+                className="img-fluid"
+                src={program.poster_image}
+              />
+            </div>
+            <div className="col-sm-8">
+              <div
+                className="program"
+                css={css`
+                  font-style: normal;
+                  font-weight: 300;
+                  font-size: 36px;
+                  line-height: 44px;
+                `}
+              >
+                {program.name}
+              </div>
+              <div
+                css={css`
+                  margin-top: 1rem;
+                  margin-bottom: 1rem;
+                `}
+              >
+                <div data-testid="program">
+                  <div
+                    css={css`
+                      margin-top: 1rem;
+                    `}
+                  >
+                    <EventIcon style={{ fontSize: 20, color: "FFC80A" }} />
+                    <div
+                      css={css`
+                        display: inline;
+                        margin-left: 2rem;
+                        font-style: normal;
+                        font-weight: normal;
+                        font-size: 18px;
+                        line-height: 22px;
+                      `}
+                    >
+                      {start_date_time} - {end_date_time}
+                    </div>
+                  </div>
+                  <div
+                    css={css`
+                      margin-top: 1rem;
+                    `}
+                  >
+                    <LocationOnIcon style={{ fontSize: 20, color: "FFC80A" }} />
+                    <div
+                      css={css`
+                        display: inline;
+                        margin-left: 2rem;
+                        font-style: normal;
+                        font-weight: normal;
+                        font-size: 18px;
+                        line-height: 22px;
+                      `}
+                    >
+                      {program.location}
+                    </div>
+                  </div>
+                  <div
+                    css={css`
+                      margin-top: 1rem;
+                    `}
+                  >
+                    <PersonIcon style={{ fontSize: 20, color: "FFC80A" }} />
+                    <div
+                      css={css`
+                        display: inline;
+                        margin-left: 2rem;
+                        font-style: normal;
+                        font-weight: normal;
+                        font-size: 18px;
+                        line-height: 22px;
+                      `}
+                    >
+                      {program.speaker}
+                    </div>
+                  </div>
+                  <div
+                    css={css`
+                      margin-top: 2rem;
+                      margin-bottom: 1rem;
+                      font-style: normal;
+                      font-weight: normal;
+                      font-size: 24px;
+                      line-height: 22px;
+                    `}
+                  >
+                    Deskripsi
+                  </div>
+                  <div>{program.description}</div>
+                </div>
+              </div>
+              <div className="container-fluid row">
+                <div className="col-6">
+                  <div
+                    css={css`
+                      flex-grow: 1;
+                    `}
+                  >
+                    <LinkYellow
+                      css={css`
+                        border: 3px solid #3c8dbc;
+                      `}
+                      to="ubah"
+                    >
+                      UBAH
+                    </LinkYellow>
+                  </div>
+                </div>
+                <div className="col-6">
+                  <div
+                    css={css`
+                      flex-grow: 1;
+                    `}
+                  >
+                    <ButtonDeleteStyled
+                      data-testid="button-delete-subcategory"
+                      onClick={deleteProgram}
+                    >
+                      HAPUS
+                    </ButtonDeleteStyled>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default DetailPengguna;
diff --git a/src/page/program/EditProgram.jsx b/src/page/program/EditProgram.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..186a6daccfd9f7c1e3f54ef52556c9a06fa266dd
--- /dev/null
+++ b/src/page/program/EditProgram.jsx
@@ -0,0 +1,99 @@
+import React from "react";
+import useFetchSingleData from "../../utils/useFetchSingleData";
+import { css } from "@emotion/core";
+import { ErrorDiv } from "../../component/html/html";
+import FormProgram from "./FormProgram";
+import { ArrowBack, ErrorOutline } from "@material-ui/icons";
+import { navigate } from "@reach/router";
+import useSendData from "../../utils/useSendData";
+
+const EditProgram = ({ idProgram }) => {
+  const url = `${process.env.REACT_APP_BASE_URL}/programs/${idProgram}/`;
+  const [initialData, errorState] = useFetchSingleData(url);
+  const [send, error] = useSendData({ url, method: "PATCH", redirect: -1 });
+  const onSubmit = (data) => {
+    const formData = new FormData();
+    formData.append("name", data["name"]);
+    formData.append("description", data["description"]);
+    formData.append("start_date_time", data["start_date_time"]);
+    formData.append("end_date_time", data["end_date_time"]);
+    formData.append("location", data["location"]);
+    formData.append("speaker", data["speaker"]);
+    if (data["poster_image"].length !== 0)
+      formData.append("poster_image", data["poster_image"][0]);
+    send(formData);
+  };
+  if (errorState || Object.keys(initialData).length === 0)
+    return (
+      <div
+        data-testid="waiting-edit-program"
+        css={css`
+          display: flex;
+          margin: 2rem 3rem 3rem 3rem;
+          flex-direction: column;
+          font-size: 25px;
+        `}
+      >
+        Fetching data..
+      </div>
+    );
+  return (
+    <div
+      data-testid="edit-program"
+      css={css`
+        display: flex;
+        margin: 2rem 3rem 3rem 3rem;
+        flex-direction: column;
+      `}
+    >
+      {error && <ErrorDiv>Error !, Data tidak dapat disimpan</ErrorDiv>}
+      <div
+        css={css`
+          display: flex;
+          flex-direction: row;
+        `}
+      >
+        <button
+          css={css`
+            background-color: Transparent;
+            background-repeat: no-repeat;
+            border: none;
+            cursor: pointer;
+            overflow: hidden;
+            outline: none;
+          `}
+          onClick={() => navigate(-1)}
+        >
+          <ArrowBack fontSize="large" />
+        </button>
+        <div
+          css={css`
+            font-size: 36px;
+          `}
+        >
+          Edit {initialData.name}
+        </div>
+      </div>
+      <div
+        css={css`
+          margin-top: 2.5rem;
+          display: flex;
+        `}
+      >
+        <ErrorOutline style={{ fontSize: 28, color: "FFC80A" }} />
+        <div
+          css={css`
+            font-weight: 600;
+            font-size: 24px;
+            line-height: 29px;
+          `}
+        >
+          Informasi Program
+        </div>
+      </div>
+      <FormProgram {...{ onSubmit, initialData }} />
+    </div>
+  );
+};
+
+export default EditProgram;
diff --git a/src/page/program/FormProgram.jsx b/src/page/program/FormProgram.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..0ed17ca6603b9c936ca200e64912637b11d15787
--- /dev/null
+++ b/src/page/program/FormProgram.jsx
@@ -0,0 +1,116 @@
+import React from "react";
+import { useForm } from "react-hook-form";
+import {
+  ErrorDiv,
+  RowInput,
+  InputForm,
+  LabelInput,
+  InputSubmitForm,
+} from "../../component/html/html";
+import { css } from "@emotion/core";
+
+const FormProgram = ({ onSubmit, initialData = null }) => {
+  const { register, handleSubmit, errors } = useForm({
+    defaultValues:
+      initialData !== null
+        ? {
+            name: initialData["name"],
+            description: initialData["description"],
+            start_date_time: initialData["start_date_time"],
+            end_date_time: initialData["end_date_time"],
+            location: initialData["location"],
+            speaker: initialData["speaker"],
+          }
+        : {},
+  });
+  return (
+    <form
+      data-testid="form-program"
+      onSubmit={handleSubmit(onSubmit)}
+      css={css`
+        display: flex;
+        flex-direction: column;
+      `}
+    >
+      <RowInput>
+        <LabelInput htmlFor="name">Nama program </LabelInput>
+        <InputForm
+          data-testid="name-program-input"
+          name="name"
+          ref={register({ required: true })}
+        />
+        {errors.name && <ErrorDiv>Nama program tidak boleh kosong</ErrorDiv>}
+      </RowInput>
+      <RowInput>
+        <LabelInput htmlFor="description">Deskripsi </LabelInput>
+        <InputForm
+          data-testid="description-program-input"
+          name="description"
+          ref={register({ required: true })}
+        />
+        {errors.description}
+      </RowInput>
+      <RowInput>
+        <LabelInput htmlFor="start_date_time">
+          Tanggal dan waktu mulai{" "}
+        </LabelInput>
+        <InputForm
+          type="datetime-local"
+          data-testid="start-date-time-program-input"
+          name="start_date_time"
+          ref={register({ required: false })}
+        />
+        {errors.start_date_time}
+      </RowInput>
+      <RowInput>
+        <LabelInput htmlFor="end_date_time">
+          Tanggal dan waktu berakhir{" "}
+        </LabelInput>
+        <InputForm
+          type="datetime-local"
+          data-testid="end-date-time-program-input"
+          name="end_date_time"
+          ref={register({ required: false })}
+        />
+        {errors.end_date_time}
+      </RowInput>
+      <RowInput>
+        <LabelInput htmlFor="location">Lokasi </LabelInput>
+        <InputForm
+          data-testid="location-program-input"
+          name="location"
+          ref={register({ required: false })}
+        />
+        {errors.location}
+      </RowInput>
+      <RowInput>
+        <LabelInput htmlFor="speaker">Pembicara </LabelInput>
+        <InputForm
+          data-testid="speaker-program-input"
+          name="speaker"
+          ref={register({ required: false })}
+        />
+        {errors.name}
+      </RowInput>
+      {initialData !== null && initialData["poster_image"] != null ? (
+        <img
+          css={css`
+            height: 10rem;
+            object-fit: contain;
+          `}
+          alt={initialData["name"]}
+          src={initialData["poster_image"]}
+        />
+      ) : null}
+      <RowInput>
+        <LabelInput htmlFor="poster_image">Gambar Poster </LabelInput>
+        <InputForm type="file" name="poster_image" ref={register} />
+      </RowInput>
+      <RowInput>
+        <InputSubmitForm type="submit" data-testid="submit-category" />
+      </RowInput>
+    </form>
+  );
+};
+
+export default FormProgram;
diff --git a/src/page/program/ListProgram.jsx b/src/page/program/ListProgram.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..f58da09e41430fdaad38c11a2b4434d3d53be52a
--- /dev/null
+++ b/src/page/program/ListProgram.jsx
@@ -0,0 +1,52 @@
+import React from "react";
+import TableComponent from "../../component/TableComponent";
+import { css } from "@emotion/core";
+import LinkYellow from "../../component/LinkYellow";
+
+const ListProgram = () => {
+  const data = {
+    url: `${process.env.REACT_APP_BASE_URL}/programs/`,
+    pageDefault: 1,
+    searchDefault: "",
+    title: "",
+    keyValuePairs: [
+      ["id", "id"],
+      ["name", "Nama Program"],
+    ],
+    link: "",
+  };
+  return (
+    <div
+      css={css`
+        display: flex;
+        flex-direction: column;
+        margin: 2rem 3rem 3rem 3rem;
+      `}
+    >
+      <div
+        css={css`
+          font-size: 35px;
+        `}
+      >
+        KELOLA PROGRAM
+      </div>
+      <div
+        css={css`
+          width: 35%;
+          display: flex;
+          flex-direction: row;
+          margin-bottom: 2rem;
+          margin-top: 1rem;
+        `}
+      >
+        <LinkYellow to="tambah">TAMBAH</LinkYellow>
+        <LinkYellow className="ml-2" to="/program">
+          LIHAT
+        </LinkYellow>
+      </div>
+      <TableComponent {...data} />
+    </div>
+  );
+};
+
+export default ListProgram;
diff --git a/src/page/program/TambahProgram.jsx b/src/page/program/TambahProgram.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..f7ef910196b34245e8989f536a77a030d56ad335
--- /dev/null
+++ b/src/page/program/TambahProgram.jsx
@@ -0,0 +1,64 @@
+import React from "react";
+import { css } from "@emotion/core";
+import { ErrorDiv } from "../../component/html/html";
+import FormProgram from "./FormProgram";
+import LinkYellow from "../../component/LinkYellow";
+import useSendData from "../../utils/useSendData";
+
+const TambahProgram = () => {
+  const url = `${process.env.REACT_APP_BASE_URL}/programs/`;
+  const [send, error] = useSendData({
+    url,
+    method: "POST",
+    redirect: "/program",
+  });
+  const onSubmit = (data) => {
+    const formData = new FormData();
+    formData.append("name", data["name"]);
+    formData.append("description", data["description"]);
+    formData.append("start_date_time", data["start_date_time"]);
+    formData.append("end_date_time", data["end_date_time"]);
+    formData.append("location", data["location"]);
+    formData.append("speaker", data["speaker"]);
+    if (data["poster_image"].length !== 0)
+      formData.append("poster_image", data["poster_image"][0]);
+    send(formData);
+  };
+  return (
+    <div
+      data-testid="tambah-program"
+      css={css`
+        display: flex;
+        margin: 2rem 3rem 3rem 3rem;
+        flex-direction: column;
+      `}
+    >
+      <div>
+        {error && <ErrorDiv>Error !, Data tidak dapat disimpan</ErrorDiv>}
+      </div>
+
+      <div
+        css={css`
+          font-size: 35px;
+        `}
+      >
+        TAMBAH PROGRAM
+      </div>
+      <div
+        css={css`
+          width: 35%;
+          display: flex;
+          flex-direction: row;
+          margin-bottom: 2rem;
+          margin-top: 1rem;
+        `}
+      >
+        <LinkYellow to="/program/tambah">TAMBAH</LinkYellow>
+        <LinkYellow to="/program">LIHAT</LinkYellow>
+      </div>
+      <FormProgram {...{ onSubmit }} />
+    </div>
+  );
+};
+
+export default TambahProgram;
diff --git a/src/page/subkategori/DetailSubkategori.jsx b/src/page/subkategori/DetailSubkategori.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..e9e59683b743254b1c2b9591a673c6240d4375b9
--- /dev/null
+++ b/src/page/subkategori/DetailSubkategori.jsx
@@ -0,0 +1,150 @@
+import React, { useState } from "react";
+import useFetchSingleData from "../../utils/useFetchSingleData";
+import { css } from "@emotion/core";
+import { ButtonDeleteStyled, ErrorDiv } from "../../component/html/html";
+import TableComponent from "../../component/TableComponent";
+import LinkYellow from "../../component/LinkYellow";
+import { Link, navigate } from "@reach/router";
+import { ArrowBack } from "@material-ui/icons";
+import useDelete from "../../utils/useDelete";
+import {
+  Dialog,
+  DialogActions,
+  DialogContent,
+  DialogContentText,
+  DialogTitle,
+  Button,
+} from "@material-ui/core";
+
+const DetailSubkategori = ({ idSubKategori }) => {
+  const url = `${process.env.REACT_APP_BASE_URL}/subcategories/${idSubKategori}/`;
+  const [subcategory, error] = useFetchSingleData(url);
+  const [deleteProduct, errorDelete] = useDelete(url);
+  const [openModal, setOpenModal] = useState(false);
+  const data = {
+    url: `${process.env.REACT_APP_BASE_URL}/products/`,
+    pageDefault: 1,
+    searchDefault: "",
+    title: "",
+    keyValuePairs: [
+      ["id", "id"],
+      ["name", "Nama"],
+      ["price", "Harga"],
+      ["stock", "Stok"],
+    ],
+    argument: `subcategory=${idSubKategori}`,
+  };
+  const handleClose = () => setOpenModal(false);
+  return (
+    <div
+      data-testid="page-detail-subkategori"
+      css={css`
+        display: flex;
+        margin: 2rem 3rem 3rem 3rem;
+        flex-direction: column;
+      `}
+    >
+      <Dialog open={openModal} onClose={handleClose}>
+        <DialogTitle>{`Hapus subkategori ${subcategory.name}`}</DialogTitle>
+        <DialogContent>
+          <DialogContentText>
+            Apakah yakin untuk menghapus subkategori {subcategory.name}?
+          </DialogContentText>
+          <DialogActions>
+            <Button onClick={handleClose} color="primary">
+              TIDAK
+            </Button>
+            <Button
+              data-testid="button-delete-subcategory"
+              color="secondary"
+              onClick={() => {
+                deleteProduct();
+                handleClose();
+              }}
+            >
+              HAPUS
+            </Button>
+          </DialogActions>
+        </DialogContent>
+      </Dialog>
+      {error && <ErrorDiv>Error !, Please relogin..</ErrorDiv>}
+      {errorDelete && (
+        <ErrorDiv>
+          Tidak dapat menghapus subkategori, mohon periksa apakah ada produk
+          didalam kategori ini.
+        </ErrorDiv>
+      )}
+      <div
+        css={css`
+          display: flex;
+          flex-direction: column;
+        `}
+      >
+        <button
+          css={css`
+            align-self: start;
+            background-color: Transparent;
+            background-repeat: no-repeat;
+            border: none;
+            cursor: pointer;
+            overflow: hidden;
+            outline: none;
+          `}
+          onClick={() => navigate("/subkategori")}
+        >
+          <ArrowBack fontSize="large" />
+        </button>
+        <div
+          css={css`
+            font-size: 2rem;
+            display: flex;
+            flex-direction: row;
+            align-items: baseline;
+          `}
+        >
+          <div
+            css={css`
+              flex-grow: 2;
+            `}
+          >
+            Subkategori: {subcategory.name}
+          </div>
+          <div
+            css={css`
+              flex-grow: 1;
+            `}
+          >
+            <LinkYellow to="ubah">EDIT</LinkYellow>
+          </div>
+          <div
+            css={css`
+              flex-grow: 1;
+            `}
+          >
+            <ButtonDeleteStyled
+              data-testid="button-delete-subcategory-modal"
+              onClick={() => setOpenModal(true)}
+            >
+              HAPUS
+            </ButtonDeleteStyled>
+          </div>
+        </div>
+        <div
+          css={css`
+            font-size: 1.5rem;
+          `}
+        >
+          Kategori:
+          <span>
+            <Link to={`/kategori/${subcategory.category}`}>
+              {subcategory.category_name}
+            </Link>
+          </span>
+        </div>
+      </div>
+      <TableComponent {...data} />
+    </div>
+  );
+};
+
+export default DetailSubkategori;
diff --git a/src/page/subkategori/EditSubkategori.jsx b/src/page/subkategori/EditSubkategori.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..72c3419153ec8dd2c3fd09079e17a6b7a4b86ff2
--- /dev/null
+++ b/src/page/subkategori/EditSubkategori.jsx
@@ -0,0 +1,94 @@
+import React from "react";
+import useFetchSingleData from "../../utils/useFetchSingleData";
+import { css } from "@emotion/core";
+import { ErrorDiv } from "../../component/html/html";
+import FormSubkategori from "./FormSubkategori";
+import { ArrowBack, ErrorOutline } from "@material-ui/icons";
+import { navigate } from "@reach/router";
+import useSendData from "../../utils/useSendData";
+
+const EditSubkategori = ({ idSubKategori }) => {
+  const url = `${process.env.REACT_APP_BASE_URL}/subcategories/${idSubKategori}/`;
+  const [initialData, errorState] = useFetchSingleData(url);
+  const [send, error] = useSendData({ url, method: "PATCH", redirect: -1 });
+  const onSubmit = (data) => {
+    const formData = new FormData();
+    formData.append("name", data["name"]);
+    formData.append("category", data["category"]);
+    if (data["image"].length !== 0) formData.append("image", data["image"][0]);
+    send(formData);
+  };
+  if (errorState || Object.keys(initialData).length === 0)
+    return (
+      <div
+        data-testid="waiting-edit-subkategori"
+        css={css`
+          display: flex;
+          margin: 2rem 3rem 3rem 3rem;
+          flex-direction: column;
+          font-size: 25px;
+        `}
+      >
+        Fetching data..
+      </div>
+    );
+  return (
+    <div
+      data-testid="edit-subkategori"
+      css={css`
+        display: flex;
+        margin: 2rem 3rem 3rem 3rem;
+        flex-direction: column;
+      `}
+    >
+      {error && <ErrorDiv>Error !, Data tidak dapat disimpan</ErrorDiv>}
+      <div
+        css={css`
+          display: flex;
+          flex-direction: row;
+        `}
+      >
+        <button
+          css={css`
+            background-color: Transparent;
+            background-repeat: no-repeat;
+            border: none;
+            cursor: pointer;
+            overflow: hidden;
+            outline: none;
+          `}
+          onClick={() => navigate(-1)}
+        >
+          <ArrowBack fontSize="large" />
+        </button>
+        <div
+          css={css`
+            font-size: 36px;
+          `}
+        >
+          Edit {initialData.name}
+        </div>
+      </div>
+      <div
+        css={css`
+          margin-top: 2.5rem;
+          display: flex;
+        `}
+      >
+        <ErrorOutline style={{ fontSize: 28, color: "FFC80A" }} />
+        <div
+          css={css`
+            font-weight: 600;
+            font-size: 24px;
+            line-height: 29px;
+          `}
+        >
+          Informasi Subkategori
+        </div>
+      </div>
+      <FormSubkategori {...{ onSubmit, initialData }} />
+    </div>
+  );
+};
+
+export default EditSubkategori;
diff --git a/src/page/subkategori/FormSubkategori.jsx b/src/page/subkategori/FormSubkategori.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..3389cd54e043fe3d9ff000305f1030523e13742c
--- /dev/null
+++ b/src/page/subkategori/FormSubkategori.jsx
@@ -0,0 +1,94 @@
+import React from "react";
+import { useForm } from "react-hook-form";
+import useFetchList from "../../utils/useFetchList";
+import {
+  ErrorDiv,
+  RowInput,
+  InputForm,
+  LabelInput,
+  InputSelectForm,
+  InputSubmitForm,
+} from "../../component/html/html";
+import { css } from "@emotion/core";
+
+const FormSubkategori = ({ onSubmit, initialData = null }) => {
+  const { register, handleSubmit, errors } = useForm({
+    defaultValues:
+      initialData !== null
+        ? { name: initialData["name"], category: initialData["category"] }
+        : {},
+  });
+  const url = `${process.env.REACT_APP_BASE_URL}/categories/`;
+  const [results, errorState, , , , , ,] = useFetchList([
+    url,
+    null,
+    null,
+    null,
+    null,
+  ]);
+  return (
+    <form
+      data-testid="form-subcategory"
+      onSubmit={handleSubmit(onSubmit)}
+      css={css`
+        display: flex;
+        flex-direction: column;
+      `}
+    >
+      <div>
+        {errorState && (
+          <ErrorDiv>Error loading form !, Please relogin..</ErrorDiv>
+        )}
+      </div>
+      <RowInput>
+        <LabelInput htmlFor="name">Nama subkategori </LabelInput>
+        <InputForm
+          data-testid="name-subkategori-input"
+          name="name"
+          ref={register({ required: true })}
+        />
+        {errors.name && (
+          <ErrorDiv>Nama subkategori tidak boleh kosong</ErrorDiv>
+        )}
+      </RowInput>
+      {results === undefined || Object.keys(results).length === 0 ? null : (
+        <RowInput>
+          <LabelInput htmlFor="name">Kategori </LabelInput>
+          <InputSelectForm
+            data-testid="category-subcategory-input"
+            name="category"
+            ref={register({ required: true })}
+          >
+            {results.map((item) => (
+              <option key={item.id} value={item.id}>
+                {item.name}
+              </option>
+            ))}
+            {errors.category && (
+              <ErrorDiv>Kategori tidak boleh kosong</ErrorDiv>
+            )}
+          </InputSelectForm>
+        </RowInput>
+      )}
+      {initialData !== null && initialData["image"] != null ? (
+        <img
+          css={css`
+            height: 10rem;
+            object-fit: contain;
+          `}
+          alt={initialData["name"]}
+          src={initialData["image"]}
+        />
+      ) : null}
+      <RowInput>
+        <LabelInput htmlFor="gambar">Foto subkategori </LabelInput>
+        <InputForm type="file" name="image" ref={register} />
+      </RowInput>
+      <RowInput>
+        <InputSubmitForm type="submit" data-testid="submit-subcategory" />
+      </RowInput>
+    </form>
+  );
+};
+
+export default FormSubkategori;
diff --git a/src/page/subkategori/ListSubkategori.jsx b/src/page/subkategori/ListSubkategori.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..2d4178c32ca141470cbcd5cbe9e1c325756c7676
--- /dev/null
+++ b/src/page/subkategori/ListSubkategori.jsx
@@ -0,0 +1,51 @@
+import React from "react";
+import TableComponent from "../../component/TableComponent";
+import { css } from "@emotion/core";
+import LinkYellow from "../../component/LinkYellow";
+
+const ListSubkategori = () => {
+  const data = {
+    url: `${process.env.REACT_APP_BASE_URL}/subcategories/`,
+    pageDefault: 1,
+    searchDefault: "",
+    title: "",
+    keyValuePairs: [
+      ["id", "id"],
+      ["name", "Nama"],
+      ["category_name", "Category"],
+    ],
+    link: "",
+  };
+  return (
+    <div
+      css={css`
+        display: flex;
+        flex-direction: column;
+        margin: 2rem 3rem 3rem 3rem;
+      `}
+    >
+      <div
+        css={css`
+          font-size: 35px;
+        `}
+      >
+        KELOLA SUBKATEGORI
+      </div>
+      <div
+        css={css`
+          width: 35%;
+          display: flex;
+          flex-direction: row;
+          margin-bottom: 2rem;
+          margin-top: 1rem;
+        `}
+      >
+        <LinkYellow to="tambah">TAMBAH</LinkYellow>
+        <LinkYellow to="/subkategori">LIHAT</LinkYellow>
+      </div>
+      <TableComponent {...data} />
+    </div>
+  );
+};
+
+export default ListSubkategori;
diff --git a/src/page/subkategori/TambahSubkategori.jsx b/src/page/subkategori/TambahSubkategori.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..daf70d0c506d756bea8ab1cc51be88c3e39bd14c
--- /dev/null
+++ b/src/page/subkategori/TambahSubkategori.jsx
@@ -0,0 +1,63 @@
+import React from "react";
+import { css } from "@emotion/core";
+import { ErrorDiv } from "../../component/html/html";
+import FormSubkategori from "./FormSubkategori";
+import LinkYellow from "../../component/LinkYellow";
+import useSendData from "../../utils/useSendData";
+
+const TambahSubkategori = () => {
+  const url = `${process.env.REACT_APP_BASE_URL}/subcategories/`;
+  const [send, error] = useSendData({
+    url,
+    method: "POST",
+    redirect: "/subkategori",
+  });
+  const onSubmit = (data) => {
+    const formData = new FormData();
+    formData.append("name", data["name"]);
+    formData.append("category", data["category"]);
+    if (data["image"].length !== 0) formData.append("image", data["image"][0]);
+    send(formData);
+  };
+  return (
+    <div
+      data-testid="tambah-subkategori"
+      css={css`
+        display: flex;
+        margin: 2rem 3rem 3rem 3rem;
+        flex-direction: column;
+      `}
+    >
+      <div>
+        {error && (
+          <ErrorDiv>
+            Error !, Data tidak dapat disimpan, pastikan nama subkategori unik.
+          </ErrorDiv>
+        )}
+      </div>
+
+      <div
+        css={css`
+          font-size: 35px;
+        `}
+      >
+        TAMBAH SUBKATEGORI
+      </div>
+      <div
+        css={css`
+          width: 35%;
+          display: flex;
+          flex-direction: row;
+          margin-bottom: 2rem;
+          margin-top: 1rem;
+        `}
+      >
+        <LinkYellow to="/subkategori/tambah">TAMBAH</LinkYellow>
+        <LinkYellow to="/subkategori">LIHAT</LinkYellow>
+      </div>
+      <FormSubkategori {...{ onSubmit }} />
+    </div>
+  );
+};
+
+export default TambahSubkategori;
diff --git a/src/page/transaksi/DetailTransaksi.jsx b/src/page/transaksi/DetailTransaksi.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..e151cba85cc7fabe89129ffb9f7d19e3e0ed839c
--- /dev/null
+++ b/src/page/transaksi/DetailTransaksi.jsx
@@ -0,0 +1,382 @@
+import React, { useState } from "react";
+import useFetchSingleData from "../../utils/useFetchSingleData";
+import { css } from "@emotion/core";
+import { ArrowBack, Contacts, Phone, Photo } from "@material-ui/icons";
+import { Link, navigate } from "@reach/router";
+import PersonIcon from "@material-ui/icons/Person";
+import Table from "@material-ui/core/Table";
+import TableBody from "@material-ui/core/TableBody";
+import TableCell from "@material-ui/core/TableCell";
+import TableContainer from "@material-ui/core/TableContainer";
+import TableHead from "@material-ui/core/TableHead";
+import TableRow from "@material-ui/core/TableRow";
+import Paper from "@material-ui/core/Paper";
+import Status from "../../component/Status";
+import { stringToCurrency, stringToDate } from "../../component/TableUtils";
+import { ErrorDiv } from "../../component/html/html";
+import Button from "@material-ui/core/Button";
+import Dialog from "@material-ui/core/Dialog";
+import DialogActions from "@material-ui/core/DialogActions";
+import DialogContent from "@material-ui/core/DialogContent";
+import DialogTitle from "@material-ui/core/DialogTitle";
+import FormStatus from "./FormStatus";
+const DetailTransaksi = ({ idTransaksi }) => {
+  const url = `${process.env.REACT_APP_BASE_URL}/transactions/${idTransaksi}`;
+  const [transaction, error] = useFetchSingleData(url);
+  const [dialogOpen, setDialogOpen] = useState(false);
+
+  const handleClickOpen = () => {
+    setDialogOpen(true);
+  };
+
+  const handleClose = () => {
+    setDialogOpen(false);
+  };
+
+  if (Object.keys(transaction).length === 0)
+    return (
+      <div
+        data-testid="waiting-detail-transaksi"
+        css={css`
+          display: flex;
+          margin: 2rem 3rem 3rem 3rem;
+          flex-direction: column;
+          font-size: 25px;
+        `}
+      >
+        {error && <ErrorDiv>Something error</ErrorDiv>}
+        Fetching data..
+      </div>
+    );
+  return (
+    <div
+      data-testid="page-detail-transaksi"
+      css={css`
+        display: flex;
+        margin: 2rem 3rem 3rem 3rem;
+        flex-direction: column;
+        justify-content: space-around;
+        height: 125vh;
+      `}
+    >
+      {error && <ErrorDiv>Something error</ErrorDiv>}
+      <button
+        css={css`
+          align-self: start;
+          background-color: Transparent;
+          background-repeat: no-repeat;
+          border: none;
+          cursor: pointer;
+          overflow: hidden;
+          outline: none;
+        `}
+        onClick={() => navigate(-1)}
+      >
+        <ArrowBack fontSize="large" />
+      </button>
+      <div
+        css={css`
+          font-size: 1rem;
+        `}
+      >
+        ADMIN - KELOLA TRANSAKSI
+      </div>
+      <div
+        css={css`
+          font-size: 1.5rem;
+          display: flex;
+          align-content: space-between;
+        `}
+      >
+        <div
+          css={css`
+            display: flex;
+          `}
+        >
+          <div
+            css={css`
+              margin-right: 1rem;
+            `}
+          >
+            Transaction code:
+          </div>
+          <div>{transaction.transaction_number}</div>
+        </div>
+        {transaction.transaction_status !== "006" &&
+          transaction.transaction_status !== "007" && (
+            <FormStatus
+              {...{
+                idTransaksi,
+                defaultStatus: transaction.transaction_status,
+                paymentMethod: transaction.payment_method,
+              }}
+            />
+          )}
+      </div>
+      <div
+        css={css`
+          font-size: 1.5rem;
+          display: flex;
+        `}
+      >
+        <div
+          css={css`
+            margin-right: 1rem;
+          `}
+        >
+          Status:{" "}
+        </div>
+        <Status
+          label={transaction.readable_transaction_status}
+          status={transaction.transaction_status}
+        />
+      </div>
+      <div
+        css={css`
+          font-size: 1.2rem;
+          display: flex;
+        `}
+      >
+        <div
+          css={css`
+            margin-right: 0.5rem;
+          `}
+        >
+          Date created:{" "}
+        </div>
+        <div>{stringToDate(transaction.created_at)}</div>
+      </div>
+      <div
+        css={css`
+          font-size: 1.2rem;
+          display: flex;
+        `}
+      >
+        <div
+          css={css`
+            margin-right: 0.5rem;
+          `}
+        >
+          Date updated:{" "}
+        </div>
+        <div>{stringToDate(transaction.updated_at)}</div>
+      </div>
+      <div
+        css={css`
+          display: flex;
+          font-size: 1.2rem;
+          align-items: baseline;
+        `}
+      >
+        <PersonIcon />
+        <Link to={`/pengguna/${transaction.user}`}>
+          {transaction.user_full_name} / {transaction.user_username}
+        </Link>
+      </div>
+
+      <div
+        css={css`
+          display: flex;
+          font-size: 1.2rem;
+          align-items: baseline;
+        `}
+      >
+        <Phone />
+        <div>{transaction.user_phone_number}</div>
+      </div>
+      <div
+        css={css`
+          display: flex;
+          flex-direction: column;
+        `}
+      >
+        <div
+          css={css`
+            font-size: 1.2rem;
+          `}
+        >
+          <Contacts />
+          Alamat Pengiriman
+          <div>{`${transaction.shipping_address}, 
+          RT ${transaction.shipping_neighborhood}, 
+          RW ${transaction.shipping_hamlet}, 
+          Kelurahan ${transaction.shipping_urban_village}, 
+          Kecamatan ${transaction.shipping_sub_district}`}</div>
+        </div>
+      </div>
+      <TableContainer component={Paper}>
+        <Table aria-label="simple table">
+          <TableHead>
+            <TableRow>
+              <TableCell>Product Code</TableCell>
+              <TableCell>Product Name</TableCell>
+              <TableCell>Quantity</TableCell>
+              <TableCell>Product Price</TableCell>
+              <TableCell>Subtotal</TableCell>
+            </TableRow>
+          </TableHead>
+          <TableBody>
+            {transaction.transaction_items.map((row) => (
+              <TableRow key={row.id}>
+                <TableCell component="th" scope="row">
+                  <Link to={`/produk/${row.product}`}>{row.product_code}</Link>
+                </TableCell>
+                <TableCell>{row.product_name}</TableCell>
+                <TableCell>{row.quantity}</TableCell>
+                <TableCell>{stringToCurrency(row.product_price)}</TableCell>
+                <TableCell>
+                  {stringToCurrency(
+                    `${Number.parseFloat(row.product_price) * row.quantity}`
+                  )}
+                </TableCell>
+              </TableRow>
+            ))}
+            <TableRow>
+              <TableCell rowSpan={3} colSpan={3} />
+              <TableCell align="left" colSpan={1}>
+                Donasi
+              </TableCell>
+              <TableCell align="left">
+                {stringToCurrency(transaction.donation)}
+              </TableCell>
+            </TableRow>
+            <TableRow>
+              <TableCell align="left" colSpan={1}>
+                Shipping cost
+              </TableCell>
+              <TableCell align="left">
+                {stringToCurrency(transaction.shipping_costs)}
+              </TableCell>
+            </TableRow>
+            <TableRow>
+              <TableCell colSpan={1} align="left">
+                Total
+              </TableCell>
+              <TableCell align="left">
+                {stringToCurrency(transaction.subtotal)}
+              </TableCell>
+            </TableRow>
+          </TableBody>
+        </Table>
+      </TableContainer>
+      <div
+        css={css`
+          display: flex;
+          flex-direction: column;
+        `}
+      >
+        {" "}
+        <div
+          css={css`
+            font-size: 1.5rem;
+          `}
+        >
+          Pembayaran
+        </div>
+        {transaction.payment_method === "COD" && (
+          <div
+            css={css`
+              font-size: 1.3rem;
+            `}
+          >
+            Cash On Delivery
+          </div>
+        )}
+        {transaction.payment_method === "TRF" && (
+          <div
+            css={css`
+              font-size: 1.3rem;
+            `}
+          >
+            Transfer
+            <div>
+              {transaction.proof_of_payment === null ? (
+                <div>User belum mengirimkan bukti pembayaran</div>
+              ) : (
+                <div
+                  css={css`
+                    display: flex;
+                    flex-direction: column;
+                  `}
+                >
+                  <div
+                    css={css`
+                      display: flex;
+                      font-size: 1.2rem;
+                      align-items: baseline;
+                    `}
+                  >
+                    <div
+                      css={css`
+                        margin-right: 1rem;
+                      `}
+                    >
+                      Sender name:{" "}
+                    </div>
+                    <div>{transaction.user_bank_account_name}</div>
+                  </div>
+                  <div
+                    css={css`
+                      display: flex;
+                      font-size: 1.2rem;
+                      align-items: baseline;
+                    `}
+                  >
+                    <div
+                      css={css`
+                        margin-right: 1rem;
+                      `}
+                    >
+                      Sender account number:{" "}
+                    </div>
+                    <div>{transaction.user_bank_account_number}</div>
+                  </div>
+                  <Button
+                    onClick={handleClickOpen}
+                    variant="contained"
+                    color="primary"
+                    size="medium"
+                    startIcon={<Photo />}
+                    data-testid="button-see-proof"
+                  >
+                    Bukti
+                  </Button>
+                </div>
+              )}
+            </div>
+          </div>
+        )}
+      </div>
+      <Dialog
+        maxWidth="xl"
+        open={dialogOpen}
+        onClose={handleClose}
+        aria-labelledby="max-width-dialog-title"
+      >
+        <DialogTitle id="max-width-dialog-title">Bukti Pembayaran</DialogTitle>
+        <DialogContent>
+          <img
+            css={css`
+              height: 80vh;
+              width: 80vw;
+              object-fit: contain;
+            `}
+            src={transaction.proof_of_payment}
+            alt="Bukti Pembayaran"
+          />
+        </DialogContent>
+        <DialogActions>
+          <Button
+            data-testid="button-close-proof"
+            onClick={handleClose}
+            color="primary"
+          >
+            Close
+          </Button>
+        </DialogActions>
+      </Dialog>
+    </div>
+  );
+};
+
+export default DetailTransaksi;
diff --git a/src/page/transaksi/FormStatus.jsx b/src/page/transaksi/FormStatus.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..e97a90fae126771ab1365aa230e19c86d791fa7d
--- /dev/null
+++ b/src/page/transaksi/FormStatus.jsx
@@ -0,0 +1,77 @@
+import React from "react";
+import { useForm } from "react-hook-form";
+import useSendData from "../../utils/useSendData";
+import {
+  ErrorDiv,
+  InputSelectForm,
+  LabelInput,
+  RowInput,
+} from "../../component/html/html";
+import { css } from "@emotion/core";
+import Button from "@material-ui/core/Button";
+import { Save } from "@material-ui/icons";
+
+const FormStatus = ({ idTransaksi, defaultStatus, paymentMethod }) => {
+  const url = `${process.env.REACT_APP_BASE_URL}/transactions/${idTransaksi}`;
+  const { register, handleSubmit } = useForm({
+    defaultValues: {
+      transaction_status: defaultStatus,
+    },
+  });
+  const [send, errorSend] = useSendData({
+    url,
+    header: { "Content-Type": "application/json" },
+    method: "PUT",
+    redirect: `/transaksi/`,
+  });
+
+  const onSubmit = (data) => {
+    send(JSON.stringify({ ...data }));
+  };
+  return (
+    <div data-testid="form-status">
+      {errorSend && <ErrorDiv>Status transaksi tidak dapat disimpan</ErrorDiv>}
+      <form
+        onSubmit={handleSubmit(onSubmit)}
+        css={css`
+          display: flex;
+        `}
+      >
+        <RowInput>
+          <LabelInput
+            htmlFor="status"
+            css={css`
+              margin-right: 1rem;
+            `}
+          />
+          <InputSelectForm
+            data-testid="dropdown-status"
+            id="status"
+            ref={register({ required: true })}
+            name="transaction_status"
+          >
+            {paymentMethod === "TRF" && (
+              <option value="001">Waiting for proof of payment</option>
+            )}
+            <option value="002">Waiting for seller confirmation</option>
+            <option value="003">In process</option>
+            <option value="004">Being shipped</option>
+            <option value="005">Completed</option>
+            <option value="006">Canceled</option>
+          </InputSelectForm>
+        </RowInput>
+        <Button
+          data-testid="button-submit-status"
+          type="submit"
+          variant="contained"
+          color="primary"
+          startIcon={<Save />}
+        >
+          Simpan
+        </Button>
+      </form>
+    </div>
+  );
+};
+
+export default FormStatus;
diff --git a/src/page/transaksi/ListTransaksi.jsx b/src/page/transaksi/ListTransaksi.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..d44596307fcee537fa05f0756dde652a59bd6c9e
--- /dev/null
+++ b/src/page/transaksi/ListTransaksi.jsx
@@ -0,0 +1,86 @@
+import React from "react";
+import TableComponent from "../../component/TableComponent";
+import { css } from "@emotion/core";
+import LinkYellow from "../../component/LinkYellow";
+import {
+  stringToCurrency,
+  stringToDate,
+  transactionToColoredStatus,
+} from "../../component/TableUtils";
+
+const ListTransaksi = () => {
+  const data = {
+    url: `${process.env.REACT_APP_BASE_URL}/transactions/`,
+    pageDefault: 1,
+    title: "",
+    keyValuePairs: [
+      ["id", "id"],
+      ["transaction_number", "ID Transaksi"],
+      ["user_username", "Username"],
+      ["created_at", "Tanggal Pembuatan", stringToDate],
+      ["updated_at", "Tanggal Update", stringToDate],
+      ["", "Status", transactionToColoredStatus],
+      ["subtotal", "Total", stringToCurrency],
+    ],
+    link: "/transaksi/",
+    filter: [
+      ["updated_at_date_range_after", "Updated from"],
+      ["updated_at_date_range_before", "Updated before"],
+      {
+        transaction_status: {
+          label: "Status transaksi",
+          choices: [
+            { "001": "Waiting for proof of payment" },
+            { "002": "Waiting for seller confirmation" },
+            { "003": "In process" },
+            { "004": "Being shipped" },
+            { "005": "Completed" },
+            { "006": "Canceled" },
+          ],
+        },
+      },
+    ],
+  };
+  return (
+    <div
+      css={css`
+        display: flex;
+        flex-direction: column;
+        margin: 2rem 3rem 3rem 3rem;
+      `}
+    >
+      <div
+        css={css`
+          font-size: 35px;
+        `}
+      >
+        KELOLA TRANSAKSI
+      </div>
+      <div
+        css={css`
+          width: 100%;
+          justify-content: space-between;
+          display: flex;
+          flex-direction: row;
+          margin-bottom: 1.8rem;
+          margin-top: 1rem;
+        `}
+      >
+        <div
+          css={css`
+            display: flex;
+            width: 35%;
+          `}
+        >
+          <LinkYellow to="/subkategori">SUBKATEGORI</LinkYellow>
+          <LinkYellow className="ml-2" to="/kategori">
+            KATEGORI
+          </LinkYellow>
+        </div>
+      </div>
+      <TableComponent {...data} />
+    </div>
+  );
+};
+
+export default ListTransaksi;
diff --git a/src/routes.jsx b/src/routes.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..10f043d398785a506e0acaff47a6177a9ccb35de
--- /dev/null
+++ b/src/routes.jsx
@@ -0,0 +1,80 @@
+import React from "react";
+import { Router } from "@reach/router";
+import ProtectedRoute from "./component/routes/ProtectedRoute";
+import Login from "./page/login/Login";
+import UnauthenticatedRoute from "./component/routes/UnauthenticatedRoute";
+import DetailPengguna from "./page/pengguna/DetailPengguna";
+import ListPengguna from "./page/pengguna/ListPengguna";
+import ListSubkategori from "./page/subkategori/ListSubkategori";
+import DetailSubkategori from "./page/subkategori/DetailSubkategori";
+import TambahSubkategori from "./page/subkategori/TambahSubkategori";
+import EditSubkategori from "./page/subkategori/EditSubkategori";
+import ListProduk from "./page/produk/ListProduk";
+import DetailProduk from "./page/produk/DetailProduk";
+import TambahProduk from "./page/produk/TambahProduk";
+import EditProduk from "./page/produk/EditProduk";
+
+import ListKategori from "./page/kategori/ListKategori";
+import DetailKategori from "./page/kategori/DetailKategori";
+import TambahKategori from "./page/kategori/TambahKategori";
+import EditKategori from "./page/kategori/EditKategori";
+import ListProgram from "./page/program/ListProgram";
+import DetailProgram from "./page/program/DetailProgram";
+import TambahProgram from "./page/program/TambahProgram";
+import EditProgram from "./page/program/EditProgram";
+
+import ListTransaksi from "./page/transaksi/ListTransaksi";
+import DetailTransaksi from "./page/transaksi/DetailTransaksi";
+
+const Placeholder = ({ children }) => children;
+
+const Routes = () => {
+  return (
+    <Router>
+      <Placeholder path="/produk">
+        <ProtectedRoute path="/" component={ListProduk} />
+        <ProtectedRoute path="tambah" component={TambahProduk} />
+        <ProtectedRoute path=":productId" component={DetailProduk} />
+        <ProtectedRoute path=":productId/ubah" component={EditProduk} />
+      </Placeholder>
+
+      <Placeholder path="/pengguna">
+        <ProtectedRoute path="/" component={ListPengguna} />
+        <ProtectedRoute path=":userId" component={DetailPengguna} />
+      </Placeholder>
+
+      <Placeholder path="subkategori">
+        <ProtectedRoute path="/" component={ListSubkategori} />
+        <ProtectedRoute path="tambah" component={TambahSubkategori} />
+        <ProtectedRoute path=":idSubKategori" component={DetailSubkategori} />
+        <ProtectedRoute
+          path=":idSubKategori/ubah"
+          component={EditSubkategori}
+        />
+      </Placeholder>
+
+      <Placeholder path="kategori">
+        <ProtectedRoute path="/" component={ListKategori} />
+        <ProtectedRoute path="tambah" component={TambahKategori} />
+        <ProtectedRoute path=":idKategori" component={DetailKategori} />
+        <ProtectedRoute path=":idKategori/ubah" component={EditKategori} />
+      </Placeholder>
+
+      <Placeholder path="program">
+        <ProtectedRoute path="/" component={ListProgram} />
+        <ProtectedRoute path="tambah" component={TambahProgram} />
+        <ProtectedRoute path=":idProgram" component={DetailProgram} />
+        <ProtectedRoute path=":idProgram/ubah" component={EditProgram} />
+      </Placeholder>
+
+      <Placeholder path="transaksi">
+        <ProtectedRoute path="/" component={ListTransaksi} />
+        <ProtectedRoute path=":idTransaksi" component={DetailTransaksi} />
+      </Placeholder>
+
+      <UnauthenticatedRoute path="/" component={Login} />
+    </Router>
+  );
+};
+
+export default Routes;
diff --git a/src/store/actions/action_types.js b/src/store/actions/action_types.js
new file mode 100644
index 0000000000000000000000000000000000000000..33f613b1530885b7144a3ef5f90a146ef4e3a79d
--- /dev/null
+++ b/src/store/actions/action_types.js
@@ -0,0 +1,3 @@
+export const LOGIN = "LOGIN";
+
+export const LOGOUT = "LOGOUT";
diff --git a/src/store/actions/actions.js b/src/store/actions/actions.js
new file mode 100644
index 0000000000000000000000000000000000000000..04814d1630ef759ced6d95c7ce2747f87849062e
--- /dev/null
+++ b/src/store/actions/actions.js
@@ -0,0 +1,14 @@
+import * as ACTION_TYPES from "./action_types";
+
+export const login = (profile) => {
+  return {
+    type: ACTION_TYPES.LOGIN,
+    profile: profile,
+  };
+};
+
+export const logout = () => {
+  return {
+    type: ACTION_TYPES.LOGOUT,
+  };
+};
diff --git a/src/store/reducers/auth_reducer.js b/src/store/reducers/auth_reducer.js
new file mode 100644
index 0000000000000000000000000000000000000000..c609d9f6856da368dd9f2ebed9b535e423028eb0
--- /dev/null
+++ b/src/store/reducers/auth_reducer.js
@@ -0,0 +1,37 @@
+import * as ACTION_TYPES from "../actions/action_types";
+
+export const initialState = () => {
+  if (localStorage.getItem("auth")) {
+    return JSON.parse(localStorage.getItem("auth"));
+  }
+  return {
+    is_authenticated: false,
+    profile: null,
+  };
+};
+
+const persistState = (profile) => {
+  const state = {
+    is_authenticated: true,
+    profile: profile,
+  };
+  localStorage.setItem("auth", JSON.stringify(state));
+  return state;
+};
+
+const AuthReducer = (state, action) => {
+  switch (action.type) {
+    case ACTION_TYPES.LOGIN:
+      return persistState(action.profile);
+    case ACTION_TYPES.LOGOUT:
+      window.localStorage.clear();
+      return {
+        is_authenticated: false,
+        profile: null,
+      };
+    default:
+      return state;
+  }
+};
+
+export default AuthReducer;
diff --git a/src/utils/contex.js b/src/utils/contex.js
new file mode 100644
index 0000000000000000000000000000000000000000..6b6b84fdda60ef34f30e3bde2c9b9a565da3e62a
--- /dev/null
+++ b/src/utils/contex.js
@@ -0,0 +1,7 @@
+import { createContext, useContext } from "react";
+
+export const useAuthContext = () => useContext(AuthContext);
+
+const AuthContext = createContext(null);
+
+export default AuthContext;
diff --git a/src/utils/useDelete.jsx b/src/utils/useDelete.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..7d034291f157b8ba89b182286e70efb38bc55df3
--- /dev/null
+++ b/src/utils/useDelete.jsx
@@ -0,0 +1,33 @@
+import { useCallback, useState } from "react";
+import { useAuthContext } from "./contex";
+import { navigate } from "@reach/router";
+import { trackPromise } from "react-promise-tracker";
+
+const useDelete = (url) => {
+  const { profile } = useAuthContext();
+  const [error, setErrorState] = useState(false);
+  const deleteFunction = useCallback(() => {
+    const controller = new AbortController();
+    setErrorState(false);
+    trackPromise(
+      fetch(url, {
+        method: "DELETE",
+        signal: controller.signal,
+        headers: {
+          Authorization: `Token ${profile.token}`,
+        },
+      })
+        .then((response) => {
+          if (response.ok) {
+            navigate("./");
+          } else {
+            throw new Error("Error");
+          }
+        })
+        .catch((e) => setErrorState(!(e instanceof DOMException)))
+    );
+    return () => controller.abort();
+  }, [url, profile.token]);
+  return [deleteFunction, error];
+};
+export default useDelete;
diff --git a/src/utils/useFetchList.jsx b/src/utils/useFetchList.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..8cc283050e1044f96f85a4ffe2167836c6f49d3a
--- /dev/null
+++ b/src/utils/useFetchList.jsx
@@ -0,0 +1,85 @@
+import { useEffect, useState } from "react";
+import { useAuthContext } from "./contex";
+import { trackPromise } from "react-promise-tracker";
+const useFetchList = ([
+  urlString,
+  defaultPage,
+  defaultSearch,
+  defaultSize,
+  argument,
+  filterString,
+]) => {
+  const [statePage, setPage] = useState(defaultPage);
+  const [stateSearch, setStateSearch] = useState(defaultSearch);
+  const [stateSize, setPageSize] = useState(defaultSize);
+  const [results, setResults] = useState([]);
+  const [errorState, setErrorState] = useState(false);
+  const [count, setCount] = useState(0);
+  const [filter, setFilter] = useState(filterString);
+  const { profile } = useAuthContext();
+  useEffect(() => {
+    const controller = new AbortController();
+    setErrorState(false);
+    let url = urlString.concat("?");
+    url =
+      statePage !== null && statePage !== undefined
+        ? url.concat(`page=${statePage}&`)
+        : url;
+    url =
+      stateSize !== null && stateSize !== undefined
+        ? url.concat(`page_size=${stateSize}&`)
+        : url;
+    url =
+      argument !== null && argument !== undefined
+        ? url.concat(`${argument}&`)
+        : url;
+    url =
+      filter !== null && filter !== undefined ? url.concat(`${filter}&`) : url;
+    url =
+      stateSearch !== null && stateSearch !== undefined
+        ? url.concat(`search=${stateSearch}`)
+        : url;
+    trackPromise(
+      fetch(url, {
+        method: "GET",
+        signal: controller.signal,
+        headers: {
+          Authorization: `Token ${profile.token}`,
+        },
+      })
+        .then((response) => {
+          if (response.ok) {
+            return response.json();
+          } else {
+            throw new Error("Error");
+          }
+        })
+        .then((result) => {
+          setResults(result["results"]);
+          setCount(result["count"]);
+        })
+        .catch((e) => setErrorState(!(e instanceof DOMException)))
+    );
+    return () => controller.abort();
+  }, [
+    statePage,
+    stateSearch,
+    stateSize,
+    argument,
+    urlString,
+    profile.token,
+    filter,
+  ]);
+  return [
+    results,
+    errorState,
+    count,
+    statePage,
+    stateSize,
+    setPage,
+    setStateSearch,
+    setPageSize,
+    setFilter,
+  ];
+};
+export default useFetchList;
diff --git a/src/utils/useFetchSingleData.jsx b/src/utils/useFetchSingleData.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..d53256cf15aa3b66b5a02add349aa31bf75d77d5
--- /dev/null
+++ b/src/utils/useFetchSingleData.jsx
@@ -0,0 +1,36 @@
+import { useEffect, useState } from "react";
+import { useAuthContext } from "./contex";
+import { trackPromise } from "react-promise-tracker";
+const useFetchSingleData = (url) => {
+  const [data, setData] = useState({});
+  const [error, setError] = useState(false);
+  const { profile } = useAuthContext();
+  useEffect(() => {
+    setError(false);
+    const controller = new AbortController();
+    trackPromise(
+      fetch(url, {
+        method: "GET",
+        signal: controller.signal,
+        headers: {
+          Authorization: `Token ${profile.token}`,
+        },
+      })
+        .then((response) => {
+          if (response.ok) {
+            return response.json();
+          } else {
+            throw new Error("Error");
+          }
+        })
+        .then((result) => {
+          setData(result);
+        })
+        .catch((e) => setError(!(e instanceof DOMException)))
+    );
+    return () => controller.abort;
+  }, [url, profile.token]);
+  return [data, error];
+};
+
+export default useFetchSingleData;
diff --git a/src/utils/useSendData.jsx b/src/utils/useSendData.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..a2465eaf1c2b18e5341d3776114d9e376b4ed253
--- /dev/null
+++ b/src/utils/useSendData.jsx
@@ -0,0 +1,39 @@
+import { useAuthContext } from "./contex";
+import { useCallback, useState } from "react";
+import { trackPromise } from "react-promise-tracker";
+import { navigate } from "@reach/router";
+
+const useSendData = ({ url, header = {}, redirect = "./", method }) => {
+  const { profile } = useAuthContext();
+  const [error, setErrorState] = useState(false);
+  const sendFunction = useCallback(
+    (data) => {
+      setErrorState(false);
+      const controller = new AbortController();
+      trackPromise(
+        fetch(url, {
+          method: method,
+          signal: controller.signal,
+          headers: {
+            Authorization: `Token ${profile.token}`,
+            ...header,
+          },
+          body: data,
+        })
+          .then((response) => {
+            if (response.ok) {
+              setErrorState(false);
+              navigate(redirect);
+            } else {
+              throw new Error("Error");
+            }
+          })
+          .catch((e) => setErrorState(!(e instanceof DOMException)))
+      );
+      return () => controller.abort();
+    },
+    [url, header, profile.token]
+  );
+  return [sendFunction, error];
+};
+export default useSendData;