-
Notifications
You must be signed in to change notification settings - Fork 3
Create Kas Submission Page #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: development
Are you sure you want to change the base?
Changes from 10 commits
0db0c32
6b529e2
f8500ca
ba48f60
c937c5e
0e380b7
1af18b2
6134482
0d92cf2
0560431
adeee4d
5bd1338
9860a58
bff1414
db637ca
841c879
e3f5cd2
a5fbcc4
2364593
1bc8be5
0c1a796
d057ad9
7b30b83
eae6a71
5b14aba
3d1cc88
ebcf348
29aa174
a4fb4cb
7b53288
6975ae1
b4ca056
66b6244
146258c
4abab30
fd6ab34
67abeb5
306823d
4735cab
d59a07a
8cbf2ca
3046143
4980be9
e296d63
7752cdc
4949d9b
cc8f6a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,14 +14,27 @@ type Headers = { | |
| "Content-type": string; | ||
| }; | ||
|
|
||
| type APIContentType = { | ||
| isFile?: boolean; | ||
| isForm?: boolean; | ||
| }; | ||
|
|
||
| export default class API { | ||
| headers: Headers = { | ||
| Accept: "application/json", | ||
| "Content-type": "application/json", | ||
| }; | ||
| headers: Headers; | ||
|
||
| api: AxiosInstance; | ||
|
|
||
| constructor() { | ||
| constructor( | ||
| { isFile, isForm }: APIContentType = { isFile: false, isForm: false }, | ||
| ) { | ||
| this.headers = { | ||
| Accept: "application/json", | ||
| "Content-type": isFile | ||
| ? "multipart/form-data" | ||
| : isForm | ||
| ? "application/x-www-form-urlencoded" | ||
| : "application/json", | ||
| }; | ||
|
|
||
| this.api = axios.create({ | ||
| baseURL: `${import.meta.env.VITE_BACKEND_URL}/v1`, | ||
| headers: this.headers as unknown as AxiosHeaders, | ||
|
|
@@ -44,13 +57,18 @@ export default class API { | |
|
|
||
| async POST<T>(path: string, data: any): Promise<APIResponse<T>> { | ||
| try { | ||
| // console.log(this.headers, data); | ||
| const res = await this.api.post(path, data); | ||
| return res.data; | ||
| } catch (err: AxiosError | any) { | ||
| if (isAxiosError(err)) { | ||
| return err?.response?.data; | ||
| console.error("Axios error:", err.message); | ||
|
||
| throw new Error( | ||
| `API Error: ${err.response?.status} ${err.response?.data?.message}`, | ||
| ); | ||
| } else { | ||
| return err; | ||
| console.error("Unexpected error:", err); | ||
| throw err; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,64 @@ | ||||||
| import { APIResponse } from "@types"; | ||||||
| import API from ".."; | ||||||
|
|
||||||
| export type GetResponse = { | ||||||
| submission_id: string; | ||||||
| user: UserType; | ||||||
| payed_amount: number; | ||||||
| status: { | ||||||
| ID: number; | ||||||
| Name: string; | ||||||
| }; | ||||||
| note: string; | ||||||
| evidence: string; | ||||||
| submitted_at: string; | ||||||
| updated_at: string; | ||||||
| }; | ||||||
|
|
||||||
| export type UserType = { | ||||||
| npm: string; | ||||||
| name: string; | ||||||
| email: string; | ||||||
| kas_payed: number; | ||||||
| }; | ||||||
|
|
||||||
| export type SubmissionRequest = { | ||||||
| user: { | ||||||
| npm: string; | ||||||
| }; | ||||||
| payed_amount: number; | ||||||
| note: string; | ||||||
| evidence: string; | ||||||
| }; | ||||||
|
|
||||||
| export default class CreateKasService { | ||||||
|
||||||
| kasPath: string = "/kas-submissions"; | ||||||
| userPath: string = "/users"; | ||||||
| private api: API = new API(); | ||||||
| private apiForm: API = new API({ isForm: true }); | ||||||
| async post( | ||||||
|
||||||
| submission: SubmissionRequest | string, | ||||||
| ): Promise<APIResponse<GetResponse>> { | ||||||
| const targetPath = `${this.kasPath}`; | ||||||
| try { | ||||||
| const res: APIResponse<GetResponse> = await this.apiForm.POST( | ||||||
| targetPath, | ||||||
| submission, | ||||||
| ); | ||||||
| return res; | ||||||
| } catch (error) { | ||||||
| console.error("Error creating submission:", error); | ||||||
|
||||||
| throw error; | ||||||
| } | ||||||
| } | ||||||
| async get(queryParams: string = ""): Promise<APIResponse<UserType[]>> { | ||||||
|
||||||
| async get(queryParams: string = ""): Promise<APIResponse<UserType[]>> { | |
| async get(queryParams: string): Promise<APIResponse<UserType[]>> { |
harusnya engga perlu ini
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import { APIResponse } from "@types"; | ||
| import API from ".."; | ||
|
|
||
| export type UploadImageResponse = { | ||
| url_id: string; | ||
| }; | ||
| export type UploadImageRequest = { | ||
| file: File; | ||
| }; | ||
| export default class UploadImage { | ||
|
||
| basePath: string = "/file/images"; | ||
| private api: API = new API({ isFile: true }); | ||
| async post( | ||
|
||
| submission: UploadImageRequest, | ||
| ): Promise<APIResponse<UploadImageResponse>> { | ||
| const targetPath = `${this.basePath}`; | ||
| const res: APIResponse<UploadImageResponse> = await this.api.POST( | ||
| targetPath, | ||
| submission, | ||
| ); | ||
| return res; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import { Button, CircularProgress } from "@mui/material"; | ||
|
|
||
| interface Props { | ||
| onClick?: () => void; | ||
| disabledLoading?: boolean; | ||
| submitLoading?: boolean; | ||
| icon?: React.ReactNode; | ||
| label: string; | ||
| variant?: "contained" | "outlined"; | ||
| size?: "small" | "medium" | "large"; | ||
| color?: "primary" | "inherit" | "secondary"; | ||
| className?: string; | ||
| autoFocus?: boolean; | ||
| } | ||
| function ActionButton(props: Props): JSX.Element { | ||
| const { | ||
| onClick, | ||
| disabledLoading, | ||
| submitLoading, | ||
| icon, | ||
| label, | ||
| variant = "contained", | ||
| size = "small", | ||
| color = "inherit", | ||
| className, | ||
| autoFocus | ||
| } = props; | ||
|
|
||
| return ( | ||
| <Button | ||
| variant={variant} | ||
| color={color} | ||
| onClick={onClick} | ||
| disabled={disabledLoading || submitLoading} | ||
| size={size} | ||
| className={className} | ||
| autoFocus={autoFocus || false} | ||
| disableRipple | ||
| > | ||
| {submitLoading ? ( | ||
| <CircularProgress size={20} /> | ||
| ) : ( | ||
| <div className="flex items-center gap-2"> | ||
| {icon} | ||
| <span>{label}</span> | ||
| </div> | ||
| )} | ||
| </Button> | ||
| ); | ||
| } | ||
|
|
||
| export default ActionButton; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import { Check, Download } from "@mui/icons-material"; | ||
| import { CircularProgress } from "@mui/material"; | ||
| import { downloadImage } from "@utils/imageDownloader"; | ||
| import { useState } from "react"; | ||
|
|
||
| interface Props { | ||
| className?: string; | ||
| url: string; | ||
| } | ||
| function DownloadButton(props: Props): JSX.Element { | ||
| const { className, url } = props; | ||
| const [downloadState, setDownloadState] = useState<DownloadState>("idle"); | ||
|
|
||
| const handleDownload = async () => { | ||
| if (downloadState !== "idle") return; | ||
|
|
||
| setDownloadState("loading"); | ||
| await downloadImage(url); | ||
|
|
||
| setTimeout(() => { | ||
| setDownloadState("idle"); | ||
| }, 2000); | ||
| }; | ||
|
|
||
| return ( | ||
| <button className={className} onClick={handleDownload}> | ||
| {downloadState === "idle" ? ( | ||
| <Download style={{ fontSize: "1rem", color: "gray" }} /> | ||
| ) : downloadState === "loading" ? ( | ||
| <CircularProgress | ||
| size={10} | ||
| style={{ | ||
| color: "gray" | ||
| }} | ||
| /> | ||
| ) : ( | ||
| <Check style={{ fontSize: "1rem", color: "gray" }} /> | ||
| )} | ||
| </button> | ||
| ); | ||
| } | ||
|
|
||
| type DownloadState = "idle" | "loading"; | ||
|
|
||
| export default DownloadButton; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { default as ActionButton } from "./ActionButton"; | ||
| export { default as DownloadButton } from "./DownloadButton"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import { Dialog, DialogTitle } from "@mui/material"; | ||
|
|
||
| interface Props { | ||
| open: boolean; | ||
| onClose: () => void; | ||
| title?: string; | ||
| children: React.ReactNode; | ||
| width?: number; | ||
| } | ||
| function BaseDialog(props: Props): JSX.Element { | ||
| const { open, onClose, title, children, width = 360 } = props; | ||
|
|
||
| return ( | ||
| <Dialog | ||
| open={open} | ||
| onClose={onClose} | ||
| PaperProps={{ | ||
| style: { | ||
| width: width, | ||
| borderRadius: 10, | ||
| backgroundColor: "#373737", | ||
| color: "white", | ||
| userSelect: "text" | ||
| } | ||
| }} | ||
| disableRestoreFocus={true} | ||
| > | ||
| {title ? ( | ||
| <DialogTitle | ||
| fontSize="0.9rem" | ||
| sx={{ | ||
| padding: "0.8rem 1.2rem", | ||
| backgroundColor: "#323232", | ||
| borderBottom: "1px solid #55555590" | ||
| }} | ||
| > | ||
| {title} | ||
| </DialogTitle> | ||
| ) : null} | ||
| {children} | ||
| </Dialog> | ||
| ); | ||
| } | ||
|
|
||
| export default BaseDialog; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import { | ||
| Dialog, | ||
| DialogActions, | ||
| DialogContent, | ||
| Icon, | ||
| Typography | ||
| } from "@mui/material"; | ||
| import { DeleteForever } from "@mui/icons-material"; | ||
| import ActionButton from "../../Button/ActionButton"; | ||
|
|
||
| interface Props { | ||
| className?: string; | ||
| open: boolean; | ||
| onClose: () => void; | ||
| onClickDelete: () => void; | ||
| } | ||
| function ConfirmDeleteDialog(props: Props): JSX.Element { | ||
| return ( | ||
| <Dialog | ||
| open={props.open} | ||
| onClose={props.onClose} | ||
| PaperProps={{ | ||
| style: { | ||
| width: 360, | ||
| margin: "0", | ||
| borderRadius: 20, | ||
| backgroundColor: "#373737", | ||
| color: "white", | ||
| opacity: 0.9 | ||
| } | ||
| }} | ||
| disableRestoreFocus={true} | ||
| > | ||
| <DialogContent className="flex flex-col items-center"> | ||
| <Icon style={{ width: 64, height: 64 }}> | ||
| <DeleteForever style={{ width: 64, height: 64 }} /> | ||
| </Icon> | ||
| <Typography align="center" className="font-bold"> | ||
| Are you sure deleting this? | ||
| </Typography> | ||
| </DialogContent> | ||
| <DialogActions className="justify-center my-2 mx-auto"> | ||
| <ActionButton | ||
| label="Cancel" | ||
| onClick={props.onClose} | ||
| variant="outlined" | ||
| color="inherit" | ||
| /> | ||
| <ActionButton | ||
| label="Delete" | ||
| onClick={props.onClickDelete} | ||
| variant="contained" | ||
| color="secondary" | ||
| className="opacity-90" | ||
| autoFocus | ||
| /> | ||
| </DialogActions> | ||
| </Dialog> | ||
| ); | ||
| } | ||
|
|
||
| export default ConfirmDeleteDialog; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
buang 2 package ini kalo cuma untuk error notifikasi, pake snackbar yg sudah di develop disini