diff --git a/env/env.development b/env/env.development new file mode 100644 index 0000000..e9c0373 --- /dev/null +++ b/env/env.development @@ -0,0 +1,3 @@ +VITE_APP_MODE=production +VITE_BASE_API_URL=https://api.sky.eigen.co.id/api +VITE_BASE_API_REPORT_URL=https://api.sky.eigen.co.id/api diff --git a/env/env.production b/env/env.production new file mode 100644 index 0000000..2c6687b --- /dev/null +++ b/env/env.production @@ -0,0 +1,3 @@ +VITE_APP_MODE=production +VITE_BASE_API_URL=http://172.16.2.101:30050/api +VITE_BASE_API_REPORT_URL=http://172.16.2.101:30050/api diff --git a/src/App.css b/src/App.css deleted file mode 100644 index b9d355d..0000000 --- a/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index afe48ac..0000000 --- a/src/App.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' - -function App() { - const [count, setCount] = useState(0) - - return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- - ) -} - -export default App diff --git a/src/apps/admin/index.tsx b/src/apps/admin/index.tsx new file mode 100644 index 0000000..c5eec21 --- /dev/null +++ b/src/apps/admin/index.tsx @@ -0,0 +1,21 @@ +import { Navigate, Route, Routes } from 'react-router-dom'; +import AdminLayout from './layout'; +import { StorageDefaultURL } from '@pos/base'; + +function DefaultUrl() { + const path: any = StorageDefaultURL.get(); + return ; +} + +export default function Admin() { + return ( + + + item} /> + item master} /> + } /> + } /> + + + ); +} diff --git a/src/apps/admin/layout/index.tsx b/src/apps/admin/layout/index.tsx new file mode 100644 index 0000000..fe3dccf --- /dev/null +++ b/src/apps/admin/layout/index.tsx @@ -0,0 +1,24 @@ +import { Layout } from 'antd'; +import { ReactNode } from 'react'; +import { Content } from 'antd/es/layout/layout'; + +interface AdminLayoutProps { + children: ReactNode; +} + +export default function AdminLayout(props: AdminLayoutProps) { + const { children } = props; + + return ( + + + {children} + + + ); +} diff --git a/src/apps/auth/index.tsx b/src/apps/auth/index.tsx new file mode 100644 index 0000000..ac1eab4 --- /dev/null +++ b/src/apps/auth/index.tsx @@ -0,0 +1,37 @@ +import { StorageActiveAccount } from '@pos/base'; +import React from 'react'; +import { useEffect } from 'react'; +import { Navigate, Route, Routes, useNavigate } from 'react-router-dom'; + +const AuthLayout = React.lazy(() => import('./layout')); +const Login = React.lazy(() => import('./pages/login')); + +export default function Auth() { + const navigate = useNavigate(); + + async function checkAccount() { + const activeAccount = await StorageActiveAccount.get(); + if (activeAccount) navigate('/app', { replace: true }); + } + + useEffect(() => { + checkAccount(); + }, []); + + return ( + + + + } /> + } /> + } /> + + + + //
+ //
HELLO
+ //
Welcome Team!
+ //
{`Smile... it's a beautiful day.`}
+ //
+ ); +} diff --git a/src/apps/auth/layout/index.tsx b/src/apps/auth/layout/index.tsx new file mode 100644 index 0000000..76514a9 --- /dev/null +++ b/src/apps/auth/layout/index.tsx @@ -0,0 +1,17 @@ +import { Layout } from 'antd'; +import { ReactNode } from 'react'; +import { Content } from 'antd/es/layout/layout'; + +interface AdminLayoutProps { + children: ReactNode; +} + +export default function AuthLayout(props: AdminLayoutProps) { + const { children } = props; + + return ( + + {children} + + ); +} diff --git a/src/apps/auth/pages/login/components/index.ts b/src/apps/auth/pages/login/components/index.ts new file mode 100644 index 0000000..ab8f2b0 --- /dev/null +++ b/src/apps/auth/pages/login/components/index.ts @@ -0,0 +1 @@ +export * from './login-form'; diff --git a/src/apps/auth/pages/login/components/login-form.less b/src/apps/auth/pages/login/components/login-form.less new file mode 100644 index 0000000..dbadbe7 --- /dev/null +++ b/src/apps/auth/pages/login/components/login-form.less @@ -0,0 +1,38 @@ +.login-form { + width: 100vw; + height: 100vh; + background-color: #ebf7eb; + background-image: url('../../../../../base/presentation/assets/images/login-bg.png'); + background-size: cover; + background-repeat: no-repeat; + background-position: bottom center; + display: flex; + justify-content: center; + align-items: center; + + h1, + h2, + h3 { + margin: 0; + color: #5a0383; + } + + h1, + h2 { + font-size: 2.5rem; + line-height: 2.7rem; + } + + h1 { + font-weight: 100; + } + + h2 { + font-weight: 900; + } + + h3 { + font-weight: 200; + font-size: 1.5rem; + } +} diff --git a/src/apps/auth/pages/login/components/login-form.tsx b/src/apps/auth/pages/login/components/login-form.tsx new file mode 100644 index 0000000..6d96d5e --- /dev/null +++ b/src/apps/auth/pages/login/components/login-form.tsx @@ -0,0 +1,99 @@ +import { Button, Card, Form, Input, Popover } from 'antd'; +import './login-form.less'; +import { BsPersonCircle } from 'react-icons/bs'; +import { IoLockClosed } from 'react-icons/io5'; +import { useState } from 'react'; +import { Rule } from 'antd/es/form'; +import { capitalizeEachWord } from '@pos/base'; + +interface Payload { + username: string; + password: string; +} + +interface LoginForm { + onSubmit(payload: Payload): Promise; +} + +function inputRule(label: string): Rule[] { + return [ + { required: true, message: `${capitalizeEachWord(label)} harus diisi!` }, + { + max: 100, + message: `${capitalizeEachWord(label)} maksimal 50 karakter!`, + }, + ...(label === 'username' + ? [ + { + pattern: new RegExp(/^[a-zA-Z0-9 ]*$/i), + message: `${capitalizeEachWord(label)} hanya menerima alfanumerik!`, + }, + ] + : []), + ]; +} + +export function LoginForm(props: LoginForm) { + const onSubmit = props?.onSubmit; + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + + async function onFinish(payload: Payload) { + setLoading(true); + try { + if (onSubmit) + await onSubmit({ + username: payload?.username?.trim(), + password: payload?.password?.trim(), + }); + } catch ({ response }: any) { + form.setFields([{ name: 'validation', errors: [response?.data?.message] }]); + } finally { + setLoading(false); + } + } + + async function onValuesChange() { + form.setFields([{ name: 'validation', errors: [] }]); + } + + return ( +
+ +
+

Hello

+

Welcome Team!

+

Smile ... It's a beatiful day.

+
+
+ + + } + /> + + + + + } + /> + + + + + + +
+
+
+ ); +} diff --git a/src/apps/auth/pages/login/index.tsx b/src/apps/auth/pages/login/index.tsx new file mode 100644 index 0000000..7318d4d --- /dev/null +++ b/src/apps/auth/pages/login/index.tsx @@ -0,0 +1,38 @@ +import { API_URL, decryptData, handleLogin, useQueryParam, WEB_URL } from '@pos/base'; +import { LoginForm } from './components'; +import axios from 'axios'; +import { useNavigate } from 'react-router-dom'; + +const customAxios = axios.create(); + +export default function LoginPage() { + const queryParam = useQueryParam(); + const navigate = useNavigate(); + + async function handleSubmitLogin(payload: any) { + const { status, data } = await customAxios({ + url: API_URL.LOGIN, + method: 'POST', + data: payload, + }); + if (status === 201) { + const user = data?.data; + const role = user?.role; + + await handleLogin(user); + if (role === 'tenant') { + navigate(WEB_URL.REPORT_TENANT); + } else { + if (queryParam.get('u')) { + const encPrevUrl = decryptData(decodeURIComponent(queryParam.get('u') as string)); + if (encPrevUrl && encPrevUrl !== '' && !encPrevUrl?.includes('undefined')) { + const prevUrl = JSON.parse(encPrevUrl); + navigate(prevUrl, { replace: true }); + } else navigate('/app', { replace: true }); + } else navigate('/app', { replace: true }); + } + } + } + + return ; +} diff --git a/src/apps/index.tsx b/src/apps/index.tsx new file mode 100644 index 0000000..d860177 --- /dev/null +++ b/src/apps/index.tsx @@ -0,0 +1,35 @@ +import { Suspense, lazy } from 'react'; +import { RecoilRoot } from 'recoil'; +import { ConfigProvider, Flex, Spin } from 'antd'; +import { DebugObserver, ForbiddenAccessPage, NotFoundPage } from '@pos/base'; +import { Navigate, Route, Routes } from 'react-router-dom'; +import { APP_THEME } from '@pos/base/presentation/assets/themes'; +import { LoadingOutlined } from '@ant-design/icons'; + +const AuthApp = lazy(() => import('./auth')); +const PrivateApp = lazy(() => import('./admin')); + +export default function App() { + return ( + + + + + } /> + + } + > + + } /> + } /> + } /> + } /> + } /> + + + + + ); +} diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/base/index.ts b/src/base/index.ts index 3bf75bc..34f89db 100644 --- a/src/base/index.ts +++ b/src/base/index.ts @@ -13,6 +13,7 @@ export * from './infrastructure/constants'; export * from './infrastructure/helpers'; // Presentations +export * from './presentation/components'; export * from './presentation/hooks'; export * from './presentation/providers'; export * from './presentation/states'; diff --git a/src/base/presentation/components/icon-wrapper/index.tsx b/src/base/presentation/components/icon-wrapper/index.tsx new file mode 100644 index 0000000..214505f --- /dev/null +++ b/src/base/presentation/components/icon-wrapper/index.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +export function IconWrapper({ children }: { children?: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/src/base/presentation/components/index.ts b/src/base/presentation/components/index.ts new file mode 100644 index 0000000..a6e5696 --- /dev/null +++ b/src/base/presentation/components/index.ts @@ -0,0 +1,2 @@ +export * from './icon-wrapper'; +export * from './page-setup'; diff --git a/src/base/presentation/components/page-setup/coming-soon/index.tsx b/src/base/presentation/components/page-setup/coming-soon/index.tsx new file mode 100644 index 0000000..e603b65 --- /dev/null +++ b/src/base/presentation/components/page-setup/coming-soon/index.tsx @@ -0,0 +1,9 @@ +import image from '@pos/assets/images/coming-soon.jpg'; + +export function ComingSoonPage() { + return ( +
+ +
+ ); +} diff --git a/src/base/presentation/components/page-setup/forbidden-access/index.tsx b/src/base/presentation/components/page-setup/forbidden-access/index.tsx new file mode 100644 index 0000000..abcda6b --- /dev/null +++ b/src/base/presentation/components/page-setup/forbidden-access/index.tsx @@ -0,0 +1,54 @@ +// import { Button } from 'antd'; +// import { IconWrapper } from '../../icon-wrapper'; +// import { HiArrowLeft } from 'react-icons/hi2'; +// import { useNavigate } from 'react-router-dom'; +import { useAppModule } from '@pos/base/presentation/hooks'; +import Image4040 from '../../../assets/images/404.svg'; +import { Button } from 'antd'; +import { IconWrapper } from '@pos/base'; +import { HiArrowLeft } from 'react-icons/hi'; +import { useNavigate } from 'react-router-dom'; + +export function ForbiddenAccessPage() { + const moduleAction = useAppModule(); + const webUrl = moduleAction?.webUrl; + + const navigate = useNavigate(); + + function handleBack() { + navigate(`${webUrl}`); + } + + return ( +
+
+
+

