feat: initial commit

This commit is contained in:
caturbgs
2021-12-09 09:01:39 +07:00
parent f8fdeefa22
commit f27ddcfe90
55 changed files with 16928 additions and 0 deletions

18
src/Main.js Normal file
View File

@@ -0,0 +1,18 @@
import React from "react";
import {BrowserRouter as Router} from "react-router-dom";
import {StoreProvider} from "./utils/useStore";
import {MainRoutes} from "./routes";
import "./custom.less";
// import "./Style.css";
import ParticlesBg from "particles-bg";
export const Main = () => {
return (
<StoreProvider>
<Router>
<MainRoutes/>
<ParticlesBg color={"#ababab"} num={20} type={"cobweb"} bg={true}/>
</Router>
</StoreProvider>
);
};

View File

@@ -0,0 +1,19 @@
import React from "react";
import {Breadcrumb} from "antd";
import {Link} from "react-router-dom";
export const BreadcumbComponent = (props) => {
return (
<div>
<Breadcrumb style={{marginBottom: 10}}>
{props.data.map((e) => (
<Breadcrumb.Item>
<Link to={e.route}>
<span>{e.name}</span>
</Link>
</Breadcrumb.Item>
))}
</Breadcrumb>
</div>
);
};

View File

@@ -0,0 +1,18 @@
import React from 'react';
import {Redirect, Route} from 'react-router-dom';
import {useStore} from "../utils/useStore";
// import { isLogin } from '../utils';
export const PrivateRoute = ({component: Component, ...rest}) => {
const store = useStore();
return (
// Show the component only when the user is logged in
// Otherwise, redirect the user to /signin page
<Route {...rest} render={props => (
store.authentication.isLoggedIn ?
<Component {...props} />
: <Redirect to="/login"/>
)}/>
);
};

View File

@@ -0,0 +1,18 @@
import React from 'react';
import {Redirect, Route} from 'react-router-dom';
import {useStore} from "../utils/useStore";
export const PublicRoute = ({component: Component, restricted, ...rest}) => {
const store = useStore();
return (
// restricted = false meaning public route
// restricted = true meaning restricted route
<Route {...rest} render={props => (
store.authentication.isLoggedIn && restricted ?
<Redirect to="/app"/>
: <Component {...props} />
)}/>
);
};

5
src/config/app.js Normal file
View File

@@ -0,0 +1,5 @@
export const appConfig = {
apiUrl: 'https://api-reconcile.movic.id'
};
//export default appConfig;

99
src/custom.less Normal file
View File

@@ -0,0 +1,99 @@
@import '~antd/dist/antd.less';
@font-size-base: 12px; // major text font size
//@primary-color: #ffd037; // primary color for all components
@primary-color1: #2e9dfe; // primary color for all components
@primary-color: #ed1f24; // primary color for all components
//@link-color: #58585b; // link color
@link-color1: #196bb1; // link color
@link-color: #ed1f24; // link color
@success-color: #52c41a; // success state color
@warning-color: #faad14; // warning state color
@error-color: #f5222d; // error state color
@heading-color: rgba(0, 0, 0, 0.85); // heading text color
@text-color: #1a1f36; // major text color
@text-color-secondary: #4f566b; // secondary text color
@disabled-color: #697386; // disable state color
@border-radius-base: 6px; // major border radius
@border-color-base: #d9d9d9; // major border color
@box-shadow-base: @shadow-2; // major shadow for layers
//@background-color-base: #e3e8ee;
@background-color-base: #e3e8ee;
@link-decoration: none;
body {
font-family: 'Montserrat Alternates', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
-webkit-font-smoothing: antialiased;
background-color: @background-color-base;
font-weight: 400;
color: #1a1f36;
}
// Layout
@layout-body-background: #e3e8ee;
@layout-header-background: #001529;
@layout-header-height: 64px;
@layout-header-padding: 0 50px;
@layout-header-color: @text-color;
@layout-footer-padding: 24px 50px;
@layout-footer-background: @layout-body-background;
@layout-sider-background: @layout-header-background;
@layout-trigger-height: 48px;
@layout-trigger-background: #002140;
@layout-trigger-color: #fff;
@layout-zero-trigger-width: 36px;
@layout-zero-trigger-height: 42px;
// Statistic
// ---
@statistic-content-font-size: 20px;
@statistic-unit-font-size: 11px;
// Typography
// ---
@typography-title-font-weight: 100;
@menu-collapsed-width: 40px;
@layout-header-padding: 0 24px;
@card-shadow: 0 7px 14px 0 rgba(60, 66, 87, 0.12), 0 3px 6px 0 rgba(0, 0, 0, 0.12);
@select-item-selected-bg: transparent;
@menu-item-active-bg: transparent;
@layout-header-background: @layout-body-background;
@menu-item-active-border-width: 0;
@select-selection-item-border-color: transparent;
@menu-inline-toplevel-item-height: 20px;
@menu-item-height: 20px;
@shadow-2: 0 7px 14px 0 rgba(60, 66, 87, 0.5), 0 3px 6px 0 rgba(241, 241, 241, 0.5);
// Menu
// ---
@menu-bg: transparent;
//Dropdown
@dropdown-menu-bg: transparent;
@btn-border-width: 0;
@btn-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.12) 0px 1px 1px 0px, rgba(60, 66, 87, 0.16) 0px 0px 0px 1px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(60, 66, 87, 0.12) 0px 2px 5px 0px;
// Button
@btn-height-base: 28px;
@btn-padding-horizontal-base: 8px;
@form-vertical-label-padding: 0 0 4px;
@page-header-padding: 16px 20px;
@no-shadow: none;
@table-header-bg: #f7fafc;

48
src/index.css Normal file
View File

