Merge pull request #286 from omnivore-app/OMN-189

[OMN-189] : Settings View - Emails
This commit is contained in:
Jackson Harper 2022-03-23 15:23:35 -07:00 committed by GitHub
commit 13d80399de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 520 additions and 85 deletions

View file

@ -63,10 +63,17 @@ const StyledLabel = styled(Label, {
cursor: 'default',
})
export type DropdownAlignment =
| 'start'
| 'end'
| 'center'
type DropdownProps = {
labelText?: string
triggerElement: React.ReactNode
children: React.ReactNode
styledArrow?: boolean
align?: DropdownAlignment
}
export const DropdownSeparator = styled(Separator, {
@ -102,10 +109,11 @@ export function Dropdown(props: DropdownProps): JSX.Element {
// remove focus from dropdown
;(document.activeElement as HTMLElement).blur()
}}
align={props.align ? props.align : 'center'}
>
{props.labelText && <StyledLabel>{props.labelText}</StyledLabel>}
{props.children}
<StyledArrow offset={20} />
{props.styledArrow && <StyledArrow offset={20} />}
</DropdownContent>
</Root>
)

View file

@ -0,0 +1,77 @@
import React, { FC } from 'react';
import { styled, keyframes } from '@stitches/react';
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
const slideUpAndFade = keyframes({
'0%': { opacity: 0, transform: 'translateY(2px)' },
'100%': { opacity: 1, transform: 'translateY(0)' },
});
const slideRightAndFade = keyframes({
'0%': { opacity: 0, transform: 'translateX(-2px)' },
'100%': { opacity: 1, transform: 'translateX(0)' },
});
const slideDownAndFade = keyframes({
'0%': { opacity: 0, transform: 'translateY(-2px)' },
'100%': { opacity: 1, transform: 'translateY(0)' },
});
const slideLeftAndFade = keyframes({
'0%': { opacity: 0, transform: 'translateX(2px)' },
'100%': { opacity: 1, transform: 'translateX(0)' },
});
const StyledContent = styled(TooltipPrimitive.Content, {
borderRadius: 4,
padding: '8px 13px',
fontSize: 12,
color: '#FFFFFF',
backgroundColor: '#1C1C1E',
'@media (prefers-reduced-motion: no-preference)': {
animationDuration: '400ms',
animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
animationFillMode: 'forwards',
willChange: 'transform, opacity',
'&[data-state="delayed-open"]': {
'&[data-side="top"]': { animationName: slideDownAndFade },
'&[data-side="right"]': { animationName: slideLeftAndFade },
'&[data-side="bottom"]': { animationName: slideUpAndFade },
'&[data-side="left"]': { animationName: slideRightAndFade },
},
},
});
const StyledArrow = styled(TooltipPrimitive.Arrow, {
fill: '#1C1C1E',
});
export const TooltipProvider = TooltipPrimitive.Provider;
export const Tooltip = TooltipPrimitive.Root;
export const TooltipTrigger = TooltipPrimitive.Trigger;
export const TooltipContent = StyledContent;
export const TooltipArrow = StyledArrow;
type TooltipWrappedProps = {
tooltipContent: string,
tooltipSide ?: TooltipPrimitive.TooltipContentProps['side'],
align?: TooltipPrimitive.TooltipContentProps['align'],
alignOffset?: TooltipPrimitive.TooltipContentProps['alignOffset']
arrowStyles?: TooltipPrimitive.TooltipArrowProps['style']
style?: TooltipPrimitive.TooltipContentProps['style']
}
export const TooltipWrapped: FC<TooltipWrappedProps> = ({children, tooltipContent, tooltipSide, ...props}) => {
return (
<Tooltip>
<TooltipTrigger asChild>
{children}
</TooltipTrigger>
<TooltipContent sideOffset={5} side={tooltipSide} {...props} >
{tooltipContent}
<TooltipArrow style={props.arrowStyles} />
</TooltipContent>
</Tooltip>
)
}

View file