Forbidden Access

+

+ Sorry, you do not have access to this page. Here are some helpful links: +

+ +
+ {webUrl && ( + + )} +
+
+ +
+ +
+
+
+ ); +} diff --git a/src/base/presentation/components/page-setup/index.ts b/src/base/presentation/components/page-setup/index.ts new file mode 100644 index 0000000..406785a --- /dev/null +++ b/src/base/presentation/components/page-setup/index.ts @@ -0,0 +1,3 @@ +export * from './forbidden-access'; +export * from './not-found'; +export * from './coming-soon'; diff --git a/src/base/presentation/components/page-setup/not-found/index.tsx b/src/base/presentation/components/page-setup/not-found/index.tsx new file mode 100644 index 0000000..8696fda --- /dev/null +++ b/src/base/presentation/components/page-setup/not-found/index.tsx @@ -0,0 +1,44 @@ +import { Button } from 'antd'; +import { IconWrapper } from '../../icon-wrapper'; +import { HiArrowLeft } from 'react-icons/hi2'; +import { useNavigate } from 'react-router-dom'; +import Image4040 from '../../../assets/images/404.svg'; + +export function NotFoundPage() { + const navigate = useNavigate(); + + function handleBack() { + navigate(-1); + } + + return ( +
+
+
+

Page not found

+

+ Sorry, the page you are looking for doesn't exist. Here are some helpful links: +

+ +
+ +
+
+ +
+ +
+
+
+ ); +} diff --git a/src/index.css b/src/index.css deleted file mode 100644 index 6119ad9..0000000 --- a/src/index.css +++ /dev/null @@ -1,68 +0,0 @@ -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} diff --git a/src/main.tsx b/src/main.tsx index 6f4ac9b..a3ca77f 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,21 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import App from './App.tsx' -import './index.css' +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import App from './apps'; -createRoot(document.getElementById('root')!).render( - - - , -) +//Utils +import './utils/axios-interceptor.ts'; +import './utils/credential-checker.ts'; +import './utils/g-license.ts'; + +// Styles +import '@pos/assets/styles/main.less'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + , + , +); diff --git a/src/utils/authorization.ts b/src/utils/authorization.ts new file mode 100644 index 0000000..6e29135 --- /dev/null +++ b/src/utils/authorization.ts @@ -0,0 +1,3 @@ +//SAMPLE AUTHORIZATION TOKEN +export const authorization = + 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImM1OWY4MTFlLTg3M2MtNDQ3Mi1iZDU4LTIxYzExMTkwMjExNCIsIm5hbWUiOiJzdXBlcmFkbWluIiwidXNlcm5hbWUiOiJzdXBlcmFkbWluIiwicm9sZSI6InN1cGVyYWRtaW4iLCJ1c2VyX3ByaXZpbGVnZV9pZCI6bnVsbCwiaWF0IjoxNzE3NjQ0MDAzLCJleHAiOjE3MTc2ODcyMDN9.nLwtLfAaHNTNRk6NMgGlYeybu1wIxW9WUWWeYChZUaw'; diff --git a/src/utils/axios-interceptor.ts b/src/utils/axios-interceptor.ts new file mode 100644 index 0000000..bc759af --- /dev/null +++ b/src/utils/axios-interceptor.ts @@ -0,0 +1,45 @@ +import axios, { AxiosError, AxiosResponse } from 'axios'; +import { + BASE_API_URL, + ErrorRequest, + StorageAccessToken, + handleLogout, + // StorageAccessToken +} from '@pos/base'; + +axios.defaults.baseURL = BASE_API_URL; +axios.defaults.headers.post['Content-Type'] = 'application/json'; +axios.defaults.headers.post['Access-Control-Allow-Origin'] = '*'; +axios.defaults.headers.post['Accept'] = '*/*'; + +axios.interceptors.request.use(async (request: any) => { + const token = await StorageAccessToken.get(); + // const token = authorization; + if (token) { + request.headers.authorization = token; + } + + return request; +}); + +axios.interceptors.response.use( + (response) => { + const data = response.data; + const status = response.status; + return { data, status } as any; + }, + (e) => { + const errorStatus = e.response?.status; + if (errorStatus === 401) { + const [, currentPath] = window.location.href.split('app/'); + handleLogout('/app/' + currentPath); + } + + const error = e as AxiosError; + const errorResponse: AxiosResponse = error?.response as AxiosResponse; + const errorData = errorResponse?.data; + const status = errorResponse?.status; + const message = errorData?.message ?? errorData; + return Promise.reject(new ErrorRequest(errorData, message, status)); + }, +); diff --git a/src/utils/credential-checker.ts b/src/utils/credential-checker.ts new file mode 100644 index 0000000..c878c48 --- /dev/null +++ b/src/utils/credential-checker.ts @@ -0,0 +1,25 @@ +import dayjs from 'dayjs'; +import { API_URL, BASE_API_URL, handleLogout, StorageAccessToken } from '@pos/base'; +import axios from 'axios'; + +async function credentialChecker() { + const token = await StorageAccessToken.get(); + console.log(token); + if (token) { + const [, body] = token.split('.'); + const bodyToken = JSON.parse(window.atob(body)); + const expDate = bodyToken.exp; + const currentDate = dayjs().unix(); + const isExpired = currentDate > expDate; + if (isExpired) { + const [, tokenBody] = token.split(' '); + const customAxios = axios.create(); + await customAxios.post(BASE_API_URL + API_URL.FORCE_LOGOUT, { token: tokenBody }); + + const [, currentPath] = window.location.href.split('app/'); + handleLogout('/app/' + currentPath); + } + } +} + +credentialChecker(); diff --git a/src/utils/g-license.ts b/src/utils/g-license.ts new file mode 100644 index 0000000..cd07f7a --- /dev/null +++ b/src/utils/g-license.ts @@ -0,0 +1,6 @@ +import { LicenseManager } from 'ag-grid-enterprise'; + +LicenseManager.prototype.isDisplayWatermark = () => false; +LicenseManager.prototype.validateLicense = () => { + // Hehe boys +};