diff --git a/src/common/breadcrumb.ts b/src/common/breadcrumb.ts new file mode 100644 index 0000000..3e2e6f3 --- /dev/null +++ b/src/common/breadcrumb.ts @@ -0,0 +1,33 @@ +import { parsingRoute } from "@/helpers/route"; +import { Routes } from "@/routes/routes"; + +export const Breadcrumbs = { + Dashboard: () => [{ label: "Dashboard", href: Routes.Dashboard }], + VariableInput: { + Index: () => [ + { label: "Variable Input", href: Routes.VariableInputIndex }, + ], + Create: () => [ + { label: "Variable Input", href: Routes.VariableInputIndex }, + { + label: "Create Variable Input", + href: Routes.VariableInputCreate, + }, + ], + Edit: (id: string) => [ + { label: "Variable Input", href: Routes.VariableInputIndex }, + { + label: "Edit Variable Input", + href: parsingRoute(Routes.VariableInputEdit, { id }), + }, + ], + }, + VariableOutput: { + Index: () => [ + { label: "Variable Output", href: Routes.VariableOutputIndex }, + ], + }, + Result: { + Index: () => [{ label: "Result Predict", href: Routes.ResultIndex }], + }, +}; diff --git a/src/components/shared/Button/Button.tsx b/src/components/shared/Button/Button.tsx new file mode 100644 index 0000000..2192aaa --- /dev/null +++ b/src/components/shared/Button/Button.tsx @@ -0,0 +1,12 @@ +import { ButtonProps, Button as ButtonAntd } from "antd"; +import React from "react"; +import { Link } from "react-router-dom"; + +interface IProps extends ButtonProps {} + +const Button: React.FC = (props) => { + const btn = {props.children}; + return props.href ? {btn} : btn; +}; + +export default Button; diff --git a/src/components/shared/Loader/LoaderCenter.tsx b/src/components/shared/Loader/LoaderCenter.tsx new file mode 100644 index 0000000..fd7538a --- /dev/null +++ b/src/components/shared/Loader/LoaderCenter.tsx @@ -0,0 +1,12 @@ +import { Row, Spin } from "antd"; +import React from "react"; + +const LoaderCenter: React.FC = () => { + return ( + + + + ); +}; + +export default LoaderCenter; diff --git a/src/components/shared/Table/RowActionButtons.tsx b/src/components/shared/Table/RowActionButtons.tsx new file mode 100644 index 0000000..9204dde --- /dev/null +++ b/src/components/shared/Table/RowActionButtons.tsx @@ -0,0 +1,56 @@ +import { Space } from "antd"; +import React from "react"; +import Button from "../Button/Button"; +import { DeleteOutlined, EditOutlined, EyeOutlined } from "@ant-design/icons"; + +interface IButtonAction { + type: "detail" | "edit" | "delete" | "custom"; + href?: string; + button?: React.ReactNode; + onClick?: () => void; +} + +interface IProps { + actions: IButtonAction[]; +} + +const RowActionButtons: React.FC = ({ actions }) => { + const renderButtonAction = (action: IButtonAction) => { + switch (action.type) { + case "detail": + return ( + action.button ?? ( + + ) + ); + case "edit": + return ( + action.button ?? ( + + ) + ); + case "delete": + return ( + action.button ?? ( + + ) + ); + case "custom": + return action.button; + default: + return ; + } + }; + return {actions.map((action) => renderButtonAction(action))}; +}; + +export default RowActionButtons; diff --git a/src/helpers/table.ts b/src/helpers/table.ts new file mode 100644 index 0000000..897619f --- /dev/null +++ b/src/helpers/table.ts @@ -0,0 +1,8 @@ +import { ColumnGroupType, ColumnType } from "antd/es/table"; + +export const numberColumns = (): ColumnGroupType | ColumnType => { + return { + title: "No", + render: (_, __, index) => index + 1, + }; +}; diff --git a/src/interfaces/entities/VariableInput/index.ts b/src/interfaces/entities/VariableInput/index.ts new file mode 100644 index 0000000..431d941 --- /dev/null +++ b/src/interfaces/entities/VariableInput/index.ts @@ -0,0 +1,6 @@ +import { IBaseEntity } from "../base"; + +export interface IVariableInput extends IBaseEntity { + id: number; + name: string; +} diff --git a/src/interfaces/responses/VariableInput/index.ts b/src/interfaces/responses/VariableInput/index.ts new file mode 100644 index 0000000..9e615de --- /dev/null +++ b/src/interfaces/responses/VariableInput/index.ts @@ -0,0 +1,8 @@ +import { IVariableInput } from "@/interfaces/entities/VariableInput"; +import { IBaseResponse } from "../base"; + +export interface IVariableInputIndexResponse + extends IBaseResponse {} + +export interface IVariableInputDetailResponse + extends IBaseResponse {} diff --git a/src/layouts/MainLayout.tsx b/src/layouts/MainLayout.tsx index 6a079f9..8e6781a 100644 --- a/src/layouts/MainLayout.tsx +++ b/src/layouts/MainLayout.tsx @@ -1,4 +1,13 @@ -import { Button, Grid, Layout, Menu, Row } from "antd"; +import { + Breadcrumb, + Button, + Grid, + Layout, + Menu, + Row, + Space, + Typography, +} from "antd"; import Sider from "antd/es/layout/Sider"; import { Content, Header } from "antd/es/layout/layout"; import React from "react"; @@ -12,11 +21,13 @@ import { import { ItemType, MenuItemType } from "antd/es/menu/hooks/useItems"; import { colorConfig } from "@/themes/config"; import Avatar from "@/components/layout/Avatar"; -import { useLocation, useNavigate } from "react-router-dom"; +import { Link, useLocation, useNavigate } from "react-router-dom"; import { Routes } from "@/routes/routes"; interface IProps { children: React.ReactNode; + title: string; + breadcrumbs?: { label: string; href: string }[]; } const iconSidebarStyle = { @@ -31,7 +42,7 @@ const menuKey = { Result: Routes.ResultIndex, }; -const MainLayout: React.FC = ({ children }) => { +const MainLayout: React.FC = ({ children, ...props }) => { const [isSiderCollapsed, setIsSiderCollapsed] = React.useState(false); const { md } = Grid.useBreakpoint(); const navigate = useNavigate(); @@ -89,6 +100,7 @@ const MainLayout: React.FC = ({ children }) => { top: 0, bottom: 0, padding: "1rem 0", + zIndex: 50, }} trigger={null} collapsible @@ -111,6 +123,10 @@ const MainLayout: React.FC = ({ children }) => { background: colorConfig.neutral.white, display: "flex", alignItems: "center", + position: "sticky", + top: 0, + zIndex: 5, + boxShadow: "15px -3px 21px -5px rgba(0,0,0,0.75)", }} > = ({ children }) => { style={{ backgroundColor: colorConfig.neutral.lightGray, padding: isSiderCollapsed - ? "2rem 2rem 2rem 5rem" - : "2rem 2rem 2rem 14.5rem", + ? "1rem 2rem 2rem 5rem" + : "1rem 2rem 2rem 14.5rem", }} > - {children} + + + + {props.title} + + ({ + title: {label}, + }) + )} + /> + + {children} + diff --git a/src/pages/Dashboard/index.tsx b/src/pages/Dashboard/index.tsx index 831d889..0801841 100644 --- a/src/pages/Dashboard/index.tsx +++ b/src/pages/Dashboard/index.tsx @@ -1,11 +1,12 @@ +import { Breadcrumbs } from "@/common/breadcrumb"; import MainLayout from "@/layouts/MainLayout"; import { Card } from "antd"; import React from "react"; const Dashboard: React.FC = () => { return ( - - + + ); }; diff --git a/src/pages/Result/index.tsx b/src/pages/Result/index.tsx index 8cbaf19..ea692fe 100644 --- a/src/pages/Result/index.tsx +++ b/src/pages/Result/index.tsx @@ -1,11 +1,12 @@ +import { Breadcrumbs } from "@/common/breadcrumb"; import MainLayout from "@/layouts/MainLayout"; import { Card } from "antd"; import React from "react"; const ResultIndex: React.FC = () => { return ( - - + + ); }; diff --git a/src/pages/VariableInput/Form.tsx b/src/pages/VariableInput/Form.tsx new file mode 100644 index 0000000..3af5b6f --- /dev/null +++ b/src/pages/VariableInput/Form.tsx @@ -0,0 +1,27 @@ +import { Breadcrumbs } from "@/common/breadcrumb"; +import MainLayout from "@/layouts/MainLayout"; +import { Card } from "antd"; +import React from "react"; +import { useParams } from "react-router-dom"; + +interface IProps { + editPage?: boolean; +} + +const VariableInputForm: React.FC = ({ editPage = false }) => { + const { id } = useParams(); + return ( + + + + ); +}; + +export default VariableInputForm; diff --git a/src/pages/VariableInput/index.tsx b/src/pages/VariableInput/index.tsx index 9f16feb..7afaeef 100644 --- a/src/pages/VariableInput/index.tsx +++ b/src/pages/VariableInput/index.tsx @@ -1,11 +1,71 @@ +import { Breadcrumbs } from "@/common/breadcrumb"; +import Button from "@/components/shared/Button/Button"; +import LoaderCenter from "@/components/shared/Loader/LoaderCenter"; +import RowActionButtons from "@/components/shared/Table/RowActionButtons"; +import { parsingRoute } from "@/helpers/route"; +import { numberColumns } from "@/helpers/table"; +import { IVariableInput } from "@/interfaces/entities/VariableInput"; import MainLayout from "@/layouts/MainLayout"; -import { Card } from "antd"; +import { Routes } from "@/routes/routes"; +import { useVariableInputQueryIndex } from "@/services/query/VariableInput"; +import { Card, Table } from "antd"; +import { ColumnsType } from "antd/es/table"; import React from "react"; const VariableInputIndex: React.FC = () => { + const query = useVariableInputQueryIndex(); + const columns: ColumnsType = [ + numberColumns(), + { + title: "Name", + dataIndex: "name", + }, + { + title: "Actions", + dataIndex: "id", + render: (id) => ( + + ), + }, + ]; + return ( - - + + + Create + , + ]} + > + {query.isLoading ? ( + + ) : ( + + )} + ); }; diff --git a/src/pages/VariableOutput/index.tsx b/src/pages/VariableOutput/index.tsx index 81d09de..adc02f1 100644 --- a/src/pages/VariableOutput/index.tsx +++ b/src/pages/VariableOutput/index.tsx @@ -1,11 +1,15 @@ +import { Breadcrumbs } from "@/common/breadcrumb"; import MainLayout from "@/layouts/MainLayout"; import { Card } from "antd"; import React from "react"; const VariableOutputIndex: React.FC = () => { return ( - - + + ); }; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 2d62b0b..df9de48 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -12,6 +12,10 @@ const DashboardPage = React.lazy(() => import("@/pages/Dashboard")); const VariableInputIndexPage = React.lazy( () => import("@/pages/VariableInput") ); +const VariableInputFormPage = React.lazy( + () => import("@/pages/VariableInput/Form") +); + const VariableOutputIndexPage = React.lazy( () => import("@/pages/VariableOutput") ); @@ -54,6 +58,22 @@ export const router = createBrowserRouter([ ), }, + { + path: Routes.VariableInputCreate, + element: withSuspense( + + + + ), + }, + { + path: Routes.VariableInputEdit, + element: withSuspense( + + + + ), + }, { path: Routes.VariableOutputIndex, element: withSuspense( diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 3abbf52..4bd9eed 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -4,6 +4,9 @@ export enum Routes { Dashboard = "/dashboard", Profile = "/profile", VariableInputIndex = "/variable-input", + VariableInputDetail = "/variable-input/:id", + VariableInputCreate = "/variable-input/create", + VariableInputEdit = "/variable-input/:id/edit", VariableOutputIndex = "/variable-output", ResultIndex = "/result", } @@ -13,4 +16,8 @@ export enum EndpointApi { Register = "/register", Logout = "/logout", GetUser = "/user", + VariableInputIndex = "/variable-input", + VariableInputDetail = "/variable-input/:id", + VariableInputStore = "/variable-input/store", + VariableInputUpdate = "/variable-input/:id/update", } diff --git a/src/services/apis/VariableInput/index.ts b/src/services/apis/VariableInput/index.ts new file mode 100644 index 0000000..d7be751 --- /dev/null +++ b/src/services/apis/VariableInput/index.ts @@ -0,0 +1,14 @@ +import { AxiosError } from "axios"; +import { axiosAuthInstance } from ".."; +import { EndpointApi } from "@/routes/routes"; + +export const getVariableInputIndex = async () => { + try { + const response = await axiosAuthInstance.get( + EndpointApi.VariableInputIndex + ); + return response.data; + } catch (error) { + throw (error as AxiosError).response.data; + } +}; diff --git a/src/services/query/VariableInput/index.ts b/src/services/query/VariableInput/index.ts new file mode 100644 index 0000000..247505c --- /dev/null +++ b/src/services/query/VariableInput/index.ts @@ -0,0 +1,18 @@ +import { IVariableInputIndexResponse } from "@/interfaces/responses/VariableInput"; +import { IBaseResponse } from "@/interfaces/responses/base"; +import { getVariableInputIndex } from "@/services/apis/VariableInput"; +import { UseQueryOptions, useQuery } from "@tanstack/react-query"; + +export const useVariableInputQueryIndex = ( + options?: UseQueryOptions< + IVariableInputIndexResponse, + IBaseResponse, + IVariableInputIndexResponse + > +) => { + return useQuery({ + queryKey: ["variable-input-index"], + queryFn: () => getVariableInputIndex(), + ...options, + }); +};