Skip to content

Commit

Permalink
Frontend: Adds "Save Image To Clipboard" option (#3705)
Browse files Browse the repository at this point in the history
# Description and Motivation
<!--- bulleted, high level items. use keywords (eg "closes #144" or
"fixes #4323") -->


## Img to Clipboard Feature
- closes #3703 
- repurposed "save image" function to allow saving to clipboard or
download
- makes new card option button to save img to clipboard
- adds snackbar notification that clipboard save was successful
- adds image thumbnail into snackbar notice

## Refactors
- refactors `<HetDialog>` to accept a child element to make it more
reusable, rather than sending in specific pieces of the message as props
- new reusable component for Card Export Menu Items
- new hook to co-locate all card image/share stuff
- settings tweak to auto-organize imports and remove unused imports
automatically

## Has this been tested? How?

- passing

## Screenshots (if appropriate)

<img width="1605" alt="Screenshot 2024-10-04 at 2 15 13 PM"
src="https://github.com/user-attachments/assets/398a8323-ca74-43b8-81ae-1d34cf979f82">
<img width="1605" alt="Screenshot 2024-10-04 at 2 15 26 PM"
src="https://github.com/user-attachments/assets/61691c78-976f-4c08-bf31-dfa053032619">
<img width="1605" alt="Screenshot 2024-10-04 at 2 15 41 PM"
src="https://github.com/user-attachments/assets/7c452258-ba80-41a8-a018-8257f9ba4815">


## Types of changes

(leave all that apply)

- New content or feature
- Refactor / chore

## New frontend preview link is below in the Netlify comment 😎
  • Loading branch information
benhammondmusic authored Oct 4, 2024
1 parent 781018f commit baa7331
Show file tree
Hide file tree
Showing 12 changed files with 348 additions and 194 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
],
"editor.codeActionsOnSave": {
"quickfix.biome": "explicit",
"source.organizeImports": "always",
"source.addMissingImports": "always"
},
"[javascript]": {
Expand Down
6 changes: 1 addition & 5 deletions frontend/src/cards/CardWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ import type {
MetricQueryResponse,
} from '../data/query/MetricQuery'
import { WithMetadataAndMetrics } from '../data/react/WithLoadingOrErrorUI'
import { Sources } from './ui/Sources'
import type { MapOfDatasetMetadata } from '../data/utils/DatasetTypes'
import type { ScrollableHashId } from '../utils/hooks/useStepObserver'
import CardOptionsMenu from './ui/CardOptionsMenu'
import { saveCardImage } from './ui/DownloadCardImageHelpers'
import { Sources } from './ui/Sources'

function CardWrapper(props: {
// prevent layout shift as component loads
Expand Down Expand Up @@ -55,9 +54,6 @@ function CardWrapper(props: {
tabIndex={-1}
>
<CardOptionsMenu
downloadTargetScreenshot={() =>
saveCardImage(props.scrollToHash, props.downloadTitle)
}
reportTitle={props.reportTitle}
scrollToHash={props.scrollToHash}
/>
Expand Down
18 changes: 9 additions & 9 deletions frontend/src/cards/ui/CardOptionsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import Popover from '@mui/material/Popover'
import type { PopoverOrigin } from '@mui/material/Popover'
import { DownloadCardImageButton } from './DownloadCardImageButton'
import CopyLinkButton from './CopyLinkButton'
import CardShareIcons from './CardShareIcons'
import CardShareIconButtons from './CardShareIconButtons'
import { usePopover } from '../../utils/hooks/usePopover'
import type { ScrollableHashId } from '../../utils/hooks/useStepObserver'
import { useIsBreakpointAndUp } from '../../utils/hooks/useIsBreakpointAndUp'
import { CopyCardImageToClipboardButton } from './CopyCardImageToClipboardButton'

interface CardOptionsMenuProps {
downloadTargetScreenshot: () => Promise<boolean>
reportTitle: string
scrollToHash: ScrollableHashId
}
Expand All @@ -21,9 +21,6 @@ export default function CardOptionsMenu(props: CardOptionsMenuProps) {
const shareMenu = usePopover()
const isSm = useIsBreakpointAndUp('sm')

const urlWithoutHash = window.location.href.split('#')[0]
const urlWithHash = `${urlWithoutHash}#${props.scrollToHash}`

const anchorOrigin: PopoverOrigin = {
vertical: 'top',
horizontal: 'right',
Expand Down Expand Up @@ -58,16 +55,19 @@ export default function CardOptionsMenu(props: CardOptionsMenuProps) {
<CopyLinkButton
scrollToHash={props.scrollToHash}
popover={shareMenu}
urlWithHash={urlWithHash}
/>
<CopyCardImageToClipboardButton
scrollToHash={props.scrollToHash}
popover={shareMenu}
/>
<DownloadCardImageButton
downloadTargetScreenshot={props.downloadTargetScreenshot}
popover={shareMenu}
scrollToHash={props.scrollToHash}
/>
<CardShareIcons
<CardShareIconButtons
reportTitle={props.reportTitle}
popover={shareMenu}
urlWithHash={urlWithHash}
scrollToHash={props.scrollToHash}
/>
</MenuList>
</Popover>
Expand Down
104 changes: 104 additions & 0 deletions frontend/src/cards/ui/CardShareIconButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import type { ComponentType } from 'react'
import {
EmailIcon,
EmailShareButton,
FacebookIcon,
FacebookShareButton,
LinkedinIcon,
LinkedinShareButton,
TwitterShareButton,
XIcon,
} from 'react-share'
import { het } from '../../styles/DesignTokens'
import { HetCardExportMenuItem } from '../../styles/HetComponents/HetCardExportMenuItem'
import { useCardImage } from '../../utils/hooks/useCardImage'
import type { PopoverElements } from '../../utils/hooks/usePopover'
import type { ScrollableHashId } from '../../utils/hooks/useStepObserver'

const shareIconAttributes = {
iconFillColor: het.hexShareIconGray,
bgStyle: { fill: 'none' },
size: 39,
}

interface CardShareIconButtonsProps {
popover: PopoverElements
reportTitle: string
scrollToHash: ScrollableHashId
}

interface ShareButtonConfig {
ShareButton: ComponentType<any>
Icon: ComponentType<any>
label: string
options: Record<string, any>
}

export default function CardShareIconButtons(props: CardShareIconButtonsProps) {
const title = `Health Equity Tracker - ${props.reportTitle}`
const emailShareBody = `${title}${'\n'}${'\n'}`

const { cardUrlWithHash, handleClose } = useCardImage(
props.popover,
props.scrollToHash,
)

const shareButtons: ShareButtonConfig[] = [
{
ShareButton: TwitterShareButton,
Icon: XIcon,
label: 'Share on X',
options: {
hashtags: ['healthequity'],
related: ['@SatcherHealth', '@MSMEDU'],
'aria-label': 'Share to X (formerly Twitter)',
},
},
{
ShareButton: FacebookShareButton,
Icon: FacebookIcon,
label: 'Share on Facebook',
options: {
hashtag: '#healthequity',
'aria-label': 'Post this report to Facebook',
},
},
{
ShareButton: LinkedinShareButton,
Icon: LinkedinIcon,
label: 'Share on LinkedIn',
options: {
source: 'Health Equity Tracker',
'aria-label': 'Share to LinkedIn',
},
},
{
ShareButton: EmailShareButton,
Icon: EmailIcon,
label: 'Email card link',
options: {
body: emailShareBody,
subject: 'Sharing from healthequitytracker.org',
'aria-label': 'Share by email',
},
},
]

return (
<>
{shareButtons.map(({ ShareButton, Icon, label, options }) => (
<HetCardExportMenuItem
key={label}
Icon={Icon}
onClick={handleClose}
className='p-0'
iconProps={shareIconAttributes}
>
<ShareButton url={cardUrlWithHash} {...options}>
{label}
</ShareButton>
</HetCardExportMenuItem>
))}
</>
)
}
91 changes: 0 additions & 91 deletions frontend/src/cards/ui/CardShareIcons.tsx

This file was deleted.

51 changes: 51 additions & 0 deletions frontend/src/cards/ui/CopyCardImageToClipboardButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ContentCopy } from '@mui/icons-material'
import type { PopoverElements } from '../../utils/hooks/usePopover'
import HetDialog from '../../styles/HetComponents/HetDialog'
import HetTerm from '../../styles/HetComponents/HetTerm'
import type { ScrollableHashId } from '../../utils/hooks/useStepObserver'
import SimpleBackdrop from '../../pages/ui/SimpleBackdrop'
import { useCardImage } from '../../utils/hooks/useCardImage'
import { HetCardExportMenuItem } from '../../styles/HetComponents/HetCardExportMenuItem'

interface CopyCardImageToClipboardButtonProps {
popover: PopoverElements
scrollToHash: ScrollableHashId
}

export function CopyCardImageToClipboardButton(
props: CopyCardImageToClipboardButtonProps,
) {
const {
cardName,
isThinking,
setIsThinking,
imgDataUrl,
hetDialogOpen,
handleCopyImgToClipboard,
handleClose,
} = useCardImage(props.popover, props.scrollToHash)

return (
<>
<SimpleBackdrop open={isThinking} setOpen={setIsThinking} />
<HetCardExportMenuItem
onClick={handleCopyImgToClipboard}
Icon={ContentCopy}
>
Copy Image To Clipboard
</HetCardExportMenuItem>
<HetDialog open={hetDialogOpen} handleClose={handleClose}>
Copied <HetTerm>{cardName}</HetTerm> image to clipboard!
{imgDataUrl && (
<div className='mt-4 rounded-lg overflow-hidden border border-gray-200'>
<img
src={imgDataUrl}
alt={`Preview of ${cardName}`}
className='w-full h-auto max-w-tiny object-contain bg-gray-50'
/>
</div>
)}
</HetDialog>
</>
)
}
44 changes: 13 additions & 31 deletions frontend/src/cards/ui/CopyLinkButton.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,30 @@
import { useState } from 'react'
import type { ScrollableHashId } from '../../utils/hooks/useStepObserver'
import ListItemIcon from '@mui/material/ListItemIcon'
import LinkIcon from '@mui/icons-material/Link'
import MenuItem from '@mui/material/MenuItem'
import type { PopoverElements } from '../../utils/hooks/usePopover'
import HetDialog from '../../styles/HetComponents/HetDialog'
import HetTerm from '../../styles/HetComponents/HetTerm'
import { useCardImage } from '../../utils/hooks/useCardImage'
import { HetCardExportMenuItem } from '../../styles/HetComponents/HetCardExportMenuItem'

interface CopyLinkButtonProps {
popover: PopoverElements
scrollToHash: ScrollableHashId
urlWithHash: string
}

export default function CopyLinkButton(props: CopyLinkButtonProps) {
const [open, setOpen] = useState(false)

let cardName = props.scrollToHash.replaceAll('-', ' ') ?? 'Card'
cardName = cardName[0].toUpperCase() + cardName.slice(1)

const title = `Copy direct link to: ${cardName}`

function handleClick() {
async function asyncHandleClick() {
await navigator.clipboard.writeText(props.urlWithHash)
setOpen(true)
}
asyncHandleClick().catch((error) => error)
}

function handleClose() {
setOpen(false)
props.popover.close()
}
const { cardName, hetDialogOpen, handleCopyLink, handleClose } = useCardImage(
props.popover,
props.scrollToHash,
)

return (
<>
<MenuItem aria-label={title} onClick={handleClick} className='pl-3'>
<ListItemIcon className='flex items-center px-2 py-1'>
<LinkIcon className='mx-1 w-8' />
<div className='pl-1 text-altBlack'>Copy card link</div>
</ListItemIcon>
</MenuItem>

<HetDialog open={open} cardName={cardName} handleClose={handleClose} />
<HetCardExportMenuItem Icon={LinkIcon} onClick={handleCopyLink}>
Copy Card Link
</HetCardExportMenuItem>
<HetDialog open={hetDialogOpen} handleClose={handleClose}>
Direct link to <HetTerm>{cardName}</HetTerm> copied to clipboard!
</HetDialog>
</>
)
}
Loading

0 comments on commit baa7331

Please sign in to comment.