@ -1,3 +1,5 @@
import { DotsThree, DotsThreeVertical } from 'phosphor-react'
type Orientation = 'horizontal' | 'vertical'
type MoreOptionsIconProps = {
@ -7,48 +9,9 @@ type MoreOptionsIconProps = {
}
export function MoreOptionsIcon(props: MoreOptionsIconProps): JSX.Element {
const dots =
props.orientation == 'horizontal' ? (
<>
<circle
cx="7"
cy="12"
r="1.5"
transform="rotate(-90 7 12)"
fill={props.strokeColor}
/>
<circle
cx="12"
cy="12"
r="1.5"
transform="rotate(-90 12 12)"
fill={props.strokeColor}
/>
<circle
cx="17"
cy="12"
r="1.5"
transform="rotate(-90 17 12)"
fill={props.strokeColor}
/>
</>
return props.orientation == 'horizontal' ? (
<DotsThree size={props.size} color={props.strokeColor}/>
) : (
<>
<circle cx="12" cy="6" r="1.5" fill={props.strokeColor} />
<circle cx="12" cy="12" r="1.5" fill={props.strokeColor} />
<circle cx="12" cy="18" r="1.5" fill={props.strokeColor} />
</>
<DotsThreeVertical size={props.size} color={props.strokeColor}/>
)
return (
<svg
width={props.size}
height={props.size}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{dots}
</svg>
)
}

View file

@ -0,0 +1,31 @@
import { gql } from 'graphql-request'
import { gqlFetcher } from '../networkHelpers'
export async function deleteNewsletterEmailMutation(
newsletterEmailId: string
): Promise<string | undefined> {
const mutation = gql`
mutation DeleteNewsletterEmailMutation($newsletterEmailId: ID!) {
deleteNewsletterEmail(newsletterEmailId: $newsletterEmailId) {
... on DeleteNewsletterEmailSuccess {
newsletterEmail {
id
address
}
}
... on DeleteNewsletterEmailError {
errorCodes
}
}
}
`
try {
const data = await gqlFetcher(mutation, { newsletterEmailId })
console.log('delete email', data)
return 'data'
} catch (error) {
console.log('deleteNewsletterEmailMutation error', error)
return undefined
}
}

View file

@ -23,6 +23,7 @@
"@radix-ui/react-id": "^0.1.1",
"@radix-ui/react-popover": "^0.1.1",
"@radix-ui/react-separator": "^0.1.0",
"@radix-ui/react-tooltip": "^0.1.7",
"@segment/analytics-next": "^1.33.5",
"@sentry/nextjs": "^6.16.1",
"@stitches/react": "^1.2.5",

View file

@ -8,6 +8,7 @@ import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { Analytics, AnalyticsBrowser } from '@segment/analytics-next'
import { segmentApiKey } from '../lib/appConfig'
import { TooltipProvider } from '../components/elements/Tooltip'
function OmnivoreApp({ Component, pageProps }: AppProps): JSX.Element {
const router = useRouter()
@ -48,7 +49,9 @@ function OmnivoreApp({ Component, pageProps }: AppProps): JSX.Element {
defaultLocale="en"
messages={englishTranslations}
>
<Component {...pageProps} />
<TooltipProvider delayDuration={200}>
<Component {...pageProps} />
</TooltipProvider>
</IntlProvider>
</IdProvider>
)

View file

@ -2,19 +2,31 @@ import { PrimaryLayout } from '../../components/templates/PrimaryLayout'
import { Button } from '../../components/elements/Button'
import { useGetNewsletterEmailsQuery } from '../../lib/networking/queries/useGetNewsletterEmailsQuery'
import { createNewsletterEmailMutation } from '../../lib/networking/mutations/createNewsletterEmailMutation'
import { CopyLinkIcon } from '../../components/elements/images/CopyLinkIcon'
import { theme } from '../../components/tokens/stitches.config'
import { Box, HStack, VStack } from '../../components/elements/LayoutPrimitives'
import { deleteNewsletterEmailMutation } from '../../lib/networking/mutations/deleteNewsletterEmailMutation'
import { MoreOptionsIcon } from '../../components/elements/images/MoreOptionsIcon'
import { Info, Plus, Trash, Copy } from 'phosphor-react'
import {
Dropdown,
DropdownOption,
} from '../../components/elements/DropdownElements'
import { TooltipWrapped } from '../../components/elements/Tooltip'
import { theme, styled } from '../../components/tokens/stitches.config'
import {
Box,
SpanBox,
HStack,
VStack,
} from '../../components/elements/LayoutPrimitives'
import { useCopyLink } from '../../lib/hooks/useCopyLink'
import { toast, Toaster } from 'react-hot-toast'
import { useCallback } from 'react'
import Link from 'next/link'
import { StyledText } from '../../components/elements/StyledText'
import { applyStoredTheme } from '../../lib/themeUpdater'
import Link from 'next/link'
enum TextType {
EmailAddress,
ConfirmationCode
ConfirmationCode,
}
type CopyTextButtonProps = {
@ -22,76 +34,401 @@ type CopyTextButtonProps = {
type: TextType
}
const HeaderWrapper = styled(Box, {
display: 'none',
width: '863px',
'@md': {
display: 'block',
},
})
const TableCard = styled(Box, {
backgroundColor: '$grayBg',
display: 'flex',
alignItems: 'center',
padding: '10px 12px',
border: '0.5px solid $grayBgActive',
width: '100%',
'&:hover': {
border: '0.5px solid #FFD234',
},
'@md': {
paddingLeft: '0',
width: '863px',
},
})
const TableHeading = styled(Box, {
backgroundColor: '$grayBgActive',
border: '1px solid rgba(0, 0, 0, 0.06)',
display: 'flex',
alignItems: 'center',
padding: '14px 0 14px 40px',
borderRadius: '5px 5px 0px 0px',
width: '863px',
})
const Input = styled('input', {
backgroundColor: 'transparent',
color: '$grayTextContrast',
marginTop: '5px',
marginLeft: '38px',
'&[disabled]': {
border: 'none',
},
})
const CopyTextBtnWrapper = styled(Box, {
padding: '1px',
background: '$grayBgActive',
borderRadius: '6px',
border: '1px solid rgba(0, 0, 0, 0.06)',
})
const MobileBtnWrapper = styled(Box, {
display: 'flex',
position: 'fixed',
bottom: '16px',
right: '25px',
'@md': {
display: 'none',
},
})
const InfoIcon = styled(Info, {
marginTop: '8px',
'&:hover': {
cursor: 'pointer',
},
})
const TooltipStyle = {
backgroundColor: '#F9D354',
color: '#0A0806',
}
const MoreOptions = ({ onDelete }: { onDelete: () => void }) => (
<Dropdown
align={'end'}
triggerElement={
<Box
css={{
'&:hover': {
cursor: 'pointer',
},
}}
>
<MoreOptionsIcon
size={24}
strokeColor={theme.colors.grayTextContrast.toString()}
orientation="horizontal"
/>
</Box>
}
>
<DropdownOption
onSelect={() => {
return true
}}
>
<HStack alignment={'center'} distribution={'start'}>
<Trash size={24} color={theme.colors.omnivoreRed.toString()} />
<Button
css={{
color: theme.colors.omnivoreRed.toString(),
marginLeft: '8px',
border: 'none',
backgroundColor: 'transparent',
'&:hover': {
border: 'none',
backgroundColor: 'transparent',
},
}}
onClick={onDelete}
>
Delete
</Button>
</HStack>
</DropdownOption>
</Dropdown>
)
function CopyTextButton(props: CopyTextButtonProps): JSX.Element {
const { copyLink, isLinkCopied } = useCopyLink(props.text,
'newsletter_' + (props.type == TextType.EmailAddress ? 'email_address' : 'confirmation_code')
const { copyLink, isLinkCopied } = useCopyLink(
props.text,
'newsletter_' +
(props.type == TextType.EmailAddress
? 'email_address'
: 'confirmation_code')
)
const copy = useCallback(() => {
copyLink()
toast(props.type == TextType.EmailAddress ? 'Email Address Copied' : 'Confirmation Code Copied')
toast(
props.type == TextType.EmailAddress
? 'Email Address Copied'
: 'Confirmation Code Copied'
)
}, [])
return (
<Button style="plainIcon" onClick={copy}><CopyLinkIcon strokeColor={theme.colors.grayText.toString()} isCompleted={isLinkCopied} /></Button>
<Button style="plainIcon" onClick={copy}>
<Copy color={theme.colors.grayTextContrast.toString()} />
</Button>
)
}
export default function EmailsPage(): JSX.Element {
const { emailAddresses, revalidate, isValidating } = useGetNewsletterEmailsQuery()
const { emailAddresses, revalidate, isValidating } =
useGetNewsletterEmailsQuery()
applyStoredTheme(false)
async function createEmail(): Promise<void> {
await createNewsletterEmailMutation()
revalidate()
toast.success('Email Created')
}
async function deleteEmail(id: string): Promise<void> {
await deleteNewsletterEmailMutation(id)
revalidate()
toast.success('Email Deleted!')
}
return (
<PrimaryLayout pageTestId="settings-emails-tag">
<Toaster />
<Toaster
containerStyle={{
top: '5rem',
}}
/>
<VStack
distribution=
"center"
distribution="center"
css={{
mx: '42px',
maxWidth: '640px',
mx: '10px',
maxWidth: '865px',
color: '$grayText',
'@smDown': {
paddingBottom: '5rem',
paddingTop: '2rem',
'@md': {
m: '16px',
maxWidth: '85%',
alignSelf: 'center',
maxWidth: '72%',
mx: '42px',
paddingTop: '0',
},
}}
>
<StyledText style='fixedHeadline' css={{ mb: '8px' }}>Email Addresses</StyledText>
<StyledText style="body" css={{ mb: '42px', mt: '0px' }}>
Add PDFs to your library, or subscribe to emails using an Omnivore email address.
</StyledText>
{emailAddresses && emailAddresses.map((email) => {
<HeaderWrapper>
<Box style={{ display: 'flex', alignItems: 'center' }}>
<Box>
<StyledText
style="fixedHeadline"
css={{
fontSize: '24px',
fontWeight: '700',
}}
>
Email Addresses{' '}
</StyledText>
</Box>
<Box style={{ flex: '1', marginLeft: '9px' }}>
<Link passHref href="/help/newsletters">
<a style={{ textDecoration: 'none' }}>
<TooltipWrapped
tooltipContent="Learn More"
tooltipSide={'top'}
style={TooltipStyle}
arrowStyles={{ fill: '#F9D354' }}
>
<InfoIcon size={24} css={{ color: '$grayText' }} />
</TooltipWrapped>
</a>
</Link>
</Box>
<Box>
<Button
onClick={createEmail}
style="ctaDarkYellow"
css={{
display: 'flex',
alignItems: 'center',
}}
>
<Plus size={18} style={{ marginRight: '6.5px' }} />
<SpanBox>Add Email</SpanBox>
</Button>
</Box>
</Box>
<TableHeading>
<Box
css={{
flex: '49%',
}}
>
<StyledText
style="highlightTitle"
css={{
color: '$grayTextContrast',
}}
>
EMAIL
</StyledText>
</Box>
<Box style={{ flex: '51%' }}>
<StyledText
style="highlightTitle"
css={{
color: '$grayTextContrast',
}}
>
CONFIRMATION CODE
</StyledText>
</Box>
</TableHeading>
</HeaderWrapper>
{emailAddresses &&
emailAddresses.map((email, i) => {
const { address, confirmationCode, id } = email
const isLastChild = i === emailAddresses.length - 1
return (
<Box css={{ marginBottom: '26px' }} key={email.id}>
<HStack distribution='end' css={{ verticalAlign: 'middle' }}>
<input type="text" value={email.address} disabled style={{width: "280px"}}></input>
<CopyTextButton text={email.address} type={TextType.EmailAddress} />
</HStack>
<HStack distribution='end' css={{ verticalAlign: 'middle' }}>
{email.confirmationCode && (
<>
<input type="text" value="3rwfdsdf" disabled style={{width: "120px"}}></input>
<CopyTextButton text={email.confirmationCode} type={TextType.ConfirmationCode} />
</>
<TableCard
key={id}
css={{
'&:hover': {
background: 'rgba(255, 234, 159, 0.12)',
},
'@mdDown': {
borderTopLeftRadius: i === 0 ? '5px' : '',
borderTopRightRadius: i === 0 ? '5px' : '',
},
borderBottomLeftRadius: isLastChild ? '5px' : '',
borderBottomRightRadius: isLastChild ? '5px' : '',
}}
>
<Box
css={{
display: 'flex',
flexDirection: 'column',
width: '100%',
'@md': {
flexDirection: 'row',
},
}}
>
<HStack
distribution="start"
css={{
display: 'flex',
padding: '4px 4px 4px 0px',
}}
>
<Input
type="text"
value={address}
disabled
css={{
marginLeft: '0',
width: '100%',
'@md': {
marginLeft: '38px',
width: '320px',
},
}}
></Input>
<CopyTextBtnWrapper
css={{
'@mdDown': {
marginRight: '10px',
marginLeft: '18px',
},
}}
>
<CopyTextButton
text={address}
type={TextType.EmailAddress}
/>
</CopyTextBtnWrapper>
<Box
css={{
textAlign: 'right',
display: 'flex',
'@md': {
display: 'none',
},
}}
>
<MoreOptions onDelete={() => deleteEmail(id)} />
</Box>
</HStack>
{confirmationCode && (
<HStack
distribution="start"
css={{
display: 'flex',
backgroundColor: '$grayBgActive',
borderRadius: '6px',
padding: '8px 4px 4px 7px',
'@md': {
backgroundColor: 'transparent',
},
}}
>
<>
<Input
type="text"
value={confirmationCode}
disabled
style={{ flex: '1' }}
></Input>
<Box>
<CopyTextBtnWrapper
css={{
border: 'none',
}}
>
<CopyTextButton
text={confirmationCode}
type={TextType.ConfirmationCode}
/>
</CopyTextBtnWrapper>
</Box>
</>
</HStack>
)}
</Box>
<HStack distribution={'start'}>
<Box
css={{
textAlign: 'right',
display: 'none',
'@md': {
display: 'flex',
},
}}
>
<MoreOptions onDelete={() => deleteEmail(id)} />
</Box>
</HStack>
</Box>
</TableCard>
)
})}
<Box style={{ width: '100%', color: 'green' }}>
<Button onClick={createEmail} style="ctaDarkYellow">
Create new Email
})}
<MobileBtnWrapper>
<Button
onClick={createEmail}
style="ctaDarkYellow"
css={{
display: 'flex',
border: '1px solid $grayBorder',
borderRadius: '8px',
}}
>
<Plus size={24} />
</Button>
</Box>
<p><Link href="/help/newsletters">Lean more about setting up Newsletter Email Addresses</Link></p>
</MobileBtnWrapper>
</VStack>
</PrimaryLayout>
)

View file

@ -4485,6 +4485,13 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-previous@0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-0.1.1.tgz#0226017f72267200f6e832a7103760e96a6db5d0"
integrity sha512-O/ZgrDBr11dR8rhO59ED8s5zIXBRFi8MiS+CmFGfi7MJYdLbfqVOmQU90Ghf87aifEgWe6380LA69KBneaShAg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-rect@0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-0.1.1.tgz#6c15384beee59c086e75b89a7e66f3d2e583a856"
@ -4507,6 +4514,14 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-visually-hidden@0.1.4":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-0.1.4.tgz#6c75eae34fb5d084b503506fbfc05587ced05f03"
integrity sha512-K/q6AEEzqeeEq/T0NPChvBqnwlp8Tl4NnQdrI/y8IOY7BRR+Ug0PEsVk6g48HJ7cA1//COugdxXXVVK/m0X1mA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "0.1.4"
"@radix-ui/rect@0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-0.1.1.tgz#95b5ba51f469bea6b1b841e2d427e17e37d38419"