From db8d5c7e327ee135a077b65e0bd00ecf6e391599 Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert-Lif Date: Tue, 17 Feb 2026 12:11:19 -0500 Subject: [PATCH 1/2] donation overlay v2 --- src/components/common/buttons/Button.tsx | 4 +- .../buttons/button_types/DonateButton.tsx | 20 -- .../donate_button/DonateButton.module.css | 178 ++++++++++ .../donate_button/DonateButton.tsx | 325 ++++++++++++++++++ src/components/common/buttons/index.ts | 2 +- .../common/overlay/OverlayBackdrop.module.css | 4 + .../common/overlay/OverlayBackdrop.tsx | 77 +++++ src/components/layout/Footer.tsx | 2 +- src/components/layout/Header.tsx | 31 +- 9 files changed, 598 insertions(+), 45 deletions(-) delete mode 100644 src/components/common/buttons/button_types/DonateButton.tsx create mode 100644 src/components/common/buttons/button_types/donate_button/DonateButton.module.css create mode 100644 src/components/common/buttons/button_types/donate_button/DonateButton.tsx create mode 100644 src/components/common/overlay/OverlayBackdrop.module.css create mode 100644 src/components/common/overlay/OverlayBackdrop.tsx diff --git a/src/components/common/buttons/Button.tsx b/src/components/common/buttons/Button.tsx index cd1b2009..c5059d49 100644 --- a/src/components/common/buttons/Button.tsx +++ b/src/components/common/buttons/Button.tsx @@ -23,7 +23,7 @@ export type BaseButtonProps = BaseVisualProps & { target?: React.HTMLAttributeAnchorTarget rel?: string -} +} & Omit, keyof BaseVisualProps> export function BaseButton(props: BaseButtonProps) { const { @@ -38,6 +38,7 @@ export function BaseButton(props: BaseButtonProps) { renderContent, target, rel, + ...rest } = props const showNavChevron = @@ -92,6 +93,7 @@ export function BaseButton(props: BaseButtonProps) { disabled={disabled} onClick={onClick} className={finalClassName} + {...rest} > {content} diff --git a/src/components/common/buttons/button_types/DonateButton.tsx b/src/components/common/buttons/button_types/DonateButton.tsx deleted file mode 100644 index 9037152f..00000000 --- a/src/components/common/buttons/button_types/DonateButton.tsx +++ /dev/null @@ -1,20 +0,0 @@ -'use client' - -import { BaseButton, BaseVisualProps } from '../Button' -import buttonStyles from '@/components/common/buttons/Button.module.css' - -const DONATE_HREF = 'https://secure.actblue.com/donate/pvwebsite' - -export type DonateButtonProps = BaseVisualProps - -export function DonateButton(props: DonateButtonProps) { - const { className, ...rest } = props - - const mergedClassName = [buttonStyles.prominent, className] - .filter(Boolean) - .join(' ') - - return ( - - ) -} diff --git a/src/components/common/buttons/button_types/donate_button/DonateButton.module.css b/src/components/common/buttons/button_types/donate_button/DonateButton.module.css new file mode 100644 index 00000000..d94bfeb1 --- /dev/null +++ b/src/components/common/buttons/button_types/donate_button/DonateButton.module.css @@ -0,0 +1,178 @@ +.overlayLayer { + position: fixed; + inset: 0; + z-index: 101; + display: flex; + + align-items: center; + justify-content: center; + + overflow: hidden; + + padding-top: env(safe-area-inset-top); + padding-bottom: env(safe-area-inset-bottom); +} + +.container { + width: 100%; + height: 100%; + max-height: 100vh; + + display: flex; + align-items: center; + justify-content: center; + + padding: 3rem 1rem; + box-sizing: border-box; + + overflow: auto; + -webkit-overflow-scrolling: touch; +} + +.content { + width: 100%; + max-width: 1400px; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + + box-sizing: border-box; +} + +.topBar { + width: 100%; + padding-left: 2vw; + padding-right: 2vw; + + display: flex; + justify-content: flex-start; + align-items: center; + + margin-bottom: 1.25rem; +} + +.backButtonGuard { + display: inline-flex; +} + +.cardsWrapper { + display: flex; + flex-direction: column; + align-items: stretch; + justify-content: center; + gap: 2.5rem; + + padding-left: 2vw; + padding-right: 2vw; + width: 100%; +} + +@media (min-width: 1280px) { + .cardsWrapper { + flex-direction: row; + align-items: stretch; + } +} + +.card { + position: relative; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + + width: 100%; + min-height: 280px; + padding: 2rem 1.5rem 5rem 1.5rem; + + background-color: rgba(9, 34, 58, 0.88); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0px 10px 20px rgba(0, 0, 0, 0.25); + border-radius: 0.75rem; +} + +.card:hover { + transform: translateY(-4px); +} + +@media (min-width: 768px) { + .card { + height: auto; + } +} + +@media (min-width: 1280px) { + .card { + flex: 1; + } +} + +.cardHeader { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + margin-bottom: 1rem; +} + +.cardContent { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.cardTitle { + font-size: 1.5rem; + font-weight: 700; + color: white; +} + +.cardDescription { + font-size: 1rem; + font-weight: 500; + color: white; +} + +.cardButton { + position: absolute; + bottom: 1.5rem; + right: 1.5rem; +} + +.preferredText { + position: absolute; + bottom: 1.2rem; + left: 1.5rem; + + height: 2.75rem; + + display: inline-flex; + align-items: center; + + font-size: 1.05rem; + font-weight: 750; + letter-spacing: 0.01em; + + color: rgba(255, 255, 255, 0.92); + + text-shadow: 0 8px 22px rgba(0, 0, 0, 0.35); +} + +@media (max-width: 1279px) { + .memberCard { + order: -1; + } + + .overlayLayer { + align-items: flex-start; + justify-content: center; + } + + .container { + padding: calc(env(safe-area-inset-top) + 2.25rem) 1rem + calc(env(safe-area-inset-bottom) + 1.5rem); + align-items: flex-start; + } +} diff --git a/src/components/common/buttons/button_types/donate_button/DonateButton.tsx b/src/components/common/buttons/button_types/donate_button/DonateButton.tsx new file mode 100644 index 00000000..e887a070 --- /dev/null +++ b/src/components/common/buttons/button_types/donate_button/DonateButton.tsx @@ -0,0 +1,325 @@ +'use client' + +import { BaseButton, BaseVisualProps } from '../../Button' +import overlayStyles from './DonateButton.module.css' +import buttonStyles from '@/components/common/buttons/Button.module.css' +import { OverlayBackdrop } from '@/components/common/overlay/OverlayBackdrop' +import useInView from '@/util/hooks/useInView' +import { AnimatePresence, motion } from 'motion/react' +import Image from 'next/image' +import React, { useEffect, useRef, useState } from 'react' +import { createPortal } from 'react-dom' + +export type DonateButtonProps = BaseVisualProps + +export function DonateButton(props: DonateButtonProps) { + const { className, ...rest } = props + + const mergedClassName = [buttonStyles.prominent, className] + .filter(Boolean) + .join(' ') + + const [mounted, setMounted] = useState(false) + const [open, setOpen] = useState(false) + + const openOverlay = () => { + setMounted(true) + requestAnimationFrame(() => setOpen(true)) + } + + const closeOverlay = () => { + setOpen(false) + } + + const toggleOverlay = () => { + if (!mounted) openOverlay() + else closeOverlay() + } + + return ( +
+ + + {mounted ? ( + setMounted(false)} + /> + ) : null} +
+ ) +} + +const MEMBER_HREF = 'https://secure.actblue.com/donate/pvmember' +const DONATE_HREF = 'https://secure.actblue.com/donate/pvwebsite' +const MERCH_HREF = 'https://progressivevictory.myshopify.com/' + +type DonationActionKey = 'single' | 'member' | 'merch' + +const donationActions: { + key: DonationActionKey + image: string + title: string + description: string + children: React.ReactNode +}[] = [ + { + key: 'single', + image: '/images/Halftone-Phone.webp', + title: 'Large Donation', + description: 'Select this option to make larger donations. ', + children: ( + + ), + }, + { + key: 'member', + image: '/images/Halftone-Handshake.webp', + title: 'Dues Paying Member', + description: 'Become a Dues Paying Member and get your very own PV Membership Card!', + children: ( + + ), + }, + { + key: 'merch', + image: '/images/Halftone-Clipboard.webp', + title: 'Merchandise', + description: 'Click here to check out our merch store.', + children: ( + + ), + }, +] + +interface DonationOverlayProps { + open: boolean + onRequestClose: () => void + onExited: () => void +} + +export function DonationOverlay({ + open, + onRequestClose, + onExited, +}: DonationOverlayProps) { + const { inView, observe } = useInView() + const divRef = useRef(null) + const [visible, setVisible] = useState(false) + + useEffect(() => { + if (divRef.current) observe(divRef.current) + }, [observe]) + + useEffect(() => { + if (inView) setVisible(true) + }, [inView]) + + useEffect(() => { + if (!open) return + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') onRequestClose() + } + document.addEventListener('keydown', onKeyDown) + return () => document.removeEventListener('keydown', onKeyDown) + }, [open, onRequestClose]) + + return createPortal( + <> + + + + {open ? ( + +
+
+
+
e.stopPropagation()} + > + ( + + + + + Back + + + )} + /> +
+
+ +
e.stopPropagation()} + > + {visible && + donationActions.map((action, index) => ( + + {action.children} + + ))} +
+ +
+
+
+ + ) : null} + + , + document.body + ) +} + +function Card({ + cardKey, + image, + title, + description, + delay = 0, + children, +}: { + cardKey: 'single' | 'member' | 'merch' + image: string + title: string + description: string + delay?: number + children: React.ReactNode +}) { + const cardClassName = + cardKey === 'member' + ? `${overlayStyles.card} ${overlayStyles.memberCard}` + : overlayStyles.card + + return ( + +
+

{title}

+ {title} +
+ +
+

{description}

+
+ + {cardKey === 'member' ? ( +
+ Preferred +
+ ) : null} + +
{children}
+
+ ) +} diff --git a/src/components/common/buttons/index.ts b/src/components/common/buttons/index.ts index 463bd49d..f329228f 100644 --- a/src/components/common/buttons/index.ts +++ b/src/components/common/buttons/index.ts @@ -5,5 +5,5 @@ export * from './button_types/SubNavButton' export * from './button_types/MobileNavButton' export * from './button_types/LoginButton' export * from './button_types/LogoutButton' -export * from './button_types/DonateButton' +export * from './button_types/donate_button/DonateButton' export * from './button_types/AccountButton' diff --git a/src/components/common/overlay/OverlayBackdrop.module.css b/src/components/common/overlay/OverlayBackdrop.module.css new file mode 100644 index 00000000..8e2f7c93 --- /dev/null +++ b/src/components/common/overlay/OverlayBackdrop.module.css @@ -0,0 +1,4 @@ +.backdrop { + position: fixed; + inset: 0; +} diff --git a/src/components/common/overlay/OverlayBackdrop.tsx b/src/components/common/overlay/OverlayBackdrop.tsx new file mode 100644 index 00000000..33ad1ca7 --- /dev/null +++ b/src/components/common/overlay/OverlayBackdrop.tsx @@ -0,0 +1,77 @@ +'use client' + +import styles from './OverlayBackdrop.module.css' +import cx from 'classnames' +import { AnimatePresence, motion } from 'motion/react' +import { createPortal } from 'react-dom' +import { useEffect, useMemo, useState } from 'react' + +export interface OverlayBackdropProps { + open: boolean + onClick?: () => void + zIndex?: number + blurPx?: number + tintColor?: string + + lockBodyScroll?: boolean + className?: string +} + +export function OverlayBackdrop({ + open, + onClick, + zIndex = 30, + blurPx = 10, + tintColor = 'rgba(0, 0, 0, 0.18)', + lockBodyScroll = false, + className, +}: OverlayBackdropProps) { + const [mounted, setMounted] = useState(false) + + useEffect(() => { + setMounted(true) + }, []) + + useEffect(() => { + if (!lockBodyScroll) return + if (!open) return + + const prevOverflow = document.body.style.overflow + document.body.style.overflow = 'hidden' + return () => { + document.body.style.overflow = prevOverflow + } + }, [open, lockBodyScroll]) + + const style = useMemo( + () => + ({ + zIndex, + backdropFilter: `blur(${blurPx}px)`, + WebkitBackdropFilter: `blur(${blurPx}px)`, + backgroundColor: tintColor, + }) as React.CSSProperties, + [zIndex, blurPx, tintColor] + ) + + if (!mounted) return null + + return createPortal( + + {open ? ( + , + document.body + ) +} \ No newline at end of file diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index 40fa2690..0ba5717e 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -4,9 +4,9 @@ import { LogoutButton } from '../common/buttons/button_types/LogoutButton' import { NavItem } from './types' import { BaseButton } from '@/components/common/buttons/Button' import buttonStyles from '@/components/common/buttons/Button.module.css' -import { DonateButton } from '@/components/common/buttons/button_types/DonateButton' import { LoginButton } from '@/components/common/buttons/button_types/LoginButton' import { NavButton } from '@/components/common/buttons/button_types/NavButton' +import { DonateButton } from '@/components/common/buttons/button_types/donate_button/DonateButton' import styles from '@/components/layout/footer.module.css' import { motion } from 'motion/react' import { useSession } from 'next-auth/react' diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 136f3093..01299854 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -4,10 +4,11 @@ import { NavItem } from './types' import { BaseButton } from '@/components/common/buttons/Button' import buttonStyles from '@/components/common/buttons/Button.module.css' import { AccountButton } from '@/components/common/buttons/button_types/AccountButton' -import { DonateButton } from '@/components/common/buttons/button_types/DonateButton' import { LoginButton } from '@/components/common/buttons/button_types/LoginButton' import { NavButton } from '@/components/common/buttons/button_types/NavButton' import { SubNavButton } from '@/components/common/buttons/button_types/SubNavButton' +import { DonateButton } from '@/components/common/buttons/button_types/donate_button/DonateButton' +import { OverlayBackdrop } from '@/components/common/overlay/OverlayBackdrop' import styles from '@/components/layout/header.module.css' import { AnimatePresence, motion } from 'motion/react' import { useSession } from 'next-auth/react' @@ -822,27 +823,13 @@ export function Header() { ) : null} - - {isOpen && ( - + setIsOpen(false)} + zIndex={30} + blurPx={10} + tintColor="rgba(0, 0, 0, 0.18)" + /> Date: Wed, 18 Feb 2026 09:53:13 -0500 Subject: [PATCH 2/2] Update DonateButton.tsx --- .../buttons/button_types/donate_button/DonateButton.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/common/buttons/button_types/donate_button/DonateButton.tsx b/src/components/common/buttons/button_types/donate_button/DonateButton.tsx index e887a070..cd35beb0 100644 --- a/src/components/common/buttons/button_types/donate_button/DonateButton.tsx +++ b/src/components/common/buttons/button_types/donate_button/DonateButton.tsx @@ -55,7 +55,7 @@ export function DonateButton(props: DonateButtonProps) { ) } -const MEMBER_HREF = 'https://secure.actblue.com/donate/pvmember' +const MEMBER_HREF = 'https://secure.actblue.com/donate/pvmember?refcode=website' const DONATE_HREF = 'https://secure.actblue.com/donate/pvwebsite' const MERCH_HREF = 'https://progressivevictory.myshopify.com/' @@ -71,8 +71,8 @@ const donationActions: { { key: 'single', image: '/images/Halftone-Phone.webp', - title: 'Large Donation', - description: 'Select this option to make larger donations. ', + title: 'Donation', + description: 'Select this option to make simple donations. ', children: ( ), },