diff --git a/src/components/admin/bank-transactions/grid/RenderEditBankDonationStatusCell.tsx b/src/components/admin/bank-transactions/grid/RenderEditBankDonationStatusCell.tsx index d4df07ab9..27052725b 100644 --- a/src/components/admin/bank-transactions/grid/RenderEditBankDonationStatusCell.tsx +++ b/src/components/admin/bank-transactions/grid/RenderEditBankDonationStatusCell.tsx @@ -61,7 +61,6 @@ export default function RenderBankDonationStatusCell({ params }: RenderCellProps const handleError = (e: AxiosError) => { const error = e.response?.data?.message - console.log(e.response) AlertStore.show(error ? error : t('common:alerts.error'), 'error') } diff --git a/src/components/admin/campaigns/grid/EditForm.tsx b/src/components/admin/campaigns/grid/EditForm.tsx index 6d6642c09..657b9d714 100644 --- a/src/components/admin/campaigns/grid/EditForm.tsx +++ b/src/components/admin/campaigns/grid/EditForm.tsx @@ -50,6 +50,7 @@ import { fromMoney, toMoney } from 'common/util/money' import CurrencySelect from 'components/common/currency/CurrencySelect' import OrganizerSelect from './OrganizerSelect' import AllowDonationOnComplete from '../../../common/form/AllowDonationOnComplete' +import { base64ImageUploader } from './helpers/base64ImageUploader' const formatString = 'yyyy-MM-dd' @@ -67,7 +68,7 @@ const validationSchema: yup.SchemaOf .shape({ title: yup.string().trim().min(10).max(200).required(), slug: yup.string().trim().min(10).max(200).required(), - description: yup.string().trim().min(50).max(60000).required(), + description: yup.string().trim().min(50).required(), targetAmount: yup.number().integer().positive().required(), allowDonationOnComplete: yup.bool().optional(), campaignTypeId: yup.string().uuid().required(), @@ -181,10 +182,23 @@ export default function EditForm({ campaign }: { campaign: AdminSingleCampaignRe { setFieldError }: FormikHelpers, ) => { try { + //replace base64 images with uploaded images + const descriptionWithServerImages = await base64ImageUploader( + values.description, + campaign.id, + fileUploadMutation, + ) + + //this manual validation is needed because yup validations happen before the base64ImageUploader + if (descriptionWithServerImages.length > 60000) { + return AlertStore.show(t('Description field should be less than 60,000 symbols'), 'error') + } + + //save campaign await mutation.mutateAsync({ title: values.title, slug: createSlug(values.slug), - description: values.description, + description: descriptionWithServerImages, targetAmount: toMoney(values.targetAmount), allowDonationOnComplete: values.allowDonationOnComplete, startDate: values.startDate, diff --git a/src/components/admin/campaigns/grid/helpers/base64ImageUploader.tsx b/src/components/admin/campaigns/grid/helpers/base64ImageUploader.tsx new file mode 100644 index 000000000..1497ae969 --- /dev/null +++ b/src/components/admin/campaigns/grid/helpers/base64ImageUploader.tsx @@ -0,0 +1,82 @@ +import { UseMutationResult } from '@tanstack/react-query' +import { AxiosError, AxiosResponse } from 'axios' +import { CampaignFileRole, UploadCampaignFiles } from 'components/common/campaign-file/roles' +import { CampaignUploadImage } from 'gql/campaigns' +import { ApiErrors } from 'service/apiErrors' +import crypto from 'crypto' +import getConfig from 'next/config' + +const { publicRuntimeConfig } = getConfig() + +/** + * This function finds all base64 image links in the given string, uploads them to the server + * and replaces them with the podkrepi.bg server links. + */ +export const base64ImageUploader = async function ( + textWithLinks: string, + campaignId: string, + fileUploadMutation: UseMutationResult< + AxiosResponse, + AxiosError, + UploadCampaignFiles, + unknown + >, +): Promise { + const urlPromises: Promise[] = [] + + //this is a replacer function that will be called for each base64 image + const getReplacementUrls = async ( + _matchedBase64: string, + imageType: string, + base64Data: string, + ) => { + //upload the imageData to the server and get the url + const fileUrl = await uploadImage(campaignId, base64Data, imageType, fileUploadMutation) + return fileUrl + } + + //now call the replacer to upload all images and return the urls + const base64regex = /data:image\/(png|jpg|jpeg|gif);base64,([^"]+)/g + textWithLinks.replaceAll(base64regex, (m, p1, p2) => { + urlPromises.push(getReplacementUrls(m, p1, p2)) + return '' + }) + + //and wait on all replacement promisses in the array and run the replacer again with the returned urls + return Promise.all(urlPromises).then((urls) => { + textWithLinks = textWithLinks.replaceAll(base64regex, () => urls.shift() as string) + return textWithLinks + }) +} + +//upload image to server and return the url +async function uploadImage( + campaignId: string, + base64Data: string, + imageType: string, + fileUploadMutation: UseMutationResult< + AxiosResponse, + AxiosError, + UploadCampaignFiles, + unknown + >, +): Promise { + const imageBuffer = Buffer.from(base64Data, 'base64') + + //we don't know the name so we hash the image for unique name to avoid uploading the same image twice + const fileName = crypto.createHash('sha256').update(imageBuffer).digest('hex') + '.' + imageType + const imageFile = new File([imageBuffer], fileName, { type: 'image/' + imageType }) + + return await fileUploadMutation + .mutateAsync({ + campaignId: campaignId, + files: [imageFile], + roles: [{ file: imageFile.name, role: CampaignFileRole.campaignPhoto }], + }) + .then((response) => { + return `${publicRuntimeConfig.APP_URL}/api/v1/campaign-file/` + response.data[0] + }) + .catch(() => { + return '' + }) +} diff --git a/src/components/admin/donations/grid/GridFilters.tsx b/src/components/admin/donations/grid/GridFilters.tsx index 31a044646..06375df7d 100644 --- a/src/components/admin/donations/grid/GridFilters.tsx +++ b/src/components/admin/donations/grid/GridFilters.tsx @@ -30,7 +30,6 @@ export default observer(function GridFilters() { filterName: string, filterValue: string | number | null | { from: Date; to: Date }, ) => { - console.log('Setting filter:', filterName, filterValue) donationStore.setDonationFilters(filterName, filterValue) } diff --git a/src/components/client/donation-flow/common/RadioCardGroup.tsx b/src/components/client/donation-flow/common/RadioCardGroup.tsx index c99755692..d1299d34f 100644 --- a/src/components/client/donation-flow/common/RadioCardGroup.tsx +++ b/src/components/client/donation-flow/common/RadioCardGroup.tsx @@ -78,7 +78,6 @@ function RadioCardGroup({ options, defaultValue }: RadioCardGroupProps) { setValue(event.target.value) } - console.log(theme.typography.h1) return ( { - window.scrollTo({ top: 200, behavior: 'smooth' }) -} - -export default function OneTimeDonation({ slug }: { slug: string }) { - const { data, isLoading } = useViewCampaign(slug) - // const paymentIntentMutation = useCreatePaymentIntent({ - // amount: 100, - // currency: 'BGN', - // }) - // useEffect(() => { - // paymentIntentMutation.mutate() - // }, []) - if (isLoading || !data) return - - const { campaign } = data - - const beneficiaryAvatarSource = beneficiaryCampaignPictureUrl(campaign) - - return ( - - - - - - - - - {campaign.title} - - - {/* {paymentIntentMutation.isLoading ? ( - - ) : ( - - )} */} - - {/* */} - {/* */} - - - - ) -} +import Link from 'next/link' + +import { Grid, Typography } from '@mui/material' + +import theme from 'common/theme' +import { routes } from 'common/routes' +import { beneficiaryCampaignPictureUrl } from 'common/util/campaignImageUrls' +import Layout from 'components/client/layout/Layout' +import { useViewCampaign } from 'common/hooks/campaigns' +import CenteredSpinner from 'components/common/CenteredSpinner' +import DonationStepper from '../Steps' + +import { + BeneficiaryAvatar, + BeneficiaryAvatarWrapper, + StepperWrapper, +} from './OneTimeDonation.styles' + +// import RadioAccordionGroup, { testRadioOptions } from 'components/donation-flow/common/RadioAccordionGroup' +// import RadioCardGroup, { testRadioOptions } from 'components/donation-flow/common/RadioCardGroup' +// import PaymentDetailsStripeForm from 'components/admin/donations/stripe/PaymentDetailsStripeForm' + +const scrollWindow = () => { + window.scrollTo({ top: 200, behavior: 'smooth' }) +} + +export default function OneTimeDonation({ slug }: { slug: string }) { + const { data, isLoading } = useViewCampaign(slug) + // const paymentIntentMutation = useCreatePaymentIntent({ + // amount: 100, + // currency: 'BGN', + // }) + // useEffect(() => { + // paymentIntentMutation.mutate() + // }, []) + if (isLoading || !data) return + + const { campaign } = data + + const beneficiaryAvatarSource = beneficiaryCampaignPictureUrl(campaign) + + return ( + + + + + + + + + {campaign.title} + + + {/* {paymentIntentMutation.isLoading ? ( + + ) : ( + + )} */} + + {/* */} + {/* */} + + + + ) +} diff --git a/src/components/common/form/FormRichTextField.tsx b/src/components/common/form/FormRichTextField.tsx index 72a7a4d5b..b0334dbe9 100644 --- a/src/components/common/form/FormRichTextField.tsx +++ b/src/components/common/form/FormRichTextField.tsx @@ -14,24 +14,28 @@ import BlotFormatter from 'quill-blot-formatter/' Quill.register('modules/blotFormatter', BlotFormatter) import htmlEditButton from 'quill-html-edit-button' + +Quill.register({ + 'modules/htmlEditButton': htmlEditButton, +}) + Quill.register({ 'modules/htmlEditButton': htmlEditButton, }) -export type RegisterFormProps = { +export type FormRichTextFieldProps = { name: string } -export default function FormRichTextField({ name }: RegisterFormProps) { +export default function FormRichTextField({ name }: FormRichTextFieldProps) { const { t } = useTranslation() const [, meta] = useField(name) const helperText = meta.touched ? translateError(meta.error as TranslatableField, t) : '' const reactQuillRef = useRef(null) - //this image handler inserts the image into the editor as URL to eternally hosted image - //TODO: find a way to upload the image to our backend - function handleImageInsert() { + //this image handler inserts the image into the editor as URL to externally hosted image + function handleImageUrlInsert() { let imageUrl = prompt('Enter the URL of the image:') // for a better UX find a way to use the Quill Tooltip or a Modal box if (!imageUrl) return const editor = reactQuillRef.current?.getEditor() @@ -65,7 +69,7 @@ export default function FormRichTextField({ name }: RegisterFormProps) { ['link', 'video', 'image'], ['clean'], ], - handlers: { image: handleImageInsert }, + handlers: { image: handleImageUrlInsert }, }, clipboard: { // toggle to add extra line breaks when pasting HTML: diff --git a/src/pages/api/auth/[...nextauth].ts b/src/pages/api/auth/[...nextauth].ts index 27f8177c5..5a81ad4f7 100644 --- a/src/pages/api/auth/[...nextauth].ts +++ b/src/pages/api/auth/[...nextauth].ts @@ -84,8 +84,6 @@ export const authOptions: NextAuthOptions = { session.accessToken = token.accessToken session.refreshToken = token.refreshToken - console.log('Returning session from api/auth') - return session }, async jwt({ token, user, account }) { diff --git a/src/service/restRequests.ts b/src/service/restRequests.ts index 9682d1d91..f3fe4088b 100644 --- a/src/service/restRequests.ts +++ b/src/service/restRequests.ts @@ -12,11 +12,8 @@ const { export async function fetchSession(): Promise { const res = await apiClient.get('/api/auth/session', { baseURL: APP_URL }) const session = res.data - console.log('Fetching session from /api/auth/session') - console.log(session) if (Object.keys(session).length) { - console.log('Fetching session successful.') return session } console.warn('Fetching session returned null.')