diff --git a/public/locales/bg/common.json b/public/locales/bg/common.json index be2308538..88e9f7dc3 100644 --- a/public/locales/bg/common.json +++ b/public/locales/bg/common.json @@ -135,6 +135,12 @@ "agree-with-newsletter": "Съгласявам се да получавам известия.", "agree-with-newsletter-campaign": "Съгласявам се да получавам новини за тази кампания и известия от Подкрепи.бг." }, + "files": { + "attached-files": "Прикачени файлове", + "download": "Изтегляне", + "errorDeletingFile": "Грешка при изтриване на файл", + "deletedFile": "Успешно изтрит файл" + }, "cookieConsent": "Подкрепи.бг не използва бисквитки, освен тези от трети страни, нужни за аналитичните компоненти Google Analytics и HotJar. Приемането на бисквитките ще ни помогне да подобрим вашето потребителско преживяване.", "cookieConsentButton": "Приемам", "cookieRejectButton": "Отхвърлям", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 0bc4b9909..dfc66738d 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -135,6 +135,12 @@ "agree-with-newsletter": "I agree to receive news.", "agree-with-newsletter-campaign": "I agree to receive news about this campaign and news by Podkrepi.bg." }, + "files": { + "attached-files": "Attached files", + "download": "Download", + "errorDeletingFile": "Failure deleting file", + "deletedFile": "Successfully deleted file" + }, "cookieConsent": "Podkrepi.bg doesn't use cookies, except the third-party cookies required for the analytics components Google Analytics and HotJar. Accepting the cookies will help us improve your user experience.", "cookieConsentButton": "Accept", diff --git a/src/components/admin/campaign-applications/CampaignApplicationAdminPropsEdit.tsx b/src/components/admin/campaign-applications/CampaignApplicationAdminPropsEdit.tsx index 52171a6c7..0bfaeec3a 100644 --- a/src/components/admin/campaign-applications/CampaignApplicationAdminPropsEdit.tsx +++ b/src/components/admin/campaign-applications/CampaignApplicationAdminPropsEdit.tsx @@ -9,8 +9,16 @@ import { import { CamAppDetail } from 'components/client/campaign-application/steps/CampaignApplicationSummary' import CheckboxField from 'components/common/form/CheckboxField' import OrganizerCanEditAt from './CampaignApplicationOrganizerCanEditAt' +import { UploadedFile } from 'components/common/file-upload/UploadedFilesList' +import UploadedCampaignApplicationFiles from './UploadedCampaignApplicationFiles' -export default function CampaignApplicationAdminPropsEdit({ id }: { id: string }) { +export default function CampaignApplicationAdminPropsEdit({ + id, + files, +}: { + id: string + files: UploadedFile[] +}) { const { t } = useTranslation('campaign-application') return ( @@ -40,6 +48,9 @@ export default function CampaignApplicationAdminPropsEdit({ id }: { id: string } value={} /> + + + ) } diff --git a/src/components/admin/campaign-applications/EditPage.tsx b/src/components/admin/campaign-applications/EditPage.tsx index 7fdc80e11..76aa23936 100644 --- a/src/components/admin/campaign-applications/EditPage.tsx +++ b/src/components/admin/campaign-applications/EditPage.tsx @@ -2,6 +2,7 @@ import { Box, CircularProgress, Grid, Typography } from '@mui/material' import { red } from '@mui/material/colors' import { CampaignApplicationFormData } from 'components/client/campaign-application/helpers/campaignApplication.types' import { ActionSubmitButton } from 'components/client/campaign-application/helpers/campaignApplicationFormActions.styled' +import { campaignApplicationAdminValidationSchema } from 'components/client/campaign-application/helpers/validation-schema' import CampaignApplicationBasic from 'components/client/campaign-application/steps/CampaignApplicationBasic' import CampaignApplicationDetails from 'components/client/campaign-application/steps/CampaignApplicationDetails' import CampaignApplicationOrganizer from 'components/client/campaign-application/steps/CampaignApplicationOrganizer' @@ -21,7 +22,6 @@ import { } from 'service/campaign-application' import CampaignApplicationAdminPropsEdit from './CampaignApplicationAdminPropsEdit' import OrganizerCanEditAt from './CampaignApplicationOrganizerCanEditAt' -import { campaignApplicationAdminValidationSchema } from 'components/client/campaign-application/helpers/validation-schema' export type Props = { id: string @@ -83,7 +83,7 @@ export function EditLoadedCampaign({ campaign }: { campaign: CampaignApplication }} initialValues={c.initialValues} validationSchema={campaignApplicationAdminValidationSchema.defined()}> - + diff --git a/src/components/admin/campaign-applications/UploadedCampaignApplicationFiles.tsx b/src/components/admin/campaign-applications/UploadedCampaignApplicationFiles.tsx new file mode 100644 index 000000000..457dcbf7b --- /dev/null +++ b/src/components/admin/campaign-applications/UploadedCampaignApplicationFiles.tsx @@ -0,0 +1,43 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { AxiosError } from 'axios' +import { useTranslation } from 'next-i18next' + +import { endpoints } from 'service/apiEndpoints' +import { ApiErrors } from 'service/apiErrors' + +import { UploadedFile, UploadedFilesList } from 'components/common/file-upload/UploadedFilesList' + +import { useSession } from 'next-auth/react' +import { + fetchCampaignApplicationFile, + useDeleteCampaignApplicationFile, +} from 'service/campaign-application' +import { AlertStore } from 'stores/AlertStore' + +type Props = { + campaignApplicationId: string + files: UploadedFile[] +} + +export default function UploadedCampaignApplicationFiles({ files, campaignApplicationId }: Props) { + const { t } = useTranslation(['common', 'campaign-applications']) + const queryClient = useQueryClient() + const { data: session } = useSession() + + const del = useMutation, string>({ + mutationFn: (fileId) => useDeleteCampaignApplicationFile(session)(fileId), + onError: () => AlertStore.show(t('common:alerts.errorDeletingFile'), 'error'), + onSuccess: () => { + AlertStore.show(t('common:files.deletedFile'), 'success') + queryClient.invalidateQueries([endpoints.campaignApplication.view(campaignApplicationId).url]) + }, + }) + + return ( + fetchCampaignApplicationFile(f.id, session).then((r) => r.data)} + deleteMutation={(f) => del.mutateAsync(f.id)} + /> + ) +} diff --git a/src/components/common/file-upload/UploadedFilesList.tsx b/src/components/common/file-upload/UploadedFilesList.tsx new file mode 100644 index 000000000..4c3e15a25 --- /dev/null +++ b/src/components/common/file-upload/UploadedFilesList.tsx @@ -0,0 +1,218 @@ +import { Close, Delete, FilePresent, Preview } from '@mui/icons-material' +import { + Avatar, + Button, + IconButton, + List, + ListItem, + ListItemAvatar, + ListItemText, + Tooltip, +} from '@mui/material' +import { useTranslation } from 'next-i18next' +import { createContext, useContext, useEffect, useState } from 'react' +import CenteredSpinner from '../CenteredSpinner' + +export interface FilesListContext { + deleteMutation: (file: UploadedFile) => Promise + files: UploadedFile[] + getBlobUrl: (file: UploadedFile) => Promise + download: (file: UploadedFile) => void +} + +const UploadedFilesContext = createContext({ + files: [], + download: () => Promise.reject('download query missing'), + deleteMutation: () => Promise.reject('delete mutation missing'), + getBlobUrl: () => Promise.reject('Function not implemented.'), +}) + +export interface UploadedFilesListProps { + title?: string | JSX.Element | undefined + files: UploadedFile[] + downloadQuery: (f: UploadedFile) => Promise + deleteMutation: (f: UploadedFile) => Promise + downloadText?: string +} + +export function UploadedFilesList({ + title: maybeTitle, + files, + downloadQuery, + deleteMutation, + downloadText: maybeDownloadText, +}: UploadedFilesListProps) { + const { t } = useTranslation('common') + const filesCtx = filesListContext({ + files, + downloadQuery, + deleteMutation, + }) + + const title = maybeTitle != null ?? t('files.attached-files') + const downloadText = maybeDownloadText ?? t('files.download') + + return ( + + + {typeof title === 'string' ? : title} + {(files ?? []).map((file) => ( + + ))} + + + ) +} + +export type UploadedFileViewProps = { + file: UploadedFile + role?: string + downloadText: string +} + +export function UploadedFileView({ file, role, downloadText }: UploadedFileViewProps) { + const { deleteMutation, download } = useContext(UploadedFilesContext) + + return ( + + + + + + + + {role && } + <> + + + + + deleteMutation(file)}> + + + + ) +} + +export function FilePreview(f: UploadedFile) { + const { getBlobUrl } = useContext(UploadedFilesContext) + const [fetch, setFetch] = useState(false) + const [display, setDisplay] = useState<'block' | 'none'>('none') + const [blobUrl, setBlobUrl] = useState() + + useEffect(() => { + if (fetch) { + setDisplay('block') + getBlobUrl(f) + .then(setBlobUrl) + .then(() => setFetch(false)) + .catch((e) => { + console.log(e) + }) + } + }, [fetch]) + return ( + <> + + setFetch(true)}> + + + +
+ {display === 'block' ? ( +