@@ -0,0 +1,48 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
.ant-tabs-tab {
width: 150px !important;
text-align: center !important;
display: inline !important;
color: black !important;
}
.ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
color: #2D9CDB !important;
border-color: #2D9CDB !important;
}
.ant-card {
margin-bottom: 30px !important;
width: 1000px;
height: auto;
}
.ant-table-cell {
text-align: center !important;
}
.anticon anticon-down {
text-align: right !important;
}
.ant-btn > span + .anticon {
margin-left: 770px !important;
}
.ant-btn.ant-btn-primary {
background: #2D9CDB !important;
}

17
src/index.js Normal file
View File

@@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import reportWebVitals from './reportWebVitals';
import {Main} from "./Main";
ReactDOM.render(
<React.StrictMode>
<Main/>
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

20
src/pages/About/About.js Normal file
View File

@@ -0,0 +1,20 @@
import React from "react";
import {PageHeader} from "antd";
export const About = () => {
return <div>
<PageHeader
style={{
padding: 0,
margin: 0,
height: 40,
backgroundColor: "transparent",
}}
title={"About"}
>
</PageHeader>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Accusamus aut recusandae velit! Consequatur corporis,
eum fuga, harum incidunt laboriosam minus necessitatibus neque non nostrum pariatur tempore. Dignissimos impedit
rem tempora!
</div>
};

24
src/pages/App/App.js Normal file
View File

@@ -0,0 +1,24 @@
import React, {useEffect} from "react";
import {DesktopLayout} from "./DesktopLayout";
import {useMediaQuery} from 'react-responsive';
import {useStore} from "../../utils/useStore";
export const App = () => {
// TODO: add mobile layout
const store = useStore();
const mediaQuery = {
isDesktop: useMediaQuery({minWidth: 1024}),
isTablet: useMediaQuery({minWidth: 768, maxWidth: 1023}),
isMobile: useMediaQuery({maxWidth: 767}),
isNotMobile: useMediaQuery({minWidth: 768}),
};
useEffect(() => {
store.ui.setMediaQuery(mediaQuery);
});
// const isMobileDevice = useMediaQuery({
// query: "(min-device-width: 480px)",
// });
return <DesktopLayout/>;
};

View File

@@ -0,0 +1,151 @@
import React, {useState} from "react";
import {Button, Layout, Menu, Popover, Typography,} from "antd";
import {MenuList} from "./MenuList";
import {Link} from "react-router-dom";
import {PlusSquareOutlined, UserOutlined,} from "@ant-design/icons";
import {AppRoute} from "../../routes/app";
const {Text, Paragraph} = Typography;
const {Header, Content, Sider} = Layout;
export const DesktopLayout = () => {
const [clicked, setClicked] = useState(false);
return (
<Layout
theme={"light"}
className={"transparent"}
hasSider={true}
style={{
paddingLeft: 0,
display: "flex",
width: "100vw",
height: "100vh",
}}
>
<Sider
className={"transparent"}
width={240}
style={{
overflowX: "hidden",
bottom: 0,
justifyContent: "flex-start",
paddingTop: 20,
paddingLeft: 20,
position: "fixed",
top: 0,
zIndex: 10,
}}
>
<div
style={{
paddingLeft: 20,
marginBottom: 40,
}}
>
<Paragraph
style={{
margin: 0,
padding: 0,
fontSize: 20,
marginLeft: 5,
fontWeight: 600,
color: "#828282",
}}
>
PPOB
</Paragraph>
<Paragraph
style={{
margin: 0,
padding: 0,
fontSize: 11,
marginLeft: 5,
fontWeight: 600,
color: "#413d3e",
}}
>
Admin
</Paragraph>
</div>
<MenuList closeLeftDrawer={() => {
}}/>
</Sider>
<Layout
style={{
paddingLeft: 240,
}}
>
<Header
theme={"light"}
style={{
height: 54,
paddingLeft: 0,
paddingRight: 0,
backgroundColor: "transparent",
maxWidth: 1000,
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<Button style={{right: "94%"}}>
<PlusSquareOutlined/>
</Button>
<Popover
autoAdjustOverflow={true}
placement="bottomRight"
content={
<Menu
type={"line"}
inlineIndent={0}
theme="light"
style={{
backgroundColor: "transparent",
borderRightWidth: 0,
right: "30%",
}}
mode="inline"
>
<Menu.Item>
<Link to="/app/profile">
<span>Profile</span>
</Link>
</Menu.Item>
<Menu.Item
onClick={() => {
// store.authentication.logout();
// return history.push("/login");
}}
>
<span>Sign out</span>
</Menu.Item>
</Menu>
}
title={
<Text>
info@bangun-kreatif.com
{/*{store.userData.email}{" "}*/}
<Paragraph style={{fontWeight: 400}} type={"secondary-dark"}>
Administrator
{/*{store.userData.role}*/}
</Paragraph>
</Text>
}
trigger="click"
visible={clicked}
onVisibleChange={() => setClicked(!clicked)}
>
<Button
size={"default"}
style={{}}
icon={<UserOutlined style={{fontSize: "13px"}}/>}
/>
</Popover>
</Header>
<AppRoute/>
</Layout>
</Layout>
);
};

69
src/pages/App/MenuList.js Normal file
View File

@@ -0,0 +1,69 @@
import React, {useEffect, useState} from "react";
import {Menu} from "antd";
import {Link} from "react-router-dom";
import {CalendarOutlined, HomeOutlined,} from "@ant-design/icons";
import {observer} from "mobx-react-lite";
import {useStore} from "../../utils/useStore";
const {SubMenu} = Menu;
export const MenuList = observer((props) => {
const store = useStore();
useEffect(() => {
}, []);
const [setKeys, setSetKeys] = useState(["dashboard"]);
return (
<Menu
defaultOpenKeys={["sub4"]}
theme="light"
style={{
backgroundColor: "transparent",
borderRightWidth: 0,
fontWeight: 400,
paddingLeft: 0,
}}
onClick={({keyPath, item}) => {
props.closeLeftDrawer();
}}
mode="inline"
selectedKeys={setKeys}
onSelect={({setKeys, item, selectedKeys}) => setSetKeys(selectedKeys)}
overflowedIndicator={0}
forceSubMenuRender={true}
>
<Menu.Item key="home">
<Link to={'/app/home'}>
<HomeOutlined/>
<span>Home</span>
</Link>
</Menu.Item>
<Menu.Item key="membership">
<Link to={'/app/membership'}>
<HomeOutlined/>
<span>Membership</span>
</Link>
</Menu.Item>
<Menu.Item key="product">
<Link to={'/app/product'}>
<HomeOutlined/>
<span>Product</span>
</Link>
</Menu.Item>
<Menu.Item key="product">
<Link to={'/app/transaction'}>
<HomeOutlined/>
<span>Transaction</span>
</Link>
</Menu.Item>
<Menu.Item key="about">
<Link to={'/app/about'}>
<CalendarOutlined/>
<span>About</span>
</Link>
</Menu.Item>
<Menu.Divider style={{background: "transparent", paddingTop: 15}}/>
</Menu>
);
});

25
src/pages/Home/Home.js Normal file
View File

@@ -0,0 +1,25 @@
import React from "react";
import {Button, PageHeader} from "antd";
import {store} from "../../utils/useStore";
import {observer} from "mobx-react-lite";
export const Home = observer(() => {
return <div>
<PageHeader
style={{
padding: 0,
margin: 0,
height: 40,
backgroundColor: "transparent",
}}
title={"Home"}
>
</PageHeader>
<Button onClick={() => {
store.ui.setTestValue();
}}>{store.ui.testValue}</Button>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Accusamus aut recusandae velit! Consequatur corporis,
eum fuga, harum incidunt laboriosam minus necessitatibus neque non nostrum pariatur tempore. Dignissimos impedit
rem tempora!
</div>
});

139
src/pages/Login/Login.js Normal file
View File

@@ -0,0 +1,139 @@
import React, {useState} from "react";
import {observer} from 'mobx-react-lite';
import {useStore} from "../../utils/useStore";
import {Button, Card, Checkbox, Col, Form, Input, Row, Typography} from 'antd';
import {LockOutlined, UserOutlined} from '@ant-design/icons';
import {useHistory} from "react-router-dom";
export const Login = observer(() => {
const store = useStore();
const [loading, setLoading] = useState(false);
let history = useHistory();
const onFinish = values => {
console.log('Received values of form: ', values);
enterLoading(values).then(res => {
console.log(res, "awasaa");
}).catch((error) => {
console.log({error}, "awasaa error");
});
};
const enterLoading = async (props) => {
// store.setInitialToken("ayayay", "clap");
return history.push("/app/page_example_1");
};
return <div style={{width: '100vw', display: 'flex', justifyContent: 'center'}}>
<Row justify={'center'}>
<Col>
<div style={{
display: 'flex',
justifyContent: 'flex-start',
marginTop: '5vh',
flexDirection: 'column',
alignItems: 'center',
}}>
<div style={{display: 'flex', flexDirection: 'column', alignItems: 'stretch'}}>
<Typography.Paragraph
style={{
margin: 0,
padding: 0,
fontSize: 20,
marginLeft: 5,
fontWeight: 600,
color: "#413d3e",
}}
>
Boilerplate
</Typography.Paragraph>
</div>
<Card
style={{width: 320, textAlign: 'center'}}
headStyle={{fontSize: 13, fontWeight: 200}}
className={"shadow"}
bordered={true}
title={'Sign in to your account'}
>
<Form
layout={'vertical'}
name="normal_login"
className="login-form"
onFinish={onFinish}
>
<Form.Item
label="Email"
name="email"
size={'large'}
rules={[{required: false, message: 'Please input your Username!'}]}
>
<Input
prefix={<UserOutlined className="site-form-item-icon"/>}
type="text"
placeholder="Email"/>
</Form.Item>
<Form.Item
style={{
marginBottom: 0,
}}
label="Password"
name="password"
size={'large'}
rules={[{required: false, message: 'Please input your Password!'}]}
>
<Input.Password
prefix={<LockOutlined className="site-form-item-icon"/>}
type="password"
placeholder="Password"
/>
</Form.Item>
<Form.Item
style={{
marginTop: 0,
marginBottom: 20,
padding: 0
}}
// label="Password"
name="forgot-password"
size={'small'}
rules={[{required: false, message: 'Please input your Password!'}]}
>
<a className="login-form-forgot" href="">
Forgot password
</a>
</Form.Item>
<Form.Item
style={{
marginBottom: 5,
textAlign: 'left'
}}>
<Form.Item name="remember" valuePropName="checked" noStyle>
<Checkbox>Remember me</Checkbox>
</Form.Item>
</Form.Item>
<Form.Item
style={{
marginBottom: 0,
}}>
<Button type="primary"
block
loading={loading}
htmlType="submit"
size={'large'}
onSubmit={enterLoading}
className="login-form-button">
Sign In
</Button>
</Form.Item>
</Form>
</Card>
</div>
</Col>
</Row>
</div>;
});

View File

@@ -0,0 +1,280 @@
import React, {useEffect, useState} from "react";
import {Button, Card, Col, Input, List, message, Modal, PageHeader, Row, Space, Table, Tag} from "antd";
import {useStore} from "../../utils/useStore";
import {observer} from "mobx-react-lite";
import {ExclamationCircleOutlined, FilterOutlined, PlusSquareOutlined,} from "@ant-design/icons";
import {MembershipModal} from "./MembershipModal";
import {BreadcumbComponent} from "../../component/BreadcumbComponent";
const {Search} = Input;
export const Membership = observer(() => {
const store = useStore();
const [visibleModal, setVisibleModal] = useState(false)
const [initialData, setInitialData] = useState({})
const [confirmLoading, setConfirmLoading] = useState(false);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const init = async () => {
try {
setIsLoading(true)
await store.membership.getData();
setIsLoading(false)
} catch (e) {
setIsLoading(false)
}
}
init()
}, []);
const columns = [
{
title: "Name",
dataIndex: "name",
key: "name",
},
{
title: "Username",
dataIndex: "username",
key: "username",
},
{
title: "Status",
dataIndex: "status",
key: "status",
render: (text, record) => (
<>
<Tag color="#E3E8EE" style={{color: "#4F566B"}}>
Inactive
</Tag>
<Tag color="processing">Active</Tag>
</>
)
},
{
title: "Action",
key: "action",
render: (text, record) => (
<Space size="middle">
<Button onClick={() => {
setVisibleModal(true)
setInitialData(record)
}}>Edit</Button>
<Button onClick={async () => {
handleDelete(record.id)
}}
>Delete</Button>
</Space>
),
},
];
const routeData = [
{
route: "/app/home",
name: "Home",
},
{
route: "/app/membership",
name: <span style={{fontWeight: "bold"}}>Membership</span>,
},
];
const onSubmit = async (data) => {
if (initialData.id) {
setInitialData({})
setConfirmLoading(true);
try {
await store.membership.update(initialData.id, data)
message.success("Success Update Data Member")
} catch (e) {
message.error("Failed Update Data Member")
}
setConfirmLoading(false);
setVisibleModal(false);
} else {
setInitialData({})
setConfirmLoading(true);
try {
await store.membership.create(data)
message.success("Success Add New Member")
} catch (e) {
console.log(e, "apa errornya")
message.error("Failed Add Member")
}
setConfirmLoading(false);
setVisibleModal(false);
}
}
const handleDelete = (record) => {
Modal.confirm({
title: 'Are you sure reject this record?',
icon: <ExclamationCircleOutlined/>,
okText: 'Yes',
okType: 'primary',
cancelText: 'Cancel',
onOk() {
try {
//TODO: minta apinya ke ka ilham ya, jangan di uncomment kalo pake api reconcile, nanti beneran ke apus datanya
// await store.membership.delete(record.id)
message.success('Success Delete Data')
} catch (e) {
message.error("Failed Delete Data")
}
},
onCancel() {
console.log('Cancel');
},
});
}
return (
<div>
<BreadcumbComponent data={routeData}/>
<Card>
{store.ui.mediaQuery.isDesktop && (
<div>
<Row style={{marginBottom: 20}}>
<Col span={12}>
<Button>
<FilterOutlined/>
Filter
</Button>
</Col>
<Col span={12} style={{textAlign: "right"}}>
<Search
placeholder="input search text"
style={{width: 200, marginRight: 10}}
/>
<Button onClick={() => setVisibleModal(true)}>
<PlusSquareOutlined/> New
</Button>
</Col>
</Row>
<Table
style={{textAlign: "center"}}
columns={columns}
dataSource={store.membership.data}
bordered
pagination={{
total: store.membership.total_data,
current: store.membership.page,
pageSize: store.membership.pageSize,
simple: true
}} onChange={(page) => {
store.membership.pageSize = page.pageSize;
store.membership.page = page.current;
store.membership.getData();
}} current={store.membership.page}
loading={store.membership.pageSize}/>
</div>
)}
{store.ui.mediaQuery.isMobile && (
<div>
<Card bordered={false} bodyStyle={{padding: "0"}} style={{borderRadius: 0, width: '50%'}}>
<PageHeader
className={"card-page-header"}
style={{
padding: "6px 8px",
}}
title={
<Button
icon={<FilterOutlined/>}
size={"small"}
style={{margin: 3}}
>
Filter
</Button>
}
subTitle=""
/>
</Card>
<List
itemLayout="horizontal"
position={"top"}
dataSource={store.membership.data}
style={{padding: 0}}
renderItem={(item) => {
console.log(item, "item ->");
return (
<div>
<List.Item
key={item.key}
style={{
backgroundColor: "#ffffff",
paddingTop: 0,
paddingBottom: 0,
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
width: "50%"
}}
>
<List.Item.Meta
className={["cariparkir-container"].join(" ")}
title={<h3 style={{marginBottom: 0, color: "#5469d4"}}>{item.name}</h3>}
description={
<div style={{}}>
<p>
<small>Username : {item.username}</small> <br/>
</p>
<p>
{item.status}
</p>
</div>
}
/>
{/* <div style={{ marginRight: 16 }}>
<Statistic
title={
<p
style={{
fontSize: 9,
margin: 0,
}}
>
{item.updated_at ? moment(item.updated_at).format("DD MMM YY, H:mm:ss") : "There is not top up yet"}
</p>
}
prefix={"Rp"}
precision={0}
style={{ fontSize: 12, fontWeight: 300 }}
valueStyle={{
color: "#5469d4",
fontSize: 12,
fontWeight: 600,
textAlign: "right",
}}
value={item.balances}
/>
</div> */}
</List.Item>
{/* <Divider plain style={{ margin: 0 }} /> */}
</div>
);
}}
/>
</div>
)}
</Card>
<MembershipModal visible={visibleModal}
confirmLoading={confirmLoading}
initialData={initialData}
onCreate={async (data) => {
onSubmit(data)
}}
onCancel={() => {
setInitialData({})
setVisibleModal(false);
}}/>
</div>
);
});

View File

@@ -0,0 +1,82 @@
import React from 'react';
import {Form, Input, Modal, Select,} from 'antd';
export const MembershipModal = ({
visible,
onCreate,
onCancel,
initialData,
}) => {
const [form] = Form.useForm();
const {Option} = Select;
const dataStatus = ['Active', 'Inactive']
return (
<Modal
visible={visible}
title={initialData.id ? "Edit Member" : "Create a new Member"}
okText={initialData.id ? "Edit" : "Create"}
cancelText="Cancel"
onCancel={() => {
form.resetFields()
onCancel()
}}
onOk={() => {
form
.validateFields()
.then(values => {
onCreate(values);
form.resetFields()
})
.catch(info => {
console.log('Validate Failed:', info);
});
}}
>
<Form
form={form}
layout="vertical"
name="form_in_modal"
initialValues={initialData}
>
<Form.Item
name="name"
label="Name"
rules={[{required: true, message: 'Please input Name!'}]}
>
<Input/>
</Form.Item>
<Form.Item
name="username"
label="Username"
rules={[{required: true, message: 'Please input Username!'}]}
>
<Input/>
</Form.Item>
<Form.Item
name="status"
label="Status"
rules={[{required: true, message: 'Please select Status!'}]}
>
<Select
showSearch
placeholder="Select Status"
optionFilterProp="children"
filterOption={(input, option) =>
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
filterSort={(optionA, optionB) =>
optionA.children.toLowerCase().localeCompare(optionB.children.toLowerCase())
}
>
{dataStatus.map(it => {
return <Option value={it}>{it}</Option>
})}
</Select>
</Form.Item>
</Form>
</Modal>
);
};

View File

@@ -0,0 +1,70 @@
import React from "react";
import {Button, Card, Col, Input, Row, Tabs} from "antd";
import {FilterOutlined, PlusSquareOutlined,} from "@ant-design/icons";
import {BreadcumbComponent} from "../../component/BreadcumbComponent";
import {Pulsa} from "./Pulsa";
const {TabPane} = Tabs;
const {Search} = Input;
export const Product = () => {
const callback = (key) => {
console.log(key);
};
const routeData = [
{
route: "/app/home",
name: "Home",
},
{
route: "/app/product",
name: <span style={{fontWeight: 'bold'}}>Product</span>,
},
];
return (
<div>
<BreadcumbComponent data={routeData}/>
<Card>
<Row style={{marginBottom: 20}}>
<Col span={12}>
<Button>
<FilterOutlined/>
Filter
</Button>
</Col>
<Col span={12} style={{textAlign: "right"}}>
<Search
placeholder="input search text"
style={{width: 200, marginRight: 10}}
/>
<Button>
<PlusSquareOutlined/> New
</Button>
</Col>
</Row>
<Tabs
defaultActiveKey="1"
onChange={callback}
size="default"
tabBarGutter="50"
>
<TabPane tab="Pulsa" key="1">
<Pulsa/>
</TabPane>
<TabPane tab="Game Voucher" key="2">
Game Voucher
</TabPane>
<TabPane tab="Product" key="3">
Product
</TabPane>
<TabPane tab="Prduct" key="4">
Prduct
</TabPane>
<TabPane tab="Prdct" key="5">
Prdct
</TabPane>
</Tabs>
</Card>
</div>
);
};

100
src/pages/Product/Pulsa.js Normal file
View File

@@ -0,0 +1,100 @@
import React from "react";
import {Button, Space, Table, Tag} from "antd";
export const Pulsa = () => {
const columns = [
{
title: "Kode",
dataIndex: "kode",
key: "kode",
},
{
title: "Produk",
dataIndex: "produk",
key: "produk",
},
{
title: "Harga Beli",
dataIndex: "harga_beli",
key: "harga_beli",
},
,
{
title: "Harga Jual",
dataIndex: "harga_jual",
key: "harga_beli",
},
{
title: "Gangguan",
dataIndex: "gangguan",
key: "gangguan",
},
{
title: "Tersedia",
dataIndex: "tersedia",
key: "tersedia",
},
{
title: "Action",
key: "action",
render: (text, record) => (
<Space size="middle">
<Button>Edit</Button>
<Button>Delete</Button>
</Space>
),
},
];
const dataSource = [
{
key: "1",
kode: "BROP2",
produk: "DATA AXIS 2GB-60HR",
harga_beli: "Rp.10.000",
harga_beli: "Rp.10.000",
harga_jual: "Rp.40.000",
gangguan: <Tag color="processing">Active</Tag>,
tersedia: <Tag color="processing">Ya</Tag>,
},
{
key: "2",
kode: "-",
produk: "-",
harga_beli: "-",
harga_beli: "-",
harga_jual: "-",
gangguan: <Tag color="#E3E8EE" style={{color: '#4F566B'}}>Gangguan</Tag>,
tersedia: <Tag color="#E3E8EE" style={{color: '#4F566B'}}>Tidak</Tag>,
},
{
key: "3",
kode: "-",
produk: "-",
harga_beli: "-",
harga_beli: "-",
harga_jual: "-",
gangguan: <Tag color="processing">Active</Tag>,
tersedia: <Tag color="processing">Ya</Tag>,
},
{
key: "4",
kode: "-",
produk: "-",
harga_beli: "-",
harga_beli: "-",
harga_jual: "-",
gangguan: <Tag color="processing">Active</Tag>,
tersedia: <Tag color="processing">Ya</Tag>,
}
];
return (
<div>
<Table
style={{textAlign: "center"}}
columns={columns}
dataSource={dataSource}
bordered
/>
</div>
);
};

View File

@@ -0,0 +1,127 @@
import React from "react";
import {Button, Card, Col, Dropdown, Menu, message, Modal, Row, Space,} from "antd";
import {DownOutlined, TabletOutlined, UserOutlined} from "@ant-design/icons";
//const style = { background: "#0092ff", padding: "8px 0" };
const gridStyle = {
width: "18%",
textAlign: "center",
marginRight: "8px",
marginBottom: "20px",
};
export const Pulsa = () => {
function handleMenuClick(e) {
message.info("Click on menu item.");
console.log("click", e);
}
const menu = (
<Menu onClick={handleMenuClick}>
<Menu.Item key="1" icon={<UserOutlined/>}>
1st menu item
</Menu.Item>
<Menu.Item key="2" icon={<UserOutlined/>}>
2nd menu item
</Menu.Item>
<Menu.Item key="3" icon={<UserOutlined/>}>
3rd menu item
</Menu.Item>
</Menu>
);
function success() {
Modal.success({
content: 'some messages...some messages...',
});
}
return (
<div>
<Row>
<span style={{fontWeight: "bold", marginBottom: "10px"}}>
Sub-Category
</span>
</Row>
<Row>
<Space wrap>
<Dropdown overlay={menu}>
<Button
style={{
marginBottom: "20px",
color: "grey",
}}
>
<TabletOutlined/>
Select sub-Category
<DownOutlined/>
</Button>
</Dropdown>
</Space>
</Row>
<Row>
<span style={{fontWeight: "bold", marginBottom: "10px"}}>
Produk & Nominal
</span>
</Row>
<Card>
<Card.Grid style={gridStyle} onClick={success}>
<span style={{color: "black"}}>DATA AXIS BRONET 2GB-60HR</span>
<br/>
<span style={{color: "grey", fontSize: 10}}>Harga : Rp.6.000</span>
</Card.Grid>
<Card.Grid style={gridStyle}>
<span style={{color: "black"}}>DATA AXIS BRONET 2GB-60HR</span>
<br/>
<span style={{color: "grey", fontSize: 10}}>Harga : Rp.6.000</span>
</Card.Grid>
<Card.Grid style={gridStyle}>
<span style={{color: "black"}}>DATA AXIS BRONET 2GB-60HR</span>
<br/>
<span style={{color: "grey", fontSize: 10}}>Harga : Rp.6.000</span>
</Card.Grid>
<Card.Grid style={gridStyle}>
<span style={{color: "black"}}>DATA AXIS BRONET 2GB-60HR</span>
<br/>
<span style={{color: "grey", fontSize: 10}}>Harga : Rp.6.000</span>
</Card.Grid>
<Card.Grid style={gridStyle}>
<span style={{color: "black"}}>DATA AXIS BRONET 2GB-60HR</span>
<br/>
<span style={{color: "grey", fontSize: 10}}>Harga : Rp.6.000</span>
</Card.Grid>
<Card.Grid style={gridStyle}>
<span style={{color: "black"}}>DATA AXIS BRONET 2GB-60HR</span>
<br/>
<span style={{color: "grey", fontSize: 10}}>Harga : Rp.6.000</span>
</Card.Grid>
<Card.Grid style={gridStyle}>
<span style={{color: "black"}}>DATA AXIS BRONET 2GB-60HR</span>
<br/>
<span style={{color: "grey", fontSize: 10}}>Harga : Rp.6.000</span>
</Card.Grid>
<Card.Grid style={gridStyle}>
<span style={{color: "black"}}>DATA AXIS BRONET 2GB-60HR</span>
<br/>
<span style={{color: "grey", fontSize: 10}}>Harga : Rp.6.000</span>
</Card.Grid>
<Card.Grid style={gridStyle}>
<span style={{color: "black"}}>DATA AXIS BRONET 2GB-60HR</span>
<br/>
<span style={{color: "grey", fontSize: 10}}>Harga : Rp.6.000</span>
</Card.Grid>
<Card.Grid style={gridStyle}>
<span style={{color: "black"}}>DATA AXIS BRONET 2GB-60HR</span>
<br/>
<span style={{color: "grey", fontSize: 10}}>Harga : Rp.6.000</span>
</Card.Grid>
</Card>
<Col style={{textAlign: "right"}}>
<Button style={{backgroundColor: "#2D9CDB", color: "white"}}>
Beli Sekarang
</Button>
</Col>
</div>
);
};

View File

@@ -0,0 +1,67 @@
import React from "react";
import {Button, Card, Col, Input, Row, Tabs} from "antd";
import {FilterOutlined,} from "@ant-design/icons";
import {BreadcumbComponent} from "../../component/BreadcumbComponent";
import {Pulsa} from "./Pulsa";
const {TabPane} = Tabs;
const {Search} = Input;
export const Transaction = () => {
const callback = (key) => {
console.log(key);
};
const routeData = [
{
route: "/app/home",
name: "Home",
},
{
route: "/app/transaction",
name: <span style={{fontWeight: 'bold'}}>Transaction</span>,
},
];
return (
<div>
<BreadcumbComponent data={routeData} text=""/>
<Card>
<Row style={{marginBottom: 20}}>
<Col span={12}>
<Button>
<FilterOutlined/>
Filter
</Button>
</Col>
<Col span={12} style={{textAlign: "right"}}>
<Search
placeholder="input search text"
style={{width: 200, marginRight: 10}}
/>
</Col>
</Row>
<Tabs
defaultActiveKey="1"
onChange={callback}
size="default"
tabBarGutter="50"
>
<TabPane tab="Pulsa" key="1">
<Pulsa/>
</TabPane>
<TabPane tab="Game Voucher" key="2">
Game Voucher
</TabPane>
<TabPane tab="Product" key="3">
Product
</TabPane>
<TabPane tab="Prduct" key="4">
Prduct
</TabPane>
<TabPane tab="Prdct" key="5">
Prdct
</TabPane>
</Tabs>
</Card>
</div>
);
};

13
src/reportWebVitals.js Normal file
View File

@@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({getCLS, getFID, getFCP, getLCP, getTTFB}) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

29
src/routes/app.js Normal file
View File

@@ -0,0 +1,29 @@
import {Redirect, Route, Switch} from "react-router-dom";
import {Home} from "../pages/Home/Home";
import {About} from "../pages/About/About";
import {Membership} from "../pages/Membership/Membership";
import {Product} from "../pages/Product/Product";
import {Transaction} from "../pages/Transaction/Transaction";
export const AppRoute = () => {
return <Switch>
<Route path={"/app/home"}>
<Home/>
</Route>
<Route path={"/app/membership"}>
<Membership/>
</Route>
<Route path={"/app/product"}>
<Product/>
</Route>
<Route path={"/app/transaction"}>
<Transaction/>
</Route>
<Route path={"/app/about"}>
<About/>
</Route>
<Route path="/app" exact>
<Redirect to={'/app/home'}/>
</Route>
</Switch>
}

16
src/routes/index.js Normal file
View File

@@ -0,0 +1,16 @@
import {Redirect, Route, Switch} from "react-router-dom";
import {Login} from "../pages/Login/Login";
import {PublicRoute} from "../component/PublicRoute";
import {App} from "../pages/App/App";
export const MainRoutes = (props) => {
return (
<Switch>
<Route path="/" exact>
<Redirect to={"/app/home"}/>
</Route>
<PublicRoute restricted={true} component={Login} path="/login" exact/>
<PublicRoute component={App} path="/app"/>
</Switch>
);
};

5
src/setupTests.js Normal file
View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

View File

@@ -0,0 +1,27 @@
import {makeAutoObservable} from "mobx";
export class Authentication {
ctx;
accessToken = '';
refreshToken = '';
constructor(ctx) {
this.ctx = ctx;
makeAutoObservable(this);
}
get isLoggedIn() {
return !!this.refreshToken;
}
setInitialToken(accessToken, refreshToken) {
this.setToken(accessToken, refreshToken);
}
setToken(accessToken, refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}

14
src/store/index.js Normal file
View File

@@ -0,0 +1,14 @@
import {UI} from "./ui";
import {Authentication} from "./authentication";
import {User} from "./user";
import {Membership} from "./membership";
export class Store {
ui = new UI(this);
authentication = new Authentication(this);
user = new User(this);
membership = new Membership(this);
constructor() {
}
}

37
src/store/membership.js Normal file
View File

@@ -0,0 +1,37 @@
import {action, makeAutoObservable} from "mobx";
import {http} from "../utils/http";
export class Membership {
page = 1;
pageSize = 10
data = [];
total_data = 0
constructor(ctx) {
this.ctx = ctx;
makeAutoObservable(this);
}
@action
async getData() {
const response = await http.get(`/user?page=${this.page}&pageSize=${this.pageSize}`);
this.data = response.body.data ?? []
this.total_data = response.body.total_data ?? 0
}
@action
async create(data) {
return await http.post('/user').send(data)
}
@action
async update(id, data) {
return await http.put('/user/' + id).send(data);
}
async delete(id) {
return await http.del('/user/' + id);
}
}

25
src/store/ui.js Normal file
View File

@@ -0,0 +1,25 @@
import {makeAutoObservable} from "mobx";
export class UI {
mediaQuery = {
isMobile: false,
isDesktop: true
};
testValue = "Test Mobx";
constructor(ctx) {
this.ctx = ctx;
makeAutoObservable(this);
}
setTestValue() {
this.testValue = "yo dayo";
}
setMediaQuery(data) {
if (this.mediaQuery.isDesktop !== data.isDesktop || this.mediaQuery.isMobile !== data.isMobile) {
this.mediaQuery = data;
}
};
}

13
src/store/user.js Normal file
View File

@@ -0,0 +1,13 @@
import {action, observable} from "mobx";
import {http} from "../utils/http";
export class User {
@observable data = [];
@action
async getData() {
this.data = (await http.get('/user')).body.data;
}
}

169
src/utils/http.js Normal file
View File

@@ -0,0 +1,169 @@
import superagent from "superagent";
import {appConfig} from "../config/app";
import {store} from "./useStore";
export const http = {
get: (url, opts = {}) => {
let req = superagent.get(appConfig.apiUrl + url);
if (store.token) {
req = req.set('Authorization', 'Bearer ' + store.token);
}
return req;
},
post: (url, opts) => {
let req = superagent.post(appConfig.apiUrl + url);
if (store.token) {
req = req.set('Authorization', 'Bearer ' + store.token);
}
return req;
},
put: (url, opts) => {
let req = superagent.put(appConfig.apiUrl + url);
if (store.token) {
req = req.set('Authorization', 'Bearer ' + store.token);
}
return req;
},
del: (url, opts) => {
let req = superagent.del(appConfig.apiUrl + url);
if (store.token) {
req = req.set('Authorization', 'Bearer ' + store.token);
}
return req;
},
upload: (file) => {
const request = superagent
.post(appConfig.apiUrl + '/files')
.attach('file', file);
return request;
},
uploadAntd: (args) => {
const file = args.file;
const request = http.upload(file);
request
.on('progress', event => {
args.onProgress(event);
})
.then(it => {
args.onSuccess(it);
}).catch(err => {
args.onError(err);
});
return request;
}
};
// import superagent from "superagent";
// import appConfig from "../Config/StaticVar";
// import { TokenUtil } from './token'
// import { navigationService } from './NavigationService'
// import { Alert } from 'react-native'
// import StaticVar from '../Config/StaticVar'
// import AsyncStorage from '@react-native-async-storage/async-storage'
// import FirebaseLib from '../Lib/FirebaseLib'
// let AuthIntercept = require('superagent-intercept')((err, res) => {
// if (res?.status === 403) {
// console.log("masuk sini ya")
// Alert.alert("Sesi Anda Telah Habis", "Sesi Anda Telah Habis, Harap Login Kembali.", [
// { text: "Oke", onPress: async () => {
// navigationService.reset('LoginScreen');
// await AsyncStorage.removeItem(StaticVar.DB_PROFILE)
// await FirebaseLib.signOut()
// }}
// ]);
// }
// });
// export const http = {
// get: (url, opts = {}) => {
// try{
// let req = superagent.get(appConfig.API_URL + url)
// .use(AuthIntercept)
// if (TokenUtil.accessToken) {
// req = req.set('Authorization', 'Bearer ' + TokenUtil.accessToken);
// }
// return req;
// }catch (e) {
// console.log(e, "error get superagent")
// throw e
// }
// },
// post: (url, opts) => {
// try{
// let req = superagent.post(appConfig.API_URL + url)
// .use(AuthIntercept)
// if (TokenUtil.accessToken) {
// req = req.set('Authorization', 'Bearer ' + TokenUtil.accessToken);
// }
// return req;
// }catch (e) {
// console.log(e, "error post superagent")
// throw e
// }
// },
// put: (url, opts) => {
// try{
// let req = superagent.put(appConfig.API_URL + url)
// .use(AuthIntercept)
// if (TokenUtil.accessToken) {
// req = req.set('Authorization', 'Bearer ' + TokenUtil.accessToken);
// }
// return req;
// }catch (e) {
// console.log(e, "error put superagent")
// throw e
// }
// },
// del: (url, opts) => {
// try{
// let req = superagent.del(appConfig.API_URL + url)
// .use(AuthIntercept)
// if (TokenUtil.accessToken) {
// req = req.set('Authorization', 'Bearer ' + TokenUtil.accessToken);
// }
// return req;
// }catch (e) {
// console.log(e, "error del superagent")
// throw e
// }
// },
// upload: (file) => {
// try{
// const request = superagent
// .post(appConfig.API_URL + '/v1/files')
// .use(AuthIntercept)
// .attach('file', file);
// return request;
// }catch (e) {
// console.log(e, "error upload superagent")
// throw e
// }
// },
// uploadAntd: (args) => {
// try{
// const file = args.file;
// const request = http.upload(file)
// .use(AuthIntercept)
// request
// .on('progress', event => {
// args.onProgress(event);
// })
// .then(it => {
// args.onSuccess(it);
// }).catch(err => {
// args.onError(err);
// });
// return request;
// }catch (e) {
// console.log(e, "error uploadAntd superagent")
// throw e
// }
// }
// };

62
src/utils/permute.js Normal file
View File

@@ -0,0 +1,62 @@
function k_combinations(set, k) {
var i, j, combs, head, tailcombs;
if (k > set.length || k <= 0) {
return [];
}
if (k == set.length) {
return [set];
}
if (k == 1) {
combs = [];
for (i = 0; i < set.length; i++) {
combs.push([set[i]]);
}
return combs;
}
combs = [];
for (i = 0; i < set.length - k + 1; i++) {
// head is a list that includes only our current element.
head = set.slice(i, i + 1);
// We take smaller combinations from the subsequent elements
tailcombs = k_combinations(set.slice(i + 1), k - 1);
// For each (k-1)-combination we join it with the current
// and store it to the set of k-combinations.
for (j = 0; j < tailcombs.length; j++) {
combs.push(head.concat(tailcombs[j]));
}
}
return combs;
}
function stringPermutations(str) {
let letters = str.split('')
, results = [[letters.shift()]]
while (letters.length) {
const currLetter = letters.shift()
let tmpResults = []
results.forEach(result => {
let rIdx = 0
while (rIdx <= result.length) {
const tmp = [...result]
tmp.splice(rIdx, 0, currLetter)
tmpResults.push(tmp)
rIdx++
}
})
results = tmpResults
}
return results
.map(letterArray => letterArray.join(''))
.filter((el, idx, self) => (self.indexOf(el) === idx))
.sort()
}
export function calculateCombination(text, n) {
return k_combinations(text.split(''), n).map(it => stringPermutations(it.join(''))).flat().filter((value, index, self) => {
return self.indexOf(value) === index;
});
}

46
src/utils/token.js Normal file
View File

@@ -0,0 +1,46 @@
export class TokenUtil {
static accessToken = null;
static refreshToken = null;
static loadToken() {
const accessToken = localStorage.getItem('access_token');
const refreshToken = localStorage.getItem('refresh_token');
if (accessToken) {
TokenUtil.setAccessToken(accessToken);
}
if (refreshToken) {
TokenUtil.setRefreshToken(refreshToken);
}
}
static persistToken() {
if (TokenUtil.accessToken != null) {
localStorage.setItem('access_token', TokenUtil.accessToken);
} else {
localStorage.removeItem('access_token');
}
if (TokenUtil.refreshToken != null) {
localStorage.setItem('refresh_token', TokenUtil.refreshToken);
} else {
localStorage.removeItem('refresh_token');
}
}
static setAccessToken(accessToken) {
TokenUtil.accessToken = accessToken;
}
static setRefreshToken(refreshToken) {
TokenUtil.refreshToken = refreshToken;
}
static clearAccessToken() {
TokenUtil.accessToken = null;
}
static clearRefreshToken() {
TokenUtil.accessToken = null;
}
}

23
src/utils/useStore.js Normal file
View File

@@ -0,0 +1,23 @@
import React from 'react';
import {Store} from "../store";
import {useLocalObservable} from "mobx-react-lite";
const storeContext = React.createContext(null);
export const store = new Store();
// store.authentication.loadToken();
export const StoreProvider = ({children}) => {
const localStore = useLocalObservable(() => {
return store;
});
return <storeContext.Provider value={localStore}>{children}</storeContext.Provider>
};
export const useStore = () => {
const store = React.useContext(storeContext);
if (!store) {
// throw new Error('useStore must be used within a StoreProvider.');
}
return store;
};