From 9f20920ddc751b717a747830348341bbe1e169c2 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 25 Jan 2023 16:50:39 +0800 Subject: [PATCH] Initial work on the recent emails feature --- .../networking/queries/useGetRecentEmails.tsx | 78 ++++++ packages/web/pages/settings/emails/index.tsx | 234 ++++++++++++++++++ packages/web/pages/settings/emails/recent.tsx | 196 +++++++++++++++ 3 files changed, 508 insertions(+) create mode 100644 packages/web/lib/networking/queries/useGetRecentEmails.tsx create mode 100644 packages/web/pages/settings/emails/index.tsx create mode 100644 packages/web/pages/settings/emails/recent.tsx diff --git a/packages/web/lib/networking/queries/useGetRecentEmails.tsx b/packages/web/lib/networking/queries/useGetRecentEmails.tsx new file mode 100644 index 000000000..a8b10aee8 --- /dev/null +++ b/packages/web/lib/networking/queries/useGetRecentEmails.tsx @@ -0,0 +1,78 @@ +import { gql } from 'graphql-request' +import useSWR from 'swr' +import { publicGqlFetcher } from '../networkHelpers' + +export interface RecentEmail { + id: string + from: string + to: string + subject: string + type: string + text: string + createdAt: string +} + +interface RecentEmailsResponse { + isValidating: boolean + recentEmails: RecentEmail[] + revalidate: () => void +} + +interface RecentEmailsResponseData { + recentEmails: RecentEmailsData +} + +interface RecentEmailsData { + recentEmails: RecentEmail[] +} + +export function useGetRecentEmailsQuery(): RecentEmailsResponse { + const query = gql` + query GetRecentEmails { + recentEmails { + ... on RecentEmailsSuccess { + recentEmails { + id + from + to + subject + type + text + createdAt + } + } + ... on RecentEmailsError { + errorCodes + } + } + } + ` + + const { data, mutate, error, isValidating } = useSWR(query, publicGqlFetcher) + + try { + if (error) { + throw error + } + + if (data) { + const result = data as RecentEmailsResponseData + const recentEmails = result.recentEmails.recentEmails as RecentEmail[] + return { + isValidating, + recentEmails, + revalidate: () => { + mutate() + }, + } + } + } catch (error) { + console.log('error', error) + } + return { + isValidating: false, + recentEmails: [], + // eslint-disable-next-line @typescript-eslint/no-empty-function + revalidate: () => {}, + } +} diff --git a/packages/web/pages/settings/emails/index.tsx b/packages/web/pages/settings/emails/index.tsx new file mode 100644 index 000000000..4f064d53e --- /dev/null +++ b/packages/web/pages/settings/emails/index.tsx @@ -0,0 +1,234 @@ +import { Button } from '../../../components/elements/Button' +import { useGetNewsletterEmailsQuery } from '../../../lib/networking/queries/useGetNewsletterEmailsQuery' +import { createNewsletterEmailMutation } from '../../../lib/networking/mutations/createNewsletterEmailMutation' +import { deleteNewsletterEmailMutation } from '../../../lib/networking/mutations/deleteNewsletterEmailMutation' +import { MoreOptionsIcon } from '../../../components/elements/images/MoreOptionsIcon' +import { Trash, Copy } from 'phosphor-react' +import { + Dropdown, + DropdownOption, +} from '../../../components/elements/DropdownElements' +import { theme, styled } from '../../../components/tokens/stitches.config' +import { Box, HStack } from '../../../components/elements/LayoutPrimitives' +import { useCopyLink } from '../../../lib/hooks/useCopyLink' +import { useCallback, useMemo, useState } from 'react' +import { StyledText } from '../../../components/elements/StyledText' +import { applyStoredTheme } from '../../../lib/themeUpdater' +import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' +import { formattedShortDate } from '../../../lib/dateFormatting' +import Link from 'next/link' +import { + EmptySettingsRow, + SettingsTable, + SettingsTableRow, +} from '../../../components/templates/settings/SettingsTable' +import { ConfirmationModal } from '../../../components/patterns/ConfirmationModal' + +enum TextType { + EmailAddress, + ConfirmationCode, +} + +type CopyTextButtonProps = { + text: string + type: TextType +} + +const CopyTextBtnWrapper = styled(Box, { + background: '$grayBgActive', + borderRadius: '6px', + border: '1px solid rgba(0, 0, 0, 0.06)', + width: '32px', + height: '32px', + + display: 'flex', + + color: '#3D3D3D', + + alignItems: 'center', + justifyContent: 'center', +}) + +function CopyTextButton(props: CopyTextButtonProps): JSX.Element { + const { copyLink, isLinkCopied } = useCopyLink( + props.text, + 'newsletter_' + + (props.type == TextType.EmailAddress + ? 'email_address' + : 'confirmation_code') + ) + + const copy = useCallback(() => { + copyLink() + showSuccessToast( + props.type == TextType.EmailAddress + ? 'Email Address Copied' + : 'Confirmation Code Copied' + ) + }, []) + + return ( + + ) +} + +export default function EmailsPage(): JSX.Element { + const { emailAddresses, revalidate, isValidating } = + useGetNewsletterEmailsQuery() + const [confirmDeleteEmailId, setConfirmDeleteEmailId] = useState< + undefined | string + >(undefined) + + applyStoredTheme(false) + + async function createEmail(): Promise { + const email = await createNewsletterEmailMutation() + if (!email) { + showErrorToast('Error Creating Email') + return + } + showSuccessToast('Email Created') + revalidate() + } + + async function deleteEmail(id: string): Promise { + const result = await deleteNewsletterEmailMutation(id) + if (!result) { + showErrorToast('Error Deleting Email') + return + } + revalidate() + showSuccessToast('Email Deleted') + } + + const sortedEmailAddresses = useMemo(() => { + if (!emailAddresses) { + return [] + } + return emailAddresses.sort((a, b) => a.createdAt.localeCompare(b.createdAt)) + }, [emailAddresses]) + + return ( + <> + + {sortedEmailAddresses.length > 0 ? ( + sortedEmailAddresses.map((email, i) => { + return ( + setConfirmDeleteEmailId(email.id)} + deleteTitle="Delete" + sublineElement={ + + {`created ${formattedShortDate(email.createdAt)}, `} + {`${email.subscriptionCount} subscriptions`} + + } + titleElement={ + + + + } + extraElement={ + email.confirmationCode ? ( + + <> + + {`Gmail: ${email.confirmationCode}`} + + + + + + + + + ) : ( + <> + ) + } + /> + ) + }) + ) : ( + + )} + + + {confirmDeleteEmailId ? ( + { + await deleteEmail(confirmDeleteEmailId) + setConfirmDeleteEmailId(undefined) + }} + onOpenChange={() => setConfirmDeleteEmailId(undefined)} + /> + ) : null} + + ) +} diff --git a/packages/web/pages/settings/emails/recent.tsx b/packages/web/pages/settings/emails/recent.tsx new file mode 100644 index 000000000..d5eec181c --- /dev/null +++ b/packages/web/pages/settings/emails/recent.tsx @@ -0,0 +1,196 @@ +import { useEffect, useMemo, useState } from 'react' +import { useRouter } from 'next/router' + +import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' +import { applyStoredTheme } from '../../../lib/themeUpdater' + +import { FormInputProps } from '../../../components/elements/FormElements' +import { + EmptySettingsRow, + SettingsTable, + SettingsTableRow, +} from '../../../components/templates/settings/SettingsTable' +import { StyledText } from '../../../components/elements/StyledText' +import { formattedShortDate } from '../../../lib/dateFormatting' +import { + RecentEmail, + useGetRecentEmailsQuery, +} from '../../../lib/networking/queries/useGetRecentEmails' +import Link from 'next/link' + +export default function RecentEmails(): JSX.Element { + const { recentEmails, revalidate, isValidating } = useGetRecentEmailsQuery() + const [onDeleteId, setOnDeleteId] = useState('') + const [addModalOpen, setAddModalOpen] = useState(false) + const [name, setName] = useState('') + const [value, setValue] = useState('') + const [expiresAt, setExpiresAt] = useState(new Date()) + const [formInputs, setFormInputs] = useState([]) + const [apiKeyGenerated, setApiKeyGenerated] = useState('') + const neverExpiresDate = new Date(8640000000000000) + const defaultExpiresAt = 'Never' + + applyStoredTheme(false) + + // async function onDelete(id: string): Promise { + // const result = await revokeApiKeyMutation(id) + // if (result) { + // showSuccessToast('API Key deleted', { position: 'bottom-right' }) + // } else { + // showErrorToast('Failed to delete', { position: 'bottom-right' }) + // } + // revalidate() + // } + + // async function onCreate(): Promise { + // const result = await generateApiKeyMutation({ name, expiresAt }) + // if (result) { + // setApiKeyGenerated(result) + // showSuccessToast('API key generated', { position: 'bottom-right' }) + // } else { + // showErrorToast('Failed to add', { position: 'bottom-right' }) + // } + // revalidate() + // } + + // function onAdd() { + // return setFormInputs([ + // { + // label: 'Name', + // onChange: setName, + // name: 'name', + // value: value, + // required: true, + // }, + // { + // label: 'Expires', + // name: 'expiredAt', + // required: true, + // onChange: (e) => { + // console.log('onChange: ', e) + // let additionalDays = 0 + // switch (e.target.value) { + // case 'in 7 days': + // additionalDays = 7 + // break + // case 'in 30 days': + // additionalDays = 30 + // break + // case 'in 90 days': + // additionalDays = 90 + // break + // case 'in 1 year': + // additionalDays = 365 + // break + // case 'Never': + // break + // } + // const newExpires = additionalDays ? new Date() : neverExpiresDate + // if (additionalDays) { + // newExpires.setDate(newExpires.getDate() + additionalDays) + // } + // setExpiresAt(newExpires) + // }, + // type: 'select', + // options: [ + // 'in 7 days', + // 'in 30 days', + // 'in 90 days', + // 'in 1 year', + // 'Never', + // ], + // value: defaultExpiresAt, + // }, + // ]) + // } + + const sortedRecentEmails = useMemo(() => { + if (!recentEmails) { + return [] + } + return recentEmails.sort((a, b) => a.createdAt.localeCompare(b.createdAt)) + }, [recentEmails]) + + return ( + + {sortedRecentEmails.length > 0 ? ( + sortedRecentEmails.map((recentEmail: RecentEmail, i) => { + return ( + { + console.log('onDelete triggered: ', recentEmail.id) + setOnDeleteId(recentEmail.id) + }} + deleteTitle="Delete" + sublineElement={ + + {`From ${formattedShortDate(recentEmail.from)},`} + {`Received ${formattedShortDate(recentEmail.createdAt)} at `} + + {recentEmail.to} + + + } + /> + ) + }) + ) : ( + + )} + {/* + {addModalOpen && ( + + )} + + {apiKeyGenerated && ( + { + await navigator.clipboard.writeText(apiKeyGenerated) + setApiKeyGenerated('') + }} + onOpenChange={() => setApiKeyGenerated('')} + /> + )} + + {onDeleteId && ( + { + await onDelete(onDeleteId) + setOnDeleteId('') + }} + onOpenChange={() => setOnDeleteId('')} + /> + )} */} + + ) +}