diff --git a/public/locales/bg/campaign-application.json b/public/locales/bg/campaign-application.json index 022dcd7d8..4d69d7341 100644 --- a/public/locales/bg/campaign-application.json +++ b/public/locales/bg/campaign-application.json @@ -60,5 +60,8 @@ "terms": "Общи условия ", "faq": "Често задавани въпроси" } + }, + "alerts": { + "successfully-created": "Успешно създадена кампания. " } } diff --git a/public/locales/en/campaign-application.json b/public/locales/en/campaign-application.json index c8d3170ba..3e32e7797 100644 --- a/public/locales/en/campaign-application.json +++ b/public/locales/en/campaign-application.json @@ -60,5 +60,8 @@ "terms": "Terms and Conditions ", "faq": "Frequently Asked Questions" } + }, + "alerts": { + "successfully-created": "Campaign application successfully created." } } diff --git a/src/components/admin/campaign-applications/EditPage.tsx b/src/components/admin/campaign-applications/EditPage.tsx index 19cb1fe0b..30683f19a 100644 --- a/src/components/admin/campaign-applications/EditPage.tsx +++ b/src/components/admin/campaign-applications/EditPage.tsx @@ -7,8 +7,11 @@ import AdminContainer from 'components/common/navigation/AdminContainer' import AdminLayout from 'components/common/navigation/AdminLayout' import CampaignApplicationAdminPropsEdit from './CampaignApplicationAdminPropsEdit' import { CampaignApplicationAdminEdit } from './campaignApplicationAdmin.types' +import { useState } from 'react' export default function EditPage() { + const [files, setFiles] = useState([]) + const initialValues = { organizer: { name: 'Some organizer', @@ -30,7 +33,7 @@ export default function EditPage() {
.
.
- +
.
diff --git a/src/components/client/campaign-application/CampaignApplicationForm.tsx b/src/components/client/campaign-application/CampaignApplicationForm.tsx index fdbbcc6bc..59521a002 100644 --- a/src/components/client/campaign-application/CampaignApplicationForm.tsx +++ b/src/components/client/campaign-application/CampaignApplicationForm.tsx @@ -1,6 +1,6 @@ -import { useCallback, useState } from 'react' import { Grid, StepLabel } from '@mui/material' import { Person } from 'gql/person' +import { useCallback, useState } from 'react' import { CampaignApplicationFormData, @@ -9,48 +9,49 @@ import { } from './helpers/campaignApplication.types' import GenericForm from 'components/common/form/GenericForm' -import CampaignApplicationStepperIcon from './CampaignApplicationStepperIcon' -import CampaignApplicationOrganizer from './steps/CampaignApplicationOrganizer' -import CampaignApplicationDetails from './steps/CampaignApplicationDetails' -import CampaignApplication from './steps/CampaignApplication' import CampaignApplicationFormActions from './CampaignApplicationFormActions' import CampaignApplicationRemark from './CampaignApplicationRemark' -import stepsHandler from './helpers/stepsHandler' +import CampaignApplicationStepperIcon from './CampaignApplicationStepperIcon' +import CampaignApplication from './steps/CampaignApplication' +import CampaignApplicationDetails from './steps/CampaignApplicationDetails' +import CampaignApplicationOrganizer from './steps/CampaignApplicationOrganizer' import { validationSchema } from './helpers/validation-schema' -import { - StyledCampaignApplicationStep, - StyledCampaignApplicationStepper, - StyledStepConnector, -} from './helpers/campaignApplication.styled' import { useMutation } from '@tanstack/react-query' +import { AxiosError, AxiosResponse, isAxiosError } from 'axios' +import { FormikHelpers } from 'formik' import { CreateCampaignApplicationInput, CreateCampaignApplicationResponse, + UploadCampaignApplicationFilesRequest, + UploadCampaignApplicationFilesResponse, } from 'gql/campaign-applications' -import { AxiosError, AxiosResponse, isAxiosError } from 'axios' -import { ApiErrors, matchValidator } from 'service/apiErrors' -import { useCreateCampaignApplication } from 'service/campaign-application' -import { AlertStore } from 'stores/AlertStore' +import { CampaignTypesResponse } from 'gql/campaign-types' import { t } from 'i18next' -import { CampaignTypeCategory } from 'components/common/campaign-types/categories' -import { FormikHelpers } from 'formik' +import { useTranslation } from 'next-i18next' +import { ApiErrors, matchValidator } from 'service/apiErrors' +import { + useCreateCampaignApplication, + useUploadCampaignApplicationFiles, +} from 'service/campaign-application' import { useCampaignTypesList } from 'service/campaignTypes' -import { CampaignTypesResponse } from 'gql/campaign-types' +import { AlertStore } from 'stores/AlertStore' +import { + StyledCampaignApplicationStep, + StyledCampaignApplicationStepper, + StyledStepConnector, +} from './helpers/campaignApplication.styled' const steps: StepType[] = [ { title: 'campaign-application:steps.organizer.title', - component: , }, { title: 'campaign-application:steps.campaign-application.title', - component: , }, { title: 'campaign-application:steps.campaign-application-details.title', - component: , }, ] @@ -59,8 +60,10 @@ type Props = { } export default function CampaignApplicationForm({ person }: Props) { + const { t } = useTranslation('campaign-application') const [activeStep, setActiveStep] = useState(Steps.ORGANIZER) const isLast = activeStep === Steps.CAMPAIGN_DETAILS + const [submitting, setSubmitting] = useState(false) const initialValues: CampaignApplicationFormData = { organizer: { @@ -90,18 +93,30 @@ export default function CampaignApplicationForm({ person }: Props) { }, } + const [files, setFiles] = useState([]) + const { data } = useCampaignTypesList() - const { mutation } = useCreateApplication() + const create = useCreateApplication() const handleSubmit = async ( formData: CampaignApplicationFormData, { setFieldError, resetForm }: FormikHelpers, ) => { if (isLast) { + if (submitting) { + return + } + setSubmitting(true) try { - await mutation.mutateAsync(mapCreateInput(formData, data ?? [])) + const createInput = mapCreateInput(formData, data ?? []) + await create(createInput, files) resetForm() + setSubmitting(false) + AlertStore.show(t('alerts.successfully-created'), 'success') + + // take user to next campaign application page } catch (error) { + setSubmitting(false) console.error(error) if (isAxiosError(error)) { const { response } = error as AxiosError @@ -111,7 +126,7 @@ export default function CampaignApplicationForm({ person }: Props) { } } } else { - stepsHandler({ activeStep, setActiveStep }) + setActiveStep((prevActiveStep) => prevActiveStep + 1) } } @@ -136,13 +151,18 @@ export default function CampaignApplicationForm({ person }: Props) { - {activeStep < steps.length && steps[activeStep].component} + {activeStep === Steps.ORGANIZER && } + {activeStep === Steps.CAMPAIGN && } + {activeStep === Steps.CAMPAIGN_DETAILS && ( + + )} @@ -155,7 +175,7 @@ export default function CampaignApplicationForm({ person }: Props) { } const useCreateApplication = () => { - const mutation = useMutation< + const create = useMutation< AxiosResponse, AxiosError, CreateCampaignApplicationInput @@ -165,15 +185,23 @@ const useCreateApplication = () => { onSuccess: () => AlertStore.show(t('common:alerts.message-sent'), 'success'), }) - // const fileUploadMutation = useMutation< - // AxiosResponse, - // AxiosError, - // UploadCampaignFiles - // >({ - // mutationFn: useUploadCampaignFiles(), - // }) + const fileUpload = useMutation< + AxiosResponse, + AxiosError, + UploadCampaignApplicationFilesRequest + >({ + mutationFn: useUploadCampaignApplicationFiles(), + }) + + return async (i: CreateCampaignApplicationInput, files: File[]) => { + const { + data: { id }, + } = await create.mutateAsync(i) - return { mutation } + await fileUpload.mutateAsync({ campaignApplicationId: id, files }) + + return { id } + } } function mapCreateInput( diff --git a/src/components/client/campaign-application/CampaignApplicationFormActions.tsx b/src/components/client/campaign-application/CampaignApplicationFormActions.tsx index 3b2bc6cae..e1443040d 100644 --- a/src/components/client/campaign-application/CampaignApplicationFormActions.tsx +++ b/src/components/client/campaign-application/CampaignApplicationFormActions.tsx @@ -17,12 +17,14 @@ type CampaignApplicationFormActionsProps = { activeStep: number onBack?: (event: MouseEvent) => void isLast: boolean + submitting: boolean } export default function CampaignApplicationFormActions({ onBack, activeStep, isLast, + submitting, }: CampaignApplicationFormActionsProps) { const { t } = useTranslation('campaign-application') @@ -51,6 +53,7 @@ export default function CampaignApplicationFormActions({ fullWidth label={t(isLast ? 'cta.submit' : 'cta.next')} endIcon={} + disabled={submitting} /> diff --git a/src/components/client/campaign-application/helpers/campaignApplication.types.ts b/src/components/client/campaign-application/helpers/campaignApplication.types.ts index 50e3a5990..c97b19852 100644 --- a/src/components/client/campaign-application/helpers/campaignApplication.types.ts +++ b/src/components/client/campaign-application/helpers/campaignApplication.types.ts @@ -1,6 +1,5 @@ export type Step = { title: string - component: JSX.Element } export enum Steps { diff --git a/src/components/client/campaign-application/helpers/stepsHandler.ts b/src/components/client/campaign-application/helpers/stepsHandler.ts deleted file mode 100644 index f7c2cbedb..000000000 --- a/src/components/client/campaign-application/helpers/stepsHandler.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { SetStateAction } from 'react' - -import { Steps } from './campaignApplication.types' - -interface stepsHandlerProps { - activeStep: Steps - setActiveStep: (value: SetStateAction) => void -} - -export default function stepsHandler({ activeStep, setActiveStep }: stepsHandlerProps) { - switch (activeStep) { - case Steps.ORGANIZER: - { - setActiveStep((prevActiveStep) => prevActiveStep + 1) - } - break - case Steps.CAMPAIGN: - { - setActiveStep((prevActiveStep) => prevActiveStep + 1) - } - break - case Steps.CAMPAIGN_DETAILS: - { - setActiveStep((prevActiveStep) => prevActiveStep + 1) - } - break - default: - return 'Unknown step' - } -} diff --git a/src/components/client/campaign-application/steps/CampaignApplicationDetails.tsx b/src/components/client/campaign-application/steps/CampaignApplicationDetails.tsx index 2353b9eea..733974aa8 100644 --- a/src/components/client/campaign-application/steps/CampaignApplicationDetails.tsx +++ b/src/components/client/campaign-application/steps/CampaignApplicationDetails.tsx @@ -1,13 +1,20 @@ -import { useTranslation } from 'next-i18next' import { Grid, Typography } from '@mui/material' +import { useTranslation } from 'next-i18next' -import { StyledStepHeading } from '../helpers/campaignApplication.styled' import FormTextField from 'components/common/form/FormTextField' +import { StyledStepHeading } from '../helpers/campaignApplication.styled' import theme from 'common/theme' +import FileList from 'components/common/file-upload/FileList' import FileUpload from 'components/common/file-upload/FileUpload' +import { Dispatch, SetStateAction } from 'react' + +export type Props = { + files: File[] + setFiles: Dispatch> +} -export default function CampaignApplicationDetails() { +export default function CampaignApplicationDetails({ files, setFiles }: Props) { const { t } = useTranslation('campaign-application') return ( @@ -85,18 +92,22 @@ export default function CampaignApplicationDetails() { { - return + buttonLabel={t('steps.details.documents')} + onUpload={(newFiles) => { + setFiles((prevFiles) => [...prevFiles, ...newFiles]) }} /> - - - { - return + + setFiles((prevFiles) => prevFiles.filter((file) => file.name !== deletedFile.name)) + } + rolesList={{}} + onSetFileRole={() => { + // we have no roles for the campaign application - it's all a document + return undefined }} + filesRole={[]} /> diff --git a/src/components/common/file-upload/FileList.tsx b/src/components/common/file-upload/FileList.tsx index 63aa84e0b..26d9bd9c6 100644 --- a/src/components/common/file-upload/FileList.tsx +++ b/src/components/common/file-upload/FileList.tsx @@ -49,24 +49,26 @@ function FileList({ rolesList, files, onDelete, onSetFileRole, filesRole = [] }: - - {'Избери роля'} - - id="choose-type" - size="small" - label="Избери роля" - labelId="choose-type-label" - value={ - filesRole.find((f) => f.file === file.name)?.role ?? CampaignFileRole.background - } - onChange={setFileRole(file)}> - {Object.values(rolesList).map((role) => ( - - {role} - - ))} - - + {Array.isArray(filesRole) && filesRole.length > 0 && ( + + {'Избери роля'} + + id="choose-type" + size="small" + label="Избери роля" + labelId="choose-type-label" + value={ + filesRole.find((f) => f.file === file.name)?.role ?? CampaignFileRole.background + } + onChange={setFileRole(file)}> + {Object.values(rolesList).map((role) => ( + + {role} + + ))} + + + )} ))} diff --git a/src/gql/campaign-applications.ts b/src/gql/campaign-applications.ts index d4ecd26e8..7ed1a12cd 100644 --- a/src/gql/campaign-applications.ts +++ b/src/gql/campaign-applications.ts @@ -1,6 +1,6 @@ import { CampaignTypeCategory } from 'components/common/campaign-types/categories' -export class CreateCampaignApplicationInput { +export interface CreateCampaignApplicationInput { /** * What would the campaign be called. ('Help Vesko' or 'Castrate Plovdiv Cats') */ @@ -57,3 +57,10 @@ export class CreateCampaignApplicationInput { export type CreateCampaignApplicationResponse = CreateCampaignApplicationInput & { id: string } + +export interface UploadCampaignApplicationFilesRequest { + campaignApplicationId: string + files: File[] +} + +export type UploadCampaignApplicationFilesResponse = unknown diff --git a/src/service/apiEndpoints.ts b/src/service/apiEndpoints.ts index 6b004e9bc..24c5d0df4 100644 --- a/src/service/apiEndpoints.ts +++ b/src/service/apiEndpoints.ts @@ -426,5 +426,7 @@ export const endpoints = { }, campaignApplication: { create: { url: '/campaign-application/create', method: 'POST' }, + uploadFile: (campaignId: string) => + { url: `/campaign-application/uploadFile/${campaignId}`, method: 'POST' }, }, } diff --git a/src/service/campaign-application.ts b/src/service/campaign-application.ts index d0ac528d9..1fda99e1e 100644 --- a/src/service/campaign-application.ts +++ b/src/service/campaign-application.ts @@ -1,13 +1,15 @@ import { AxiosResponse } from 'axios' import { useSession } from 'next-auth/react' -import { apiClient } from 'service/apiClient' -import { endpoints } from 'service/apiEndpoints' -import { authConfig } from 'service/restRequests' import { CreateCampaignApplicationInput, CreateCampaignApplicationResponse, + UploadCampaignApplicationFilesRequest, + UploadCampaignApplicationFilesResponse, } from 'gql/campaign-applications' +import { apiClient } from 'service/apiClient' +import { endpoints } from 'service/apiEndpoints' +import { authConfig } from 'service/restRequests' export const useCreateCampaignApplication = () => { const { data: session } = useSession() @@ -17,3 +19,23 @@ export const useCreateCampaignApplication = () => { AxiosResponse >(endpoints.campaignApplication.create.url, data, authConfig(session?.accessToken)) } + +export const useUploadCampaignApplicationFiles = () => { + const { data: session } = useSession() + return async ({ files, campaignApplicationId }: UploadCampaignApplicationFilesRequest) => { + const formData = new FormData() + files.forEach((file: File) => { + formData.append('file', file) + }) + return await apiClient.post>( + endpoints.campaignApplication.uploadFile(campaignApplicationId).url, + formData, + { + headers: { + ...authConfig(session?.accessToken).headers, + 'Content-Type': 'multipart/form-data', + }, + }, + ) + } +}