From e0f538f1bb890d18895844f669024696fb4ed472 Mon Sep 17 00:00:00 2001 From: Aaron Hoffman Date: Wed, 31 Mar 2021 17:16:22 +0200 Subject: [PATCH] Add file submission --- src/App.tsx | 4 + .../avatar-with-title/avatar-with-title.tsx | 29 +++++ src/models/auth-model/auth-model.ts | 2 +- src/models/extensions/with-status.ts | 55 ++++++++++ src/models/proposals-model/proposal-detail.ts | 66 ++++++++++++ src/models/proposals-model/proposal-model.ts | 2 + ...del-store.tsx => proposals-model-store.ts} | 38 +++++-- src/models/root-store/root-store.ts | 2 + src/screens/create-proposal.tsx | 81 +++++++++++--- src/screens/home.tsx | 16 +-- src/screens/index.ts | 1 + src/screens/proposal-detail.tsx | 77 +++++++++++++ .../proposal-detail/upload-section.tsx | 65 +++++++++++ src/services/api-helpers.ts | 14 +-- src/services/api-types.ts | 6 ++ src/services/api.ts | 101 +++++++++++++++++- src/services/response-types.ts | 5 + 17 files changed, 520 insertions(+), 44 deletions(-) create mode 100644 src/components/avatar-with-title/avatar-with-title.tsx create mode 100755 src/models/extensions/with-status.ts create mode 100644 src/models/proposals-model/proposal-detail.ts rename src/models/proposals-model/{proposals-model-store.tsx => proposals-model-store.ts} (63%) create mode 100644 src/screens/proposal-detail.tsx create mode 100644 src/screens/proposal-detail/upload-section.tsx diff --git a/src/App.tsx b/src/App.tsx index 035b42f..1454469 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,7 @@ import Login from "./screens/login" import Register from "./screens/register" import { PrivateRoute } from "./navigation/private-route" import CreateProposal from "./screens/create-proposal" +import ProposalDetail from "./screens/proposal-detail" const App = observer(function App() { const [rootStore, setRootStore] = useState(null) @@ -30,6 +31,9 @@ const App = observer(function App() { + + + diff --git a/src/components/avatar-with-title/avatar-with-title.tsx b/src/components/avatar-with-title/avatar-with-title.tsx new file mode 100644 index 0000000..cec4bb7 --- /dev/null +++ b/src/components/avatar-with-title/avatar-with-title.tsx @@ -0,0 +1,29 @@ +import React from "react" +import { Row, Col, Avatar } from "antd" +import styled from "styled-components" +import Title from "antd/lib/typography/Title" +import { color } from "../../utils/colors" +const StyledUsernameWrapper = styled.div` + display: flex; + margin-bottom: 3vh; +` +const StyledAvatar = styled(Avatar)` + margin-right: 10px; +` +interface AvatarProps { + value: string + size: 2 | 5 | 1 | 3 | 4 | undefined +} +export const AvatarWithTitle = (props: AvatarProps): JSX.Element => { + const { value, size } = props + return ( + + + + {value[0]} + {value} + + + + ) +} diff --git a/src/models/auth-model/auth-model.ts b/src/models/auth-model/auth-model.ts index 2b42821..c1a2129 100644 --- a/src/models/auth-model/auth-model.ts +++ b/src/models/auth-model/auth-model.ts @@ -13,7 +13,7 @@ export const AuthModel = types .views((self) => { return { get isLogged(): boolean { - return !!self.username + return !!self.username && self.environment.api.hasCredentials() }, } }) diff --git a/src/models/extensions/with-status.ts b/src/models/extensions/with-status.ts new file mode 100755 index 0000000..210b31d --- /dev/null +++ b/src/models/extensions/with-status.ts @@ -0,0 +1,55 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { observable, IObservableValue } from "mobx" + +export type StatusType = "idle" | "pending" | "done" | "error" + +/** + * Adds a status field to the model often for tracking api access. + * + * This property is a string which can be observed, but will not + * participate in any serialization. + * + * Use this to extend your models: + * + * ```ts + * types.model("MyModel") + * .props({}) + * .actions(self => ({})) + * .extend(withStatus) // <--- time to shine baby!!! + * ``` + * + * This will give you these 3 options: + * + * .status // returns a string + * .status = "done" // change the status directly + * .setStatus("done") // change the status and trigger an mst action + */ +export const withStatus = () => { + /** + * The observable backing store for the status field. + */ + const status: IObservableValue = observable.box("idle") + + return { + views: { + // a getter + get status() { + return status.get() as StatusType + }, + // as setter + set status(value: StatusType) { + status.set(value) + }, + }, + actions: { + /** + * Set the status to something new. + * + * @param value The new status. + */ + setStatus(value: StatusType) { + status.set(value) + }, + }, + } +} diff --git a/src/models/proposals-model/proposal-detail.ts b/src/models/proposals-model/proposal-detail.ts new file mode 100644 index 0000000..e7f4e3c --- /dev/null +++ b/src/models/proposals-model/proposal-detail.ts @@ -0,0 +1,66 @@ +import { applySnapshot, flow, Instance, SnapshotOut, types } from "mobx-state-tree" +import { GetSignedUrl, GetSingleProposal, PutFile } from "../../services/api-types" +import { withEnvironment } from "../extensions/with-environment" +import { withStatus } from "../extensions/with-status" +import { UserModel } from "../user-model/user-model" +import { ProposalModel } from "./proposal-model" + +export const ProposalDetailModel = types + .model("ProposalDetailModel") + .props({ + proposal: types.maybeNull(ProposalModel), + }) + .extend(withEnvironment) + .extend(withStatus) + .actions((self) => { + return { + getProposal: flow(function* (id: string) { + self.setStatus("pending") + try { + const response: GetSingleProposal = yield self.environment.api.getProposal(id) + if (response.kind !== "ok") { + throw response + } + + self.proposal = response.proposal as any + self.setStatus("idle") + } catch (err) { + self.setStatus("error") + } + }), + putFile: flow(function* (fileName: string, file: File, progress: (event: any) => void) { + self.setStatus("pending") + try { + const response: PutFile = yield self.environment.api.submitFile(fileName, file, progress) + if (response.kind !== "ok") { + throw response + } + self.setStatus("idle") + return response + } catch (err) { + console.log(err) + self.setStatus("error") + } + }), + getSignedUrl: flow(function* (fileName: string) { + self.setStatus("pending") + try { + const response: GetSignedUrl = yield self.environment.api.getSignedUrl(fileName) + console.log(response) + if (response.kind !== "ok") { + throw response + } + self.setStatus("idle") + return response.url + } catch (err) { + console.log(err) + self.setStatus("error") + } + }), + } + }) + +type ProposalDetailModelType = Instance +export interface ProposalDetailModel extends ProposalDetailModelType {} +type ProposalDetailModelSnapshotType = SnapshotOut +export interface ProposalDetailModelSnapshot extends ProposalDetailModelSnapshotType {} diff --git a/src/models/proposals-model/proposal-model.ts b/src/models/proposals-model/proposal-model.ts index 30b4911..c950dbe 100644 --- a/src/models/proposals-model/proposal-model.ts +++ b/src/models/proposals-model/proposal-model.ts @@ -9,6 +9,8 @@ export const ProposalModel = types description: "", limit: 0, user: UserModel, + type: types.string, + rate: 0, }) .actions((self) => { return {} diff --git a/src/models/proposals-model/proposals-model-store.tsx b/src/models/proposals-model/proposals-model-store.ts similarity index 63% rename from src/models/proposals-model/proposals-model-store.tsx rename to src/models/proposals-model/proposals-model-store.ts index 3ca56b3..333f383 100644 --- a/src/models/proposals-model/proposals-model-store.tsx +++ b/src/models/proposals-model/proposals-model-store.ts @@ -1,22 +1,23 @@ import { applySnapshot, flow, Instance, SnapshotOut, types } from "mobx-state-tree" -import { GetProposals, PostProposal } from "../../services/api-types" +import { GetProposals, GetProposalTypes, PostProposal } from "../../services/api-types" import { withEnvironment } from "../extensions/with-environment" +import { withStatus } from "../extensions/with-status" import { ProposalModel } from "./proposal-model" export const ProposalsModelStore = types .model("ProposalsModelStore") .props({ proposals: types.array(ProposalModel), - loading: false, }) .extend(withEnvironment) + .extend(withStatus) .actions((self) => { return { getProposals: flow(function* (from: number, to: number) { try { - self.loading = true + self.setStatus("pending") const response: GetProposals = yield self.environment.api.getProposals(from, to) - self.loading = false + self.setStatus("done") if (response.kind === "ok") { const proposals = response.proposals applySnapshot(self.proposals, proposals as any) @@ -24,7 +25,8 @@ export const ProposalsModelStore = types throw response } } catch (err) { - self.loading = false + self.setStatus("error") + throw err } }), @@ -33,22 +35,42 @@ export const ProposalsModelStore = types description: string, rate: number, limit: number, + type: string, ) { try { - self.loading = true + self.setStatus("pending") const response: PostProposal = yield self.environment.api.postProposal( name, description, rate, limit, + type, ) - self.loading = false + self.setStatus("done") if (response.kind === "ok") { + self.proposals.push(response.proposal) return true } else { throw response } - } catch (err) {} + } catch (err) { + console.log(err) + self.setStatus("error") + } + }), + getProposalTypes: flow(function* () { + // + try { + const response: GetProposalTypes = yield self.environment.api.getProposalTypes() + if (response.kind === "ok") { + const types = response.types + return types + } else { + throw response + } + } catch (err) { + self.setStatus("error") + } }), } }) diff --git a/src/models/root-store/root-store.ts b/src/models/root-store/root-store.ts index 295b308..df993f9 100644 --- a/src/models/root-store/root-store.ts +++ b/src/models/root-store/root-store.ts @@ -1,5 +1,6 @@ import { Instance, SnapshotOut, types } from "mobx-state-tree" import { AuthModel } from "../auth-model/auth-model" +import { ProposalDetailModel } from "../proposals-model/proposal-detail" import { ProposalsModelStore } from "../proposals-model/proposals-model-store" /** @@ -8,6 +9,7 @@ import { ProposalsModelStore } from "../proposals-model/proposals-model-store" export const RootStoreModel = types.model("RootStore").props({ authStore: types.optional(AuthModel, {}), proposalsStore: types.optional(ProposalsModelStore, {}), + proposalDetailStore: types.optional(ProposalDetailModel, {}), }) /** diff --git a/src/screens/create-proposal.tsx b/src/screens/create-proposal.tsx index 1dfc68e..ac6993e 100644 --- a/src/screens/create-proposal.tsx +++ b/src/screens/create-proposal.tsx @@ -1,13 +1,14 @@ -import { Form, Input, InputNumber, Button } from "antd" -import { SizeType } from "antd/lib/config-provider/SizeContext" +import { Form, Input, InputNumber, Button, Typography, Spin, Select } from "antd" import Layout, { Content } from "antd/lib/layout/layout" import { observer } from "mobx-react-lite" -import React from "react" -import { Switch } from "react-router-dom" +import React, { useEffect, useState } from "react" import styled from "styled-components" import { Header } from "../components" import { TopMenu } from "../components/menu/menu" import { useStores } from "../models/root-store/root-store-context" +const { Text } = Typography +const { Option } = Select + const StyledContent = styled(Content)` padding-top: 5vh; padding-bottom: 5vh; @@ -20,15 +21,25 @@ const StyledButton = styled(Button)` const StyledForm = styled(Form)` display: flex; flex-direction: column; + align-items: center; +` +const StyledSpinner = styled(Spin)` + height: "100%"; + width: "100%"; ` +const StyledFormItem = styled(Form.Item)` + width: 100%; +` + type FormFields = { name: string description: string rate: number limit: number + type: string } const textValidator = (_: any, value: string, callback: any) => { - if (value.split(" ").length < 100) { + if (value.split(" ").length < 10) { callback("Minimum text length is 200") } else { callback() @@ -36,10 +47,24 @@ const textValidator = (_: any, value: string, callback: any) => { } const CreateProposal = observer(function CreateProposal(props) { const { proposalsStore } = useStores() + const [types, setTypes] = useState([]) + const [form] = Form.useForm() const onFinish = async (value: FormFields) => { - const { name, description, rate, limit } = value - const resp = await proposalsStore.postProposal(name, description, rate, limit) + const { name, description, rate, limit, type } = value + try { + await proposalsStore.postProposal(name, description, rate, limit, type) + form.resetFields() + } catch (err) {} } + useEffect(() => { + proposalsStore.setStatus("idle") + ;(async function () { + const types = await proposalsStore.getProposalTypes() + if (types) { + setTypes(types.map((type) => type.value)) + } + })() + }, []) return (
@@ -47,19 +72,25 @@ const CreateProposal = observer(function CreateProposal(props) {
{ + if (proposalsStore.status !== "idle") { + proposalsStore.setStatus("idle") + } + }} onFinish={(value) => onFinish(value as FormFields)} > - - - + - - + - - + - + + + + + - Create + {proposalsStore.status === "pending" ? : "Submit"} + {proposalsStore.status === "error" && Process failed} + {proposalsStore.status === "done" && ( + Proposal submitted successfully. + )}
diff --git a/src/screens/home.tsx b/src/screens/home.tsx index a9ed114..0c20355 100644 --- a/src/screens/home.tsx +++ b/src/screens/home.tsx @@ -10,7 +10,7 @@ import { ProposalModel } from "../models/proposals-model/proposal-model" import { TopMenu } from "../components/menu/menu" import Text from "antd/lib/typography/Text" import { Header } from "../components" -const { Paragraph } = Typography +import { Link } from "react-router-dom" const StyledList = styled(List)` width: 100%; @@ -36,19 +36,19 @@ const Home = observer(function Home(props): ReactElement { {proposalsStore.proposals.map( - (e: ProposalModel): ReactNode => { + (proposal: ProposalModel): ReactNode => { return ( - + - {e.user.name[0]} + + {proposal.user.name[0]} } - title={{e.name}} - description={e.user.name} + title={{proposal.name}} + description={proposal.user.name} /> - {e.description} + {proposal.description} ) }, diff --git a/src/screens/index.ts b/src/screens/index.ts index 2bd71b9..0934834 100644 --- a/src/screens/index.ts +++ b/src/screens/index.ts @@ -2,3 +2,4 @@ export * from "./login" export * from "./register" export * from "./home" export * from "./create-proposal" +export * from "./proposal-detail" diff --git a/src/screens/proposal-detail.tsx b/src/screens/proposal-detail.tsx new file mode 100644 index 0000000..d990f45 --- /dev/null +++ b/src/screens/proposal-detail.tsx @@ -0,0 +1,77 @@ +import React, { useEffect, useState } from "react" +import { Col, Divider, Layout, Row, Typography } from "antd" +import { Content, Footer } from "antd/lib/layout/layout" +import Paragraph from "antd/lib/typography/Paragraph" +import Title from "antd/lib/typography/Title" +import { observer } from "mobx-react-lite" +import { useParams } from "react-router-dom" +import styled from "styled-components" +import { colors } from "../colors/colors" +import { Header } from "../components" +import { TopMenu } from "../components/menu/menu" +import { ProposalModel } from "../models/proposals-model/proposal-model" +import { useStores } from "../models/root-store/root-store-context" +import { UploadSection } from "./proposal-detail/upload-section" +import { AvatarWithTitle } from "../components/avatar-with-title/avatar-with-title" + +const { Text } = Typography + +interface ProposalDetailParams { + id: string +} + +const StyledContent = styled(Content)` + background-color: ${colors.backgroundPrimary}; +` +const StyledBody = styled.div` + margin-top: 3vh; + margin-bottom: 5vh; +` +const ProposalDetail = observer(function (props) { + const { id } = useParams() + const { proposalDetailStore } = useStores() + const [proposal, setProposal] = useState(null) + + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-extra-semi + ;(async () => { + await proposalDetailStore.getProposal(id) + setProposal(proposalDetailStore.proposal) + })() + }, []) + if (!proposal) { + return <> + } + return ( + +
+ +
+ + + + + + {proposal.name} + + + + + {proposal.description} + + + + + Rate: {proposal.rate.toString()}$ | Submissions: 2/100 + + + + Upload + + +
+
+ ) +}) + +export default ProposalDetail diff --git a/src/screens/proposal-detail/upload-section.tsx b/src/screens/proposal-detail/upload-section.tsx new file mode 100644 index 0000000..9b4060d --- /dev/null +++ b/src/screens/proposal-detail/upload-section.tsx @@ -0,0 +1,65 @@ +import { List, Progress } from "antd" +import { DraggerProps } from "antd/lib/upload" +import Dragger from "antd/lib/upload/Dragger" +import React, { useState } from "react" +import { InboxOutlined } from "@ant-design/icons" +import { PutFile } from "../../services/api-types" +import { observer } from "mobx-react-lite" + +interface UploadSectionProps { + store: { + putFile: (name: string, file: File, progress: any) => any + } +} + +export const UploadSection = observer( + (props: UploadSectionProps): JSX.Element => { + const { store } = props + const [progress, setProgress] = useState(0) + const [enableUpload, setEnableUpload] = useState(true) + const [uploadList, setUploadList] = useState>([]) + const uploader: DraggerProps = { + customRequest: async function (options) { + const { onSuccess, onError, file, onProgress } = options + const progress = (event: any) => { + const percent = Math.floor((event.loaded / event.total) * 100) + setProgress(percent) + if (onProgress) { + // @ts-expect-error Wrong interface usage + onProgress({ percent: (event.loaded / event.total) * 100 }) + } + } + setUploadList((list) => [...list, file.name]) + const fileResp = await store.putFile(file.name, file, progress) + if (fileResp?.kind === "ok") { + setEnableUpload(false) + } + }, + disabled: !enableUpload, + multiple: false, + showUploadList: false, + } + return ( + <> + +

+ +

+

Click or drag file to this area to upload

+
+ {uploadList.length > 0 && ( + ( + + } /> + + )} + /> + )} + + ) + }, +) diff --git a/src/services/api-helpers.ts b/src/services/api-helpers.ts index bc65d3f..9b3fc5c 100644 --- a/src/services/api-helpers.ts +++ b/src/services/api-helpers.ts @@ -3,6 +3,7 @@ import { ProposalsModelStore } from "../models/proposals-model/proposals-model-s import { UserModel } from "../models/user-model/user-model" import { LocalLogin } from "./local-types" import { Login, Proposal, User } from "./response-types" +import { cast } from "mobx-state-tree" export function parseUser(backendUser: User): UserModel { return { @@ -15,12 +16,13 @@ export function parseProposal(proposal: Proposal): ProposalModel { } export function parseProposals(proposalsList: Proposal[]): ProposalsModelStore { - return proposalsList.map((prop) => { - return { - ...parseProposal(prop), - user: parseUser(prop.user), - } - }) + return cast( + proposalsList.map((prop) => { + return { + ...parseProposal(prop), + } + }), + ) } export function parseAuth(auth: Login): LocalLogin { diff --git a/src/services/api-types.ts b/src/services/api-types.ts index 694e18c..30c9f30 100644 --- a/src/services/api-types.ts +++ b/src/services/api-types.ts @@ -1,9 +1,15 @@ +import { ProposalDetailModel } from "../models/proposals-model/proposal-detail" import { ProposalModel } from "../models/proposals-model/proposal-model" import { ProposalsModelStore } from "../models/proposals-model/proposals-model-store" import { GeneralApiProblem } from "./api-problem" import { LocalLogin } from "./local-types" +import { ProposalType } from "./response-types" export type GetUsersResult = { kind: "ok"; response: LocalLogin } | GeneralApiProblem export type PostRegister = { kind: "ok"; username: string } | GeneralApiProblem export type GetProposals = { kind: "ok"; proposals: ProposalsModelStore } | GeneralApiProblem export type PostProposal = { kind: "ok"; proposal: ProposalModel } | GeneralApiProblem +export type GetProposalTypes = { kind: "ok"; types: ProposalType[] } | GeneralApiProblem +export type GetSingleProposal = { kind: "ok"; proposal: ProposalDetailModel } | GeneralApiProblem +export type GetSignedUrl = { kind: "ok"; url: string } | GeneralApiProblem +export type PutFile = { kind: "ok" } | GeneralApiProblem diff --git a/src/services/api.ts b/src/services/api.ts index cae4e45..06aac30 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -1,16 +1,27 @@ import apisauce, { ApiResponse, ApisauceInstance } from "apisauce" import { AxiosRequestConfig } from "axios" import Cookies from "universal-cookie/es6" -import { ProposalsModelStore } from "../models/proposals-model/proposals-model-store" +import { ProposalDetailModel } from "../models/proposals-model/proposal-detail" import { parseAuth, parseProposal, parseProposals } from "./api-helpers" import { getGeneralApiProblem } from "./api-problem" -import { GetProposals, GetUsersResult, PostProposal, PostRegister } from "./api-types" +import { + GetProposals, + GetProposalTypes, + GetSignedUrl, + GetSingleProposal, + GetUsersResult, + PostProposal, + PostRegister, + PutFile, +} from "./api-types" import { ApiConfig, API_CONFIG } from "./apiconfig" +import { ProposalType } from "./response-types" export class Api { client: ApisauceInstance config: ApiConfig cookies: Cookies + awsClient: ApisauceInstance constructor(config: ApiConfig = API_CONFIG) { this.config = config @@ -22,15 +33,26 @@ export class Api { Accept: "application/json", }, }) + this.awsClient = apisauce.create({ + baseURL: this.config.baseUrl, + timeout: this.config.timeout, + headers: { + Accept: "*/*", + }, + }) // TODO: Delete on deploy this.client.addAsyncRequestTransform((request) => { + if (!request.headers["Authorization"]) { + request.headers["Authorization"] = "Bearer " + this.cookies.get("access") + } return new Promise((resolve) => setTimeout(resolve, 2000)) }) - this.client.addAsyncResponseTransform(async (response) => { - console.log(response) + this.client.addResponseTransform(async (response) => { if (response.status === 401) { await this.refresh() + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + response.config!.headers["Authorization"] = "Bearer " + this.cookies.get("access") const newResponse = await this.client.axiosInstance.request( response.config as AxiosRequestConfig, ) @@ -43,7 +65,9 @@ export class Api { } }) } - + hasCredentials(): boolean { + return this.cookies.get("access") && this.cookies.get("refresh") + } async login(email: string, password: string): Promise { const response: ApiResponse = await this.client.post("/api/public/login", { email: email, @@ -88,6 +112,8 @@ export class Api { const response: ApiResponse = await this.client.get("api/protected/refresh") if (!response.ok) { const problem = getGeneralApiProblem(response) + this.cookies.remove("refresh") + this.cookies.remove("access") if (problem) throw problem } this.cookies.set("refresh", response.data.refresh_token) @@ -126,6 +152,7 @@ export class Api { description: string, rate: number, limit: number, + type: string, ): Promise { this.client.headers["Authorization"] = "Bearer " + this.cookies.get("access") const response: ApiResponse = await this.client.post("/api/protected/proposal", { @@ -133,6 +160,7 @@ export class Api { description: description, rate: rate, limit: limit, + type: type, }) if (!response.ok) { const problem = getGeneralApiProblem(response) @@ -145,4 +173,67 @@ export class Api { return { kind: "bad-data" } } } + async getProposalTypes(): Promise { + const response: ApiResponse = await this.client.get("/api/public/proposal-types") + if (!response.ok) { + const problem = getGeneralApiProblem(response) + if (problem) throw problem + } + try { + return { kind: "ok", types: response.data as ProposalType[] } + } catch { + return { kind: "bad-data" } + } + } + async getProposal(id: string): Promise { + const response: ApiResponse = await this.client.get("/api/protected/proposal", { + proposal_id: id, + }) + if (!response.ok) { + const problem = getGeneralApiProblem(response) + if (problem) throw problem + } + try { + return { kind: "ok", proposal: response.data as ProposalDetailModel } + } catch { + return { kind: "bad-data" } + } + } + async getSignedUrl(fileName: string): Promise { + const response: ApiResponse = await this.client.get("/api/protected/signed-url", { + file_name: fileName, + }) + if (!response.ok) { + const problem = getGeneralApiProblem(response) + if (problem) throw problem + } + try { + return { kind: "ok", url: response.data.url as string } + } catch { + return { kind: "bad-data" } + } + } + async submitFile( + fileName: string, + file: File, + onProgress: (event: any) => void, + ): Promise { + const urlResponse: GetSignedUrl = await this.getSignedUrl(fileName) + if (urlResponse.kind !== "ok") { + throw urlResponse + } + const response: ApiResponse = await this.awsClient.put(urlResponse.url, file, { + headers: { + "Content-Type": file.type, + }, + onUploadProgress: (event) => { + onProgress(event) + }, + }) + if (!response.ok) { + const problem = getGeneralApiProblem(response) + if (problem) throw problem + } + return { kind: "ok" } + } } diff --git a/src/services/response-types.ts b/src/services/response-types.ts index 00d4da2..aeec47a 100644 --- a/src/services/response-types.ts +++ b/src/services/response-types.ts @@ -17,4 +17,9 @@ export interface Proposal { name: string description: string rate: number + type: string +} +export interface ProposalType { + value: string + id: number }