diff --git a/packages/donate-button-v4/src/components/widget/api/index.ts b/packages/donate-button-v4/src/components/widget/api/index.ts index 258520bd..43a21b2f 100644 --- a/packages/donate-button-v4/src/components/widget/api/index.ts +++ b/packages/donate-button-v4/src/components/widget/api/index.ts @@ -1,3 +1,4 @@ +import {DonateFlowCustomization} from 'src/components/widget/types/DonateFlowCustomization'; import {Fundraiser} from 'src/components/widget/types/Fundraiser'; import {Nonprofit} from 'src/components/widget/types/Nonprofit'; import {BASE_API_URL, BASE_COINGECKO_URL} from 'src/constants/url'; @@ -50,3 +51,13 @@ export async function getCoingeckoRate(coingeckoId: string) { return (data as CoingeckoData).market_data.current_price.usd; } + +export async function getCustomization(nonprofitId: string, code?: string) { + const url = `${BASE_API_URL}/${nonprofitId}/customization${ + code ? `?code=${code}` : '' + }`; + const response: DonateFlowCustomization = await fetch(url).then( + async (response) => response.json() + ); + return response; +} diff --git a/packages/donate-button-v4/src/components/widget/components/Checkbox/index.tsx b/packages/donate-button-v4/src/components/widget/components/Checkbox/index.tsx index 16eb6604..e1dc1ec3 100644 --- a/packages/donate-button-v4/src/components/widget/components/Checkbox/index.tsx +++ b/packages/donate-button-v4/src/components/widget/components/Checkbox/index.tsx @@ -15,7 +15,6 @@ type CheckboxProps = JSXInternal.HTMLAttributes; export const Checkbox: FunctionComponent = ({ children, checked, - className, ...rest }) => { const {primaryColor} = useConfigContext(); diff --git a/packages/donate-button-v4/src/components/widget/components/NonprofitInfo/FundraiserCard.tsx b/packages/donate-button-v4/src/components/widget/components/NonprofitInfo/FundraiserCard.tsx index e7bd8622..d82ebdb5 100644 --- a/packages/donate-button-v4/src/components/widget/components/NonprofitInfo/FundraiserCard.tsx +++ b/packages/donate-button-v4/src/components/widget/components/NonprofitInfo/FundraiserCard.tsx @@ -43,8 +43,7 @@ const LargeFundraiserCard = ({fundraiser}: FundraiserCardProps) => { const { name: nonprofitName, logoCloudinaryId, - coverImageCloudinaryId, - hasAdmin + coverImageCloudinaryId } = useNonprofitOrError(); const logoUrl = getCloudinaryUrl( logoCloudinaryId ?? LOGO_IMAGE_PLACEHOLDER_ID @@ -78,17 +77,10 @@ const LargeFundraiserCard = ({fundraiser}: FundraiserCardProps) => { }; const SmallFundraiserCard = ({fundraiser}: FundraiserCardProps) => { - const { - name: nonprofitName, - logoCloudinaryId, - coverImageCloudinaryId, - hasAdmin - } = useNonprofitOrError(); + const {logoCloudinaryId} = useNonprofitOrError(); const logoUrl = getCloudinaryUrl( logoCloudinaryId ?? LOGO_IMAGE_PLACEHOLDER_ID ); - const coverImageUrl = - coverImageCloudinaryId && getCloudinaryUrl(coverImageCloudinaryId); return ( diff --git a/packages/donate-button-v4/src/components/widget/components/PaymentProcess/CryptoFlow/index.tsx b/packages/donate-button-v4/src/components/widget/components/PaymentProcess/CryptoFlow/index.tsx index 8910d65a..8a356d80 100644 --- a/packages/donate-button-v4/src/components/widget/components/PaymentProcess/CryptoFlow/index.tsx +++ b/packages/donate-button-v4/src/components/widget/components/PaymentProcess/CryptoFlow/index.tsx @@ -1,6 +1,7 @@ import {ErrorMessage} from 'src/components/widget/components/ErrorMessage'; import {CryptoAmountInput} from 'src/components/widget/components/PaymentProcess/CryptoFlow/CryptoAmountInput'; import {CryptoSelector} from 'src/components/widget/components/PaymentProcess/CryptoFlow/CryptoSelector'; +import {CustomFields} from 'src/components/widget/components/PaymentProcess/CustomFields'; import { LargePaymentMethodSelect, SmallPaymentMethodSelect @@ -57,6 +58,7 @@ export const CryptoFlow = () => { )} + diff --git a/packages/donate-button-v4/src/components/widget/components/PaymentProcess/CustomFields/index.tsx b/packages/donate-button-v4/src/components/widget/components/PaymentProcess/CustomFields/index.tsx new file mode 100644 index 00000000..5b57289b --- /dev/null +++ b/packages/donate-button-v4/src/components/widget/components/PaymentProcess/CustomFields/index.tsx @@ -0,0 +1,324 @@ +import {Fragment} from 'preact/jsx-runtime'; +import {useEffect, useRef, useState} from 'preact/hooks'; +import {rotateCss} from 'src/components/widget/components/Faq/styles'; +import { + fieldSetCss, + legendCss +} from 'src/components/widget/components/PaymentProcess/styles'; +import {TextArea} from 'src/components/widget/components/TextInput'; +import { + textInputContainerCss, + textInputCss +} from 'src/components/widget/components/TextInput/styles'; +import {useConfigContext} from 'src/components/widget/hooks/useConfigContext'; +import {useCustomizationOrUndefined} from 'src/components/widget/hooks/useCustmization'; +import {useWidgetContext} from 'src/components/widget/hooks/useWidgetContext'; +import {ArrowIcon} from 'src/components/widget/icons/ArrowIcon'; +import {getColoredBorder, Borders} from 'src/components/widget/theme/borders'; +import {BREAKPOINTS} from 'src/components/widget/theme/breakpoints'; +import {COLORS} from 'src/components/widget/theme/colors'; +import {textSize} from 'src/components/widget/theme/font-sizes'; +import {Radii} from 'src/components/widget/theme/radii'; +import {Spacing, verticalStackCss} from 'src/components/widget/theme/spacing'; +import { + CustomField, + CustomFieldType +} from 'src/components/widget/types/DonateFlowCustomization'; +import css from 'src/helpers/css'; +import joinClassNames from 'src/helpers/joinClassNames'; + +const dropdownContainerCss = css({ + position: 'absolute', + width: '100%', + background: COLORS.White, + borderRadius: `0 0 ${Radii.Default} ${Radii.Default}`, + border: getColoredBorder(Borders.Normal, COLORS.LightGray), + borderTop: 'none' +}); + +const dropdownInputRowCss = css({ + display: 'flex', + alignItems: 'center', + gap: Spacing.XXS +}); + +const dropdownContentCss = css({ + overflowY: 'scroll', + height: '100%', + maxHeight: '200px' +}); + +const dropdownItemCss = css({ + border: 'none', + background: 'none', + padding: `${Spacing.S} ${Spacing.M}`, + ...textSize.s, + width: '100%', + display: 'flex', + alignItems: 'center', + gap: Spacing.XS, + ':hover': { + cursor: 'pointer' + } +}); + +const dropdownSuffixCss = (primaryColor: string, open: boolean) => + css({ + marginLeft: 'auto', + ...textSize.xs, + color: COLORS.TextGray, + display: 'flex', + gap: Spacing.XS, + alignItems: 'center', + '> svg': { + color: primaryColor, + ...(open ? rotateCss : {}) + } + }); + +const dropdownOpenContainerCss = css({ + borderRadius: `${Radii.Default} ${Radii.Default} 0 0` +}); + +// Mobile: shown; desktop: hidden +const nativeSelectWrapperCss = css({ + [BREAKPOINTS.TabletUp]: {display: 'none'} +}); + +// Mobile: hidden; desktop: shown +const customDropdownWrapperCss = css({ + display: 'none', + position: 'relative', + [BREAKPOINTS.TabletUp]: {display: 'block'} +}); + +const nativeSelectCss = css({ + fontSize: textSize.s.fontSize, + lineHeight: textSize.s.fontSize, + fontFamily: 'inherit', + flex: 1, + border: 'none', + outline: 'none', + background: 'transparent', + width: '100%', + padding: 0, + margin: 0, + color: COLORS.Text, + cursor: 'pointer', + appearance: 'none' +}); + +const nativeSelectSuffixCss = (primaryColor: string) => + css({ + marginLeft: 'auto', + display: 'flex', + alignItems: 'center', + '> svg': {color: primaryColor} + }); + +const dropdownTriggerCss = css({cursor: 'pointer'}); + +const selectedValueCss = css({ + fontSize: textSize.s.fontSize, + lineHeight: textSize.s.fontSize, + fontFamily: 'inherit', + flex: 1, + background: 'transparent', + padding: 0, + margin: 0, + userSelect: 'none' +}); + +const placeholderValueCss = css({color: COLORS.TextGray}); + +interface DropdownFieldProps { + field: CustomField; + value: string; + onChange: (value: string) => void; + primaryColor: string; +} + +const DropdownField = ({ + field, + value, + onChange, + primaryColor +}: DropdownFieldProps) => { + const [showDropDown, setShowDropDown] = useState(false); + const [selectedOption, setSelectedOption] = useState(value); + const containerRef = useRef(null); + + useEffect(() => { + if (!showDropDown) return; + const handleClickOutside = (event: MouseEvent) => { + if ( + containerRef.current && + !containerRef.current.contains(event.target as Node) + ) { + setShowDropDown(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [showDropDown]); + + const displayText = + selectedOption || (field.placeholder ?? 'Select an option'); + + return ( +
+
{ + setShowDropDown((previous) => !previous); + }} + onKeyDown={({key}) => { + if (key === 'Enter' || key === ' ') setShowDropDown((p) => !p); + }} + > + + {displayText} + +
+ +
+
+ {showDropDown && ( +
{ + e.stopPropagation(); + }} + > +
+
    + {(field.options ?? []).map((option) => ( +
  • + +
  • + ))} +
+
+
+ )} +
+ ); +}; + +export const CustomFields = () => { + const customization = useCustomizationOrUndefined(); + const {customFieldValues, setCustomFieldValues} = useWidgetContext(); + const {primaryColor} = useConfigContext(); + + const fields = customization?.fields; + if (!fields || fields.length === 0) return null; + + const handleChange = (heading: string, value: string) => { + setCustomFieldValues((previous) => ({...previous, [heading]: value})); + }; + + console.log('Rendering custom fields with values', customFieldValues); + return ( +
+ {fields.map((field) => ( +
+ + {field.heading} + {field.required && ' *'} + + {field.type === CustomFieldType.TEXT_AREA ? ( +