From e6fbc136a7ce66b71cc373b59491ef61c3144dca Mon Sep 17 00:00:00 2001 From: gscott04 Date: Mon, 1 Dec 2025 18:51:24 -0500 Subject: [PATCH 1/2] wip: step1 layout + config, need to work on styling --- apps/frontend/src/api/apiClient.ts | 2 +- .../src/containers/donations/DonationForm.tsx | 2 + .../donations/donation-form.config.ts | 31 ++ .../donations/donation-form.types.ts | 10 +- .../src/containers/donations/donations.css | 89 +++++ .../donations/steps/Step1Amount.tsx | 329 +++++++++++++----- 6 files changed, 366 insertions(+), 97 deletions(-) create mode 100644 apps/frontend/src/containers/donations/donation-form.config.ts diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index ad621f0..3d9e59e 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -12,7 +12,7 @@ export type DonationCreateRequest = { donationType: 'one_time' | 'recurring'; dedicationMessage: string; // allow '' from ui showDedicationPublicly: boolean; - recurringInterval?: 'weekly' | 'bimonthly' | 'monthly' | 'quarterly'; + recurringInterval?: 'weekly' | 'monthly' | 'yearly'; }; export type CreateDonationResponse = { id: string }; diff --git a/apps/frontend/src/containers/donations/DonationForm.tsx b/apps/frontend/src/containers/donations/DonationForm.tsx index ce10f8d..76070a2 100644 --- a/apps/frontend/src/containers/donations/DonationForm.tsx +++ b/apps/frontend/src/containers/donations/DonationForm.tsx @@ -30,6 +30,8 @@ export const DonationForm: React.FC = ({ dedicationMessage: '', showDedicationPublicly: false, recurringInterval: 'monthly', + isDedicated: false, + dedicationKind: null, cardNumber: '', cardExpiry: '', cardCvc: '', diff --git a/apps/frontend/src/containers/donations/donation-form.config.ts b/apps/frontend/src/containers/donations/donation-form.config.ts new file mode 100644 index 0000000..77faf39 --- /dev/null +++ b/apps/frontend/src/containers/donations/donation-form.config.ts @@ -0,0 +1,31 @@ +import type { RecurringInterval } from './donation-form.types'; + +export const DONATION_PRESET_AMOUNTS: number[] = [10, 30, 50, 100]; + +export interface DonationRecurrenceOption { + donationType: 'one_time' | 'recurring'; + label: string; + recurringInterval?: RecurringInterval; +} + +export const DONATION_RECURRENCE_OPTIONS: DonationRecurrenceOption[] = [ + { + donationType: 'one_time', + label: 'One Time', + }, + { + donationType: 'recurring', + label: 'Weekly', + recurringInterval: 'weekly', + }, + { + donationType: 'recurring', + label: 'Monthly', + recurringInterval: 'monthly', + }, + { + donationType: 'recurring', + label: 'Yearly', + recurringInterval: 'yearly', + }, +]; diff --git a/apps/frontend/src/containers/donations/donation-form.types.ts b/apps/frontend/src/containers/donations/donation-form.types.ts index d8ff930..74690a1 100644 --- a/apps/frontend/src/containers/donations/donation-form.types.ts +++ b/apps/frontend/src/containers/donations/donation-form.types.ts @@ -1,8 +1,6 @@ -export type RecurringInterval = - | 'weekly' - | 'bimonthly' - | 'monthly' - | 'quarterly'; +export type RecurringInterval = 'weekly' | 'monthly' | 'yearly'; + +export type DedicationKind = 'honor' | 'memory'; export type DonationStep = 1 | 2 | 3 | 4; @@ -13,6 +11,8 @@ export interface DonationFormData { amount: string; donationType: 'one_time' | 'recurring'; recurringInterval: RecurringInterval; + isDedicated?: boolean; + dedicationKind?: DedicationKind | null; isAnonymous: boolean; dedicationMessage: string; showDedicationPublicly: boolean; diff --git a/apps/frontend/src/containers/donations/donations.css b/apps/frontend/src/containers/donations/donations.css index ec6c78e..7e132c0 100644 --- a/apps/frontend/src/containers/donations/donations.css +++ b/apps/frontend/src/containers/donations/donations.css @@ -157,3 +157,92 @@ margin-top: 1rem; margin-bottom: 1rem; } + +.recurrence-options { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 0.5rem; +} + +.recurrence-option { + padding: 0.4rem 0.8rem; + border-radius: 9999px; + border: 1px solid #ccc; + background-color: #fff; + font-size: 0.9rem; + cursor: pointer; + transition: + background-color 0.15s ease, + border-color 0.15s ease; +} + +.recurrence-option.selected { + border-color: #2a7a73; + background-color: #e0f2f1; + font-weight: 600; +} + +.recurrence-option:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.amount-grid { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.amount-button { + flex: 1 1 20%; + padding: 0.5rem 0.75rem; + border-radius: 4px; + border: 1px solid #ccc; + background-color: #fff; + font-size: 0.95rem; + cursor: pointer; + transition: + background-color 0.15s ease, + border-color 0.15s ease; +} + +.amount-button.selected { + border-color: #2a7a73; + background-color: #e0f2f1; + font-weight: 600; +} + +.amount-button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.amount-custom-row { + margin-top: 0.5rem; + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.amount-custom-label { + font-size: 0.85rem; + font-weight: 600; +} + +.amount-custom-input { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.amount-custom-input input { + flex: 1; +} + +.amount-currency-prefix, +.amount-currency-suffix { + font-size: 0.85rem; + color: #555; +} diff --git a/apps/frontend/src/containers/donations/steps/Step1Amount.tsx b/apps/frontend/src/containers/donations/steps/Step1Amount.tsx index 40ba6e1..b7caf9e 100644 --- a/apps/frontend/src/containers/donations/steps/Step1Amount.tsx +++ b/apps/frontend/src/containers/donations/steps/Step1Amount.tsx @@ -1,5 +1,15 @@ import React from 'react'; -import type { DonationFormData, FormErrors } from '../donation-form.types'; +import type { + DonationFormData, + FormErrors, + DedicationKind, +} from '../donation-form.types'; + +import { + DONATION_PRESET_AMOUNTS, + DONATION_RECURRENCE_OPTIONS, + type DonationRecurrenceOption, +} from '../donation-form.config'; interface Step1AmountProps { formData: DonationFormData; @@ -12,34 +22,154 @@ interface Step1AmountProps { ) => void; } +// helpers to synthesize events so we can reuse DonationForm.handleInputChange + +const triggerTextChange = ( + onChange: Step1AmountProps['onChange'], + name: string, + value: string, +) => { + onChange({ + target: { + name, + value, + type: 'text', + }, + } as React.ChangeEvent); +}; + +const triggerCheckboxToggle = ( + onChange: Step1AmountProps['onChange'], + name: string, + checked: boolean, +) => { + onChange({ + target: { + name, + type: 'checkbox', + checked, + value: checked ? 'on' : '', + }, + } as React.ChangeEvent); +}; + export const Step1Amount: React.FC = ({ formData, errors, isSubmitting, onChange, }) => { + // donation amount presets + const handlePresetAmountClick = (amount: number) => { + triggerTextChange(onChange, 'amount', String(amount)); + }; + + const isAmountSelected = (amount: number) => + formData.amount === String(amount); + + // donation recurrence btns + const handleRecurrenceClick = (option: DonationRecurrenceOption) => { + // always set donationType + triggerTextChange(onChange, 'donationType', option.donationType); + + // for recurring options, also set the recurringInterval field + if (option.donationType === 'recurring' && option.recurringInterval) { + triggerTextChange( + onChange, + 'recurringInterval', + option.recurringInterval, + ); + } + }; + + const isRecurrenceSelected = (option: DonationRecurrenceOption) => { + if (option.donationType === 'one_time') { + return formData.donationType === 'one_time'; + } + return ( + formData.donationType === 'recurring' && + option.recurringInterval === formData.recurringInterval + ); + }; + + // dedication btns + const handleDedicationKindClick = (kind: DedicationKind) => { + triggerTextChange(onChange, 'dedicationKind', kind); + }; + + const isDedicationKindSelected = (kind: DedicationKind) => + formData.dedicationKind === kind; + return (
-

Step 1: Choose amount

+

Step 1: Choose amount & frequency

+ +
+ +
+ {DONATION_RECURRENCE_OPTIONS.map((option) => ( + + ))} +
+ {errors.recurringInterval && ( + {errors.recurringInterval} + )} +
- + +
+ {DONATION_PRESET_AMOUNTS.map((amount) => ( + + ))} +
+ +
+ Custom Amount +
+ $ + + USD +
+
+ {errors.amount && ( {errors.amount} @@ -48,88 +178,105 @@ export const Step1Amount: React.FC = ({
- - -
- - {formData.donationType === 'recurring' && ( -
- - - {errors.recurringInterval && ( - - {errors.recurringInterval} - - )} +
+
+ + Display name as anonymous when publicly shown +
- )} - -
-
